diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e87fde5c..a0e4af0b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -204,7 +204,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Python module-level assignments now contribute the calls their right-hand side makes — `app = FastAPI()`, `ENGINE = create_engine(url)`, `handler = lambda: run()`, a registry dict or list of handlers. Previously everything a module wires up at import time was missing from the graph, so the objects it builds looked unreferenced. Re-index with `codegraph index -f` after upgrading to pick up the new edges. - Rust `const` and `static` initializers now contribute their calls: `static REGISTRY: Lazy = Lazy::new(|| build())`, `const LEN: usize = compute_len()`. Previously anything a lazily-built singleton or a computed constant called was missing from the graph entirely. Re-index with `codegraph index -f` after upgrading to pick up the new edges. - Scala `val`/`var` definitions now contribute the calls their initializer makes — `val handler = () => process(msg)`, `val client = buildClient()`, `lazy val engine = start()`. Previously everything on the right-hand side was dropped, which on a val-heavy codebase (SpinalHDL hardware descriptions, Akka wiring) is most of the wiring: a 32-file SpinalHDL project gained 812 references it had been missing. Re-index with `codegraph index -f` after upgrading to pick up the new edges. -- TypeScript and JavaScript module-level declarations now name themselves as the caller of whatever their initializer runs. `const cfg = loadConfig()` recorded the *file* as loadConfig's caller, which is no use for callers or impact; it now records `cfg`. And an object literal that wasn't exported — `const handlers = { onSave: () => persist() }` — was skipped entirely, so nothing inside it reached the graph at all. Re-index with `codegraph index -f` after upgrading to pick up the new edges. +- TypeScript and JavaScript module-level declarations now name themselves as the caller of whatever their initializer runs. `const cfg = loadConfig()` recorded the *file* as loadConfig's caller, which is no use for callers or impact; it now records `cfg`, and the viewer's entry points and file screens still count that call as the file's top-level code. And an object literal that wasn't exported — `const handlers = { onSave: () => persist() }` — was skipped entirely, so nothing inside it reached the graph at all. Re-index with `codegraph index -f` after upgrading to pick up the new edges. - `codegraph_explore` again lists a dynamic-dispatch link when the same two symbols are also joined by an ordinary call, instead of dropping it from the summary. - Java fields initialized with a lambda or an anonymous class — `private final Runnable r = () -> doWork();`, `new LocationListener() { … }`, `Parcelable.Creator` — now contribute call edges, and the anonymous class and its overrides become real symbols instead of being invisible. Previously everything inside a field initializer was dropped, so a method reached only from one looked like it had no callers. Re-index with `codegraph index -f` after upgrading to pick up the new edges. - Kotlin `init { }` blocks and destructuring declarations no longer swallow their code: `init { val cfg = load() }` and `val (a, b) = makePair()` contributed no call edge at all, and now attribute to the enclosing class or file. diff --git a/src/db/queries.ts b/src/db/queries.ts index 4e2990c7c..95f198751 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -2184,6 +2184,12 @@ export class QueryBuilder { * build script does its work on the way down the file. `instantiates` counts * the same way — `new Server(...)` at module scope is the same act. * + * A call made while initializing a module-level `variable` / `constant` — + * `const service = new Service()`, `app = FastAPI()` — is attributed to the + * declared name (#693), not to the file, so the file's own edges alone would + * miss most of what a real entry point runs. Those names are the file's + * top-level code too, so `tops` counts them alongside the file node. + * * Ranking multiplies the two things an entry point does: it runs (calls), and * it wires the project together (distinct other files its symbols reach). One * alone is misleading — a registration table makes hundreds of module-level @@ -2196,12 +2202,25 @@ export class QueryBuilder { if (limit <= 0) return []; return this.db .prepare( - `WITH runs AS ( - SELECT e.source AS id, COUNT(*) AS calls - FROM edges e - JOIN nodes n ON n.id = e.source - WHERE n.kind = 'file' AND e.kind IN ('calls', 'instantiates') - GROUP BY e.source + `WITH tops AS ( + SELECT n.id AS file_id, n.id AS src + FROM nodes n + WHERE n.kind = 'file' + UNION ALL + SELECT c.source AS file_id, c.target AS src + FROM edges c + JOIN nodes f ON f.id = c.source + JOIN nodes v ON v.id = c.target + WHERE c.kind = 'contains' + AND f.kind = 'file' + AND v.kind IN ('variable', 'constant') + ), + runs AS ( + SELECT t.file_id AS id, COUNT(*) AS calls + FROM tops t + JOIN edges e ON e.source = t.src + WHERE e.kind IN ('calls', 'instantiates') + GROUP BY t.file_id ), cand AS ( SELECT r.id AS id, n.file_path AS fp, r.calls AS calls diff --git a/src/ui-server/api/file.ts b/src/ui-server/api/file.ts index 14f5e19ed..0dbab8176 100644 --- a/src/ui-server/api/file.ts +++ b/src/ui-server/api/file.ts @@ -66,6 +66,7 @@ export function buildFile(cg: CodeGraph, projectRoot: string, requested: string) const nodes = cg.getNodesInFile(storedPath); const nodeIds = nodes.map((n) => n.id); const inThisFile = new Set(nodeIds); + const nodeKindById = new Map(nodes.map((n) => [n.id, n.kind])); const fileNode = nodes.find((n) => n.kind === 'file') ?? null; // --------------------------------------------------------------------------- @@ -106,8 +107,20 @@ export function buildFile(cg: CodeGraph, projectRoot: string, requested: string) // the same signal `/api/entrypoints` ranks on. It is worth a line on this // screen because the outline cannot show it: top-level code belongs to no // symbol, so the only way to read it is to open the file node itself. + // A call made while initializing a module-level variable or constant is + // attributed to that name (#693), so those names are top-level code too and + // are counted with the file — the same set `getTopCallingFiles` ranks on. + const moduleLevelValueIds = fileNode + ? cg + .getOutgoingEdgesFrom([fileNode.id], ['contains']) + .map((e) => e.target) + .filter((id) => { + const kind = nodeKindById.get(id); + return kind === 'variable' || kind === 'constant'; + }) + : []; const topLevelEdges = fileNode - ? cg.getOutgoingEdgesFrom([fileNode.id], ['calls', 'instantiates']) + ? cg.getOutgoingEdgesFrom([fileNode.id, ...moduleLevelValueIds], ['calls', 'instantiates']) : []; return {