Find functions that are entry points in the graph. An entry point is a Function/Test node that either: 1. Has no incoming CALLS edges (true root), or 2. Has a framework decorator (e.g. ``@app.get``), or 3. Matches a conventional name pattern (``main``, ``test_*``, etc.). When *
(
store: GraphStore,
include_tests: bool = False,
)
| 148 | |
| 149 | |
| 150 | def detect_entry_points( |
| 151 | store: GraphStore, |
| 152 | include_tests: bool = False, |
| 153 | ) -> list[GraphNode]: |
| 154 | """Find functions that are entry points in the graph. |
| 155 | |
| 156 | An entry point is a Function/Test node that either: |
| 157 | 1. Has no incoming CALLS edges (true root), or |
| 158 | 2. Has a framework decorator (e.g. ``@app.get``), or |
| 159 | 3. Matches a conventional name pattern (``main``, ``test_*``, etc.). |
| 160 | |
| 161 | When *include_tests* is False (the default), Test nodes are excluded so |
| 162 | that flow analysis focuses on production entry points. |
| 163 | """ |
| 164 | # Build a set of all qualified names that are CALLS targets. Exclude |
| 165 | # edges sourced at File nodes so that script-/notebook-/top-level-only |
| 166 | # callees (e.g. ``run_job()`` invoked from module scope, a top-level |
| 167 | # ``<App />`` render) remain detectable as entry points. |
| 168 | called_qnames = store.get_all_call_targets(include_file_sources=False) |
| 169 | |
| 170 | # Scan all nodes for entry-point candidates. |
| 171 | candidate_nodes = store.get_nodes_by_kind(["Function", "Test"]) |
| 172 | |
| 173 | entry_points: list[GraphNode] = [] |
| 174 | seen_qn: set[str] = set() |
| 175 | |
| 176 | for node in candidate_nodes: |
| 177 | if not include_tests and (node.is_test or _is_test_file(node.file_path)): |
| 178 | continue |
| 179 | |
| 180 | is_entry = False |
| 181 | |
| 182 | # True root: no one calls this function. |
| 183 | if node.qualified_name not in called_qnames: |
| 184 | is_entry = True |
| 185 | |
| 186 | # Framework decorator match. |
| 187 | if _has_framework_decorator(node): |
| 188 | is_entry = True |
| 189 | |
| 190 | # Conventional name match. |
| 191 | if _matches_entry_name(node): |
| 192 | is_entry = True |
| 193 | |
| 194 | if is_entry and node.qualified_name not in seen_qn: |
| 195 | entry_points.append(node) |
| 196 | seen_qn.add(node.qualified_name) |
| 197 | |
| 198 | return entry_points |
| 199 | |
| 200 | |
| 201 | # --------------------------------------------------------------------------- |