mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
df1cd1e18596e393bfd09f23d44fd5fe00a68724
1644
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df1cd1e185 |
fix(pascal): Delphi units with re-published properties and similar syntax parse whole (#2816)
SynEdit's main unit parsed into one ERROR node on main. A re-published
property, `property OwnsObjects;` at line 303, is one of several Delphi
constructs tree-sitter-pascal has no rule for. Each breaks the parse
locally, and enough of them cost a whole unit: routines after a break
lost their calls or had them credited to the file, their locals showed
up as unit variables, and class members after an unnamed record went
missing.
The Pascal pre-parse now fixes each construct in place, offsets and
newlines kept (signatures still read the source as written):
- a re-published property (`property Cursor default crIBeam;`) is blanked;
- a conditional right after a routine's header that holds no whole body
(`{$IFDEF HASINLINE}inline;{$ENDIF}`) has its directives blanked, and
so does one nested at the start of a conditional that holds no block;
- branches that open the same `begin` keep only the first one, and an
assembler version that opens a body beside a Pascal one is blanked;
- a re-raise is blanked, or written as `;` right before an `end`;
- an unnamed record type (`fIn: record … end`) is written as `string`
with its fields blanked, so the declaration keeps its name;
- a label right before `end` is blanked;
- alternative statements after a case label, or after then/else/do
without a `;`, read as one block: the directives become begin, `;`
and end, so every branch keeps its calls.
The last fix also undoes a regression the header fix caused alone:
in HeidiSQL's SynRegExpr.pas a case statement full of `{$IFDEF}`
alternatives broke the unit once main's accidental resync points were
gone (659 references lost). With all fixes it has 1,712 against
main's 1,681.
Also fixed on the way: a routine nested in another credited the
constants its parent's body reads to itself, when it was the last one
before the body (the value-ref scan pulled in the next sibling, which
is the outer routine's body).
Validation: pascal-delphi-syntax (17 tests, 13 fail on main) and a new
pascal-nested-routines case (fails on main); 14 mutations, one per rule
and three guards, each fail a test. Two pascal-system-calls fixtures
broke the parse on purpose with constructs this now reads; they use an
old-style `begin … end.` initialization and an anonymous record holding
a method instead, which still break it, and still fail when the guard
they cover is removed. Parse census, main -> fix, files
without a unit node: SynEdit 4 -> 1, HeidiSQL 6 -> 1, DelphiMVCFramework
15 -> 1, mORMot2 34 -> 6, DWScript 2100 -> 2086 (its test scripts have
no unit header). Indexed A/B, edges removed/added: SynEdit 1889/2150,
HeidiSQL 2571/3305, DelphiMVCFramework 4915/6227, mORMot2 15298/27195,
DWScript 3758/4570; leveldb and SDWebImage identical. No file loses
more call sites than it gains outside DelphiMVCFramework, where the
six that do lose only wrong edges. Index time within noise (HeidiSQL
+3% wall, +8% CPU for the code now read); the pre-parse itself takes
223 ms over mORMot2's 25 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ab92c90a54 |
fix(js,ts): a super.method() call links to the method of the class's parent (#2818)
A TS/JS call written super.run() is recorded under its bare name, as
issue #2497 found. Inside an override of run every strategy found the
calling method itself, and the self-edge gate declined it, so the call
was unlinked; elsewhere it went to whatever method had the name, often
the class's own or a sibling class's in the same file, and, for a
parent outside the project like HTMLElement, Set or Promise, to a
project namesake.
A call written on super where it starts is now resolved through the
class's resolved extends edges, which the resolver persists before it
resolves calls: the nearest ancestor's method of that name, a static
one for a call in a static member, or nothing when no ancestor in the
project has it. The call may sit in a method, a class field, or an
arrow function inside either. One the index can't place in a class (a
class expression's method, an object literal's) keeps the old path, as
does one after a class keyword in the member, outside comments and
strings. The self-edge gate now reads a TS/JS call at its own column,
so a this.run() on the line of a super.run() keeps its edge.
The issue's first part, a method call on the receiver of an aliased
constructor, already resolves on main since PR #2661 and is unchanged.
Validation: the new test fails 3 of 4 on main
|
||
|
|
53f72fec90 |
fix(liquid): variables declared with {% capture %} are indexed, as assigned ones are (#2814)
The Liquid extractor indexed a variable declared with {% assign %} as a
variable node, in both spellings (braced, and as a line of a
{% liquid %} tag) and skipping comment and raw regions, but it never
read {% capture %}, Liquid's other way to declare a variable. A
captured variable could not be found: search missed it, and
codegraph_explore reported its name as not in the index.
Fix:
- extractAssignments reads capture tags through the same
findTagOccurrences scanner as assign, so both spellings and the
comment/raw rule apply. The name is word characters, as for assign,
optionally in single or double quotes (Liquid reads 'title' as
title), and must end at whitespace or the tag's end (-%} included). A
name that goes on with "-" or ".", which the assign rule doesn't read
either, gets no node rather than a cut-short one.
- Captures are read after assigns. Node ids are keyed by file, kind,
name and line, so an assign keeps its id when a capture of the same
name shares its line.
- One helper now builds the variable node and its contains edge for
both. It computes the declaration's column once, where the assign
code computed it three times, each a split of the whole file.
Validation:
- 13 new tests in __tests__/extraction.test.ts. 11 fail with main's
extractor and pass with the fix: 8 declaration forms (braced,
whitespace-trimmed, tight -%}, single- and double-quoted, {% liquid %}
lines with LF and CRLF, a quoted {% liquid %} line), an assign and two
captures of one name on one line (distinct ids, the assign keeps
its), tags inside a capture body, and an index-level test that search
finds captured variables in a non-theme folder. 2 guards pass on
both: no capture from comment, raw, inline comments, prose, a
nameless tag or a tag inside a string, and no cut-short hyphenated
or dotted name. Red/green on the committed tree: 42 passed and 11
failed with main's extractor, 53 passed with the fix.
- A/B on
|
||
|
|
68fcf2f1c7 |
fix(js,ts): static and instance methods override only their own kind (#2813)
The override-dispatch pass (interface-impl) linked a base class's
method to every subclass method of the same name, so a static method
counted as overriding an instance one, or the reverse, though a call to
one never runs the other (issue #2761). TypeScript nodes recorded
static already, so there the pass alone was wrong. JavaScript nodes
never did: the extractor had no isStatic hook and the kernel answered
nothing for the JS variants, so a JS class's static s() and s() were
both stored as instance methods.
JavaScript members now record their own static keyword with the hook
TypeScript uses (hasStaticModifier, which also keeps a method named
static() an instance one), and the kernel gives the JS variants the
same answer, so both engines agree. The pass skips a JS/TS base and
override whose static flags differ; a static method still overrides a
static one, and an instance method an instance one.
Validation: the new test fails 5 of 10 on main
|
||
|
|
efb091ec2f |
fix(liquid): editor-saved Shopify JSON and {% sections %} groups link their sections (#2812)
Shopify's theme editor writes a block comment ("IMPORTANT: The contents
of this file are auto-generated...") at the top of every JSON template
and section group it saves. The Liquid extractor read theme JSON with
JSON.parse, which rejects comments and trailing commas, so such a file
linked none of the sections it lists, and in an editor-saved theme most
sections looked unused. All 14 of Shopify/horizon's templates and
section groups carry the comment, so none of them linked. The links a
JSON file did produce all sat at line 1, column 0.
The same report found two more gaps. A layout's
{% sections 'header-group' %} tag, which renders the section group
sections/header-group.json, was never read. And a {% schema %} written
inside {% comment %} or {% raw %} was indexed as the section's own
schema, because only the tag scanner skipped those regions.
Fix:
- Read theme JSON with jsonc-parser's parseTree, which accepts
comments, and allow trailing commas. Shopify's theme docs say theme
JSON supports both, and Shopify's theme-check parses it with the same
options. Any other parse error still links nothing.
- Put each section reference at its "type" value's line and column.
- Read the plural sections tag wherever the singular one is read:
braced, whitespace-trimmed, or as a line of a {% liquid %} tag. It
links to sections/<group>.json, and its component is named
::sections:<group>, apart from a section of the same name. The
Shopify theme matcher already keeps any sections/ path inside the
file's own theme, so a group resolves only there (its doc comment now
says so).
- Compute the comment and raw regions once per file and share them
between the tag scanner and the schema extractor. A schema inside one
is skipped, and the search resumes after the region.
Validation:
- 10 new regression tests in __tests__/extraction.test.ts fail with
main's extractor and pass with the fix. Re-run on
|
||
|
|
08478eea53 |
fix(js,ts): a receiver declared outside the calling function goes by its declared type (#2811)
react-dom's root.render(<X />) linked to the project's one class
component render() when root was declared outside the calling
function, a module's let root: Root | null or a let popupRoot: Root in
the function around the one rendering, or held what createRoot(el)
returned (#2589). The receiver reader scans only the calling function,
so the type that rules a react-dom Root out was never read, and the
call went to the one same-named method by its name.
A TS/JS receiver the calling function doesn't type, in a function
nested in another or one a module-scope variable names, is now read by
its binding in scope at the call. A per-file brace-scope reader finds
the innermost binding of the name: a const/let/var, a parameter, a
destructuring, a for head, a function or class, a lone or destructured
arrow parameter. A declaration or parameter written with a type, or a
const/let/var whose whole value is a new of a class, types the call as
the calling function's own declaration would; an annotated object
literal stays its own members', and a new of a parameter names no
class. A class component's render() and lifecycle methods are never a
by-name guess for a receiver other than this or super, since React
calls them, which also covers an untyped const r = createRoot(el). A
first version that also took any value a package function returned to
have no project method was dropped: it unlinked correct calls through
useContext(...) values in social-app and outline.
Validation: the new test fails 3 of 3 on main
|
||
|
|
f3fdba2955 |
fix(pascal): calls inside nested Delphi routines are indexed, each under its routine (#2810)
A Pascal routine may declare routines of its own between its header and
its begin. The extractor walked only a routine's body block, so a nested
routine's code was never read: what it called was missing, and a call to
it from the routine that declares it went by name to any routine
elsewhere with that name (in mORMot2's tests, Test, Check and One calls
landed on other classes' methods).
A nested routine is now a function node qualified under the routine it
is nested in (TFoo::Bar::Inner), contained by it and walked with itself
as the caller. It sees the parameters and locals of the routines around
it that are declared above it. Its name means it in the routine that
declares it, in the routines nested there after it and in its own body,
ahead of the class and the unit. A call resolved to a nested routine
from anywhere else is what the call's own scope declares, or nothing. A
nested routine's bare calls reach the class of the method it is nested
in first. Where a routine's tree has errors and its parse swallowed the
unit's routines after it (column 0, or after the body), those are walked
as the unit's own; of two bodies under {$IFDEF} the first is walked, as
before.
Validation against origin/main
|
||
|
|
80c3680068 |
fix(extraction): drop the extractVariables hook, which the core never called (#2809)
Root cause: LanguageExtractor in src/extraction/tree-sitter-types.ts declared an optional extractVariables hook, documented together with the VariableInfo type it returned, but nothing ever called it. Variables are named by the core's own extractVariable, from its per-language branches and the variableTypes list. A language author who wrote the hook got nothing back, with no error and no warning; the reporter hit this while adding Nim. Fix: remove the hook and VariableInfo. No language implemented the hook, nothing else referred to either one, and the module is not part of the public API. The variableTypes doc now says how the core finds the declared names, and that a grammar which nests them deeper gets its own branch in extractVariable, as the add-lang skill already says. Wiring the hook in was the alternative, but nothing needs it, so it would be an API no language exercises; the open Nim PR uses a core branch instead. Validation: - New __tests__/language-extractor-hooks.test.ts reads LanguageExtractor's members with the TypeScript parser and checks that the extraction core reads each one through an extractor receiver. On main it fails, listing extractVariables; with the fix both tests pass, and all 59 remaining members are read. - The compiled JavaScript is byte-identical: of 1,164 dist files hashed, only tree-sitter-types.d.ts and its map changed. - expressjs/express (0dee2c52, 214 files) indexed before and after, on the native-kernel and the wasm paths: the graph dumps are byte-identical (12,450 lines; 1,135 nodes, 2,258 edges). Fixes #2465 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9ed79c6cf1 |
fix(add-lang): check-grammar fails a grammar that crashes or hangs its process (#2808)
Root cause: scripts/add-lang/check-grammar.mjs ran the parse loop in its own
process and printed "RESULT: PASS" as soon as the loop finished. A grammar
can finish every parse and still take the process down after that. With the
issue's vim grammar (tree-sitter-wasm 2.0.4) on Node 24, V8's optimizing tier
aborts the process from a background compile job ("Fatal process out of
memory: Zone") after PASS is printed, so the verdict and the exit code
disagree. The check also ran without the V8 flag indexing always uses
(--liftoff-only), so it judged grammars in a setup codegraph never runs them
in, and an iteration count of 0 or a word passed without a single parse.
Fix: the script runs the grammar in a child process, the way indexing does,
and prints the verdict only after that child has exited.
- The child gets indexing's V8 flags (WASM_RUNTIME_FLAGS) and loads every
vendored grammar beside the one under test, like a polyglot index.
- It parses the sample with one reused parser, walks every node of each tree
and frees it, as extraction does. Every parse of the same sample must give
the same tree.
- It then parses the sample repeated to 1 MiB, the most indexing reads from
one file, within the 10 s indexing gives one file. Only a crash or a hang
counts there.
- The parent fails the grammar when the child dies (any exit code or
signal), runs past a timeout (CODEGRAPH_CHECK_GRAMMAR_TIMEOUT_MS, 120 s by
default), or reports ERROR trees or differing trees. An iteration count that
is not a whole number above 0 is refused (exit 2).
The add-lang skill now says what counts as a FAIL.
Validation:
- New __tests__/add-lang-check-grammar.test.ts. A preload marks the process
that runs WebAssembly, records its flags, and on request kills it or makes
it hang at exit, after the parses. With main's script 4 of 5 tests fail:
PASS printed for a process that then died, a hang until the test's 60 s
limit, no --liftoff-only in the parsing process, and "0" passing. With the
fix all 5 pass. The fifth (the tree-sitter-wasms Lua ABI-13 grammar fails
with ERROR trees) passes on both.
- The issue's vim grammar (sha256 df3906a0, the issue's file) on Node 24.21.0:
main's script printed PASS and then died with the Zone OOM, exit 3, in 3 of
3 runs. The new check runs it with --liftoff-only, as indexing does, where
it parses cleanly, 1 MiB included, and passes in 3 of 3. A copy of the new
script with the flag withheld fails it in 3 of 3: "the parses finished, but
the process running them then died (exit code 2147483651, 0x80000003)".
- Old vs new on the 19 vendored grammars with real samples, plus the
tree-sitter-wasms Lua and the vim grammar, on Node 22.22.0 and 24.21.0
(84 runs): the same verdict every time, except vim on Node 24, where main's
PASS came with exit 3. No grammar's parses diverged.
Fixes #2464
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c4dbc126b0 |
fix(bundle): bundle Node 24.21.0 so Windows commands no longer abort as they exit (#2806)
Root cause: every release bundles the Node that scripts/build-bundle.sh defaults to, since the release workflow passes only the target, and that default was v24.16.0. On Windows that Node can abort just as the process exits, after the command's work is done: a V8 worker posts a delayed task after the platform's delayed-task scheduler has started closing its uv_async handle, and libuv fails "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94" with status 0xC0000409 (nodejs/node issue 56645). Node fixed it in 24.20.0 (nodejs/node PR 61999). Fix: the default moves to v24.21.0, the newest 24.x LTS release that is at least two weeks old (2026-09-07). It also carries the Windows 8.3 short-path file-watcher fix (nodejs/node PR 65118). V8 13.6.233.17, libuv 1.52.1 and the module ABI (137, N-API 10) are unchanged; SQLite goes from 3.53.0 to 3.53.4. A comment at the default says why it must stay on 24.x at 24.20.0 or later, and a test pins that, along with the workflow passing only the target. Validation, on Windows 11 x64: - Node's own regression test for the crash (test-process-exit-after-fetch-throw.js, from the v24.21.0 tag), 300 runs per arm, against the official win-x64 node.exe of each version (zips checked against nodejs.org's SHASUMS256.txt): v24.16.0 aborted 300/300 with 0xC0000409 and that async.c line 94 assertion, and the test as written failed 300/300; v24.21.0 passed 300/300. With the launchers' --liftoff-only both passed 300/300, since that flag only removes this test's trigger. - build-bundle.sh win32-x64 builds the bundle on this machine. Its node.exe is byte-identical to the official v24.21.0 one. --version through the Git Bash launcher, cmd and PowerShell, then init, status, query, callers and sync on a small TypeScript and Python project all exit 0, on the wasm path and with the native kernel bundled. - The real CLI (index, status -j, sync) run 300 times per runtime on the bundled app: 0 nonzero exits on either. - The runtime-facing test files (WASM flags, node:sqlite backend and open errors, launchers, worker teardown, version check) pass under Node 24.21.0: 9 files, 65 passed, 3 skipped (POSIX-only). - The new test fails on main's script (expected 16 to be greater than or equal to 20) and passes with the fix. Fixes #2617 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
89cd08fab8 |
fix(explore): a long query, like a pasted log, answers in seconds on a large index (#2804)
The prompt hook hands codegraph_explore the whole prompt, so a pasted log, document or source file arrives as a query of hundreds of words. findRelevantContext searched every word several times, and the reads behind those searches scanned the index each time: - findNodesByNameSubstring, four times per symbol word: `name LIKE '%word%'` with a leading wildcard reads every node of those kinds, about 80 ms each on vscode (562,636 nodes); - the LIKE fallback after an FTS miss (`name LIKE` or `qualified_name LIKE '%word%'`), about 120 ms per miss, and most words of a log miss; - the fuzzy fallback after that re-read `SELECT DISTINCT name` each time (about 150 ms), and getAllNodeNames, documented as cached, was not; - FTS joined nodes on the TEXT id instead of the rowid; - scorePathRelevance re-split every query word for every scored path, and the deprioritize predicate stat'ed codegraph.json per candidate. On vscode a 1.7k-character log took 80 s in explore, a 10k-character skill document 258 s, and the hook gave up on both. A per-statement profile put 65-80% of that in those scans. The fix: - A name-substring index answers the substring lookups from memory with exactly the rows SQLite returns, in its order: ASCII-only case folding and `_` as one character, like LIKE, and ties in length in the order the plan reads rows, the kind index's or rowid's. The plan is asked of SQLite once per statement shape, since a small index is scanned whole; a `%`, a language filter or any other plan goes to SQLite as before. - A word of letters and digits that no name and no letters-and-digits run of a qualified name holds can match nothing, so the LIKE fallback returns empty without scanning for it. - The fuzzy fallback reads the name list once and skips names outside its edit-distance length window. - These reads are memoized per database change stamp, like the dominant file (PR #1864): rebuilt after any write from any connection, never kept inside a transaction, dropped on rebind. - FTS joins on rowid, with CROSS JOIN keeping nodes_fts the outer loop as before. - Path relevance prepares each query once and each path is scored once per search; the deprioritize patterns are read once per search. - findRelevantContext searches at most 64 symbol names and 128 search terms of a query. Both lists put code-shaped words first, so a long paste is searched by its identifiers and its opening words. A 16-symbol bag (75 terms) stays whole. This is the one intended change in output, for long queries only. Validation (vscode ff0a86dc, median of 3 interleaved, fresh process): - explore, ordinary queries: 6 of 6 outputs byte-identical, 5.5-7.5 s fresh (the dominant-file aggregation is about 4 s of that) and 0.9-2.1 s warm vs 2.7-3.9 s on main; - explore, long queries: log 1.7k 80.1 to 10.7 s, markdown 3k 73.3 to 9.0 s, Java 3k 108.8 to 12.2 s, TS 3k 63.4 to 11.9 s, markdown 10k 258.3 to 10.8 s, Java 10k 144.7 to 11.1 s, TS 10k 146.7 to 9.6 s; - the real prompt hook on the issue's prompt shapes: short 7.2 to 7.0 s, log 84.2 to 12.9 s, markdown 3k 78.1 to 10.2 s, Java 3k 113.1 to 13.3 s, markdown 10k 261.7 to 11.3 s, Java 10k 146.3 to 11.1 s; - without the word caps, all 7 long queries gave byte-identical output 4.3-9.0x faster; - django 79502d08 and prometheus ed26d727: 11 of 11 queries byte-identical, tracebacks included, 1.0-2.9x faster; - the substring index against the LIKE scan on vscode: 4,830 of 4,830 lookups identical (805 words, snake_case included); - explore-long-query.test.ts: 13 tests; 8 fail on main's source. Issue #2620. Closed contributor PR #2621 (retsamekohC) was read for guidance; open PR #2260 (danusha2345) overlaps on the name-list memo and path relevance. Fixes #2620 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7b50ca8321 |
fix(pascal): Delphi calls to compiler intrinsics and plain data no longer link namesakes (#2803)
A Pascal call to a name the compiler or the System unit gives meaning
to (SizeOf, TypeInfo, High, Low, Length, Inc, Dec, Assigned, Ord, Exit,
Include, Move, Assert, typecasts like Pointer(P)) went to name
matching, which linked any project member of that name: mORMot's
TRttiProp.TypeInfo, a record's length or high field, a property named
Pointer. The built-in name check compared case, so length(S) and dec(N)
got through too. And a call that resolved to a field, property or
variable linked whatever data had the name.
Now a bare call to a System name links only to a declaration its scope
reaches: the class and its ancestors, the unit, the units it uses last
first, skipping a unit's stand-in declared under {$IFDEF} or {$IF}. A
call resolved to data stands only when the data's declared type is a
routine type (written out, a project alias to one, generic or on two
lines, or an RTL procedural type by name), retargeted to what the scope
declares. A name the calling routine declares itself links nothing; a
name the scope doesn't declare at all (a with object's member, a
declaration the parse misplaced) links to the one routine-holding field
or variable of that name. Built-in names compare without case; the RTL
namespaces keep their spelling, except system. Scope lookups now see a
unit whose unit header the parser lost, by its file name (its indented,
lost class members aren't taken for its routines), and an include file
shares its unit's scope and uses clauses. A with body's bare call to a
System name is not extracted, since it may be the with object's.
Validation, edges removed/added against origin/main: SynEdit -243/+50,
HeidiSQL -858/+296, mORMot2 -9039/+987, DelphiMVCFramework -2420/+242,
DWScript -705/+222; leveldb and SDWebImage byte-identical. Sampled
precision of calls to data, before and after: 20/50 and 50/50; of calls
to System names: 2/50 and 47/47. Sampled removed edges: 40/40 wrong.
Follow-up to #2514.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d82a4adc9d |
fix(js,ts): a call or new in a parameter's default value is the function's call (#2802)
A default runs inside the function each time a call leaves the argument
out, but both extractors pushed the function and walked only its body,
so `function renderWithNote(note = buildNote())` recorded nothing for
the call: no edge and no unresolved reference. Arrows, methods,
constructor parameter properties, class-field arrows, object-literal
methods and destructured parameters lost theirs the same way. Only an
anonymous arrow inside a body was walked whole, params included.
The TS extractor (visitParameterDefaults) and the kernel's TS/JS walker
(visit_parameter_defaults) now hand each default value, and a computed
key in a destructured parameter, to the body walker, with the scope the
body gets on the stack: the function for a named function, method or
component, the enclosing scope for an anonymous function or an
object-literal method. It runs before the body walk at every site that
walks only a body, including a class field's arrow, a forwardRef/memo
render function and a styled .attrs() function. Patterns are recursed
through; the names a parameter binds, its type annotation and a bodiless
signature are not walked. TS/JS only: a Python default runs once, when
the def does, and is left as it was.
Validation: the new test fails 13 of 14 on main
|
||
|
|
5d81d292fa |
fix(js,ts): a call or new never lands on an interface or a type alias (#2801)
A call or `new` that a strategy resolved by name or through an import could target a TypeScript interface or type alias, which nothing can call or make an instance of. memfs's fs-core `result.ts` declares `interface Ok` beside `function Ok` (and `Err` beside `Err`), and `Ok(...)` through an import reached the interface. A `new` of a global that a project only declares a type for (`new Response()`, `new Worker()`, `new Event()`) was linked by name to that type, and jest's `write()` parameter call to a `type write`. Root cause: no rule on what a call may target. Whichever strategy found the name picked among same-named nodes by order, and nothing stopped it on a type. Fix (gateTargetKind): for the JS family, a `calls` or `instantiates` result whose target is an `interface` or a `type_alias` moves to the value the target's file declares under the same name (a function, class, component, constant or variable, first by position): `function Ok` beside `interface Ok`, a class merged with an interface. With no such value the result is dropped, and the reference stays unresolved. Validation: - __tests__/call-value-targets.test.ts (3 tests): all 3 fail on main and pass on the kernel. Each of 5 rules removed in turn fails at least one. - Sync equals a fresh index (CLI) when the value beside a type is added or removed, and when a project type for a global or a parameter appears. - A/B on 14 TS/JS repos and 2 controls: memfs's 11 Ok/Err calls move from the interfaces to the functions; 165 `new` of globals dropped (vite 27, nuxt 57, webdriverio 26, insomnia 47, jest 7, memfs 1) and jest's `write()` call on `type write`. Every dropped `new` names a global (Worker, Event, Response, Request, Headers, ReadableStream, Node's Module, mocha's Context/Suite) or a local variable. vben, outline, social-app, t3-turbo, nitro, vue3-antdv-admin, pathe, c12, flask and gin are byte-identical. - 91 related test files pass (2,178 tests). - Cost: fresh index within noise (median of 3, interleaved): vite 9.07 s to 8.74 s, nuxt 11.71 s to 10.93 s, insomnia 15.26 s to 15.11 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e8ddc4e2be |
fix(js,ts): imports of a repo's own packages follow their package.json entries (#2799)
Issue 2502: a pnpm (or npm, Yarn, Bun) workspace member that declares its
entry with `main`, `module`, `types` or `exports`, like
`"main": "./src/index.ts"`, has no index file at its root, so
`import { caseStatus } from '@acme/core'` failed and its calls fell back to
name matching, where a same-named private function nearer the caller took
them. Issue 2456: a sibling package another declares as a path dependency
(`"acme-shared": "file:../common"`), with no root `workspaces`, was not
known as a package at all.
Root cause: workspace-packages.ts mapped a member's name to its folder only
(its header said it did not yet honour `exports` or `main`), and the
resolver probed `<folder>/index.*`. Path dependencies were read only from
ohpm manifests.
Fix:
- workspace-packages.ts reads each package's declared entries: `exports`
(subpaths, `*` patterns, fallback arrays, nested conditions, read with
import/require/node/module/default first, other conditions next, types
last) and the root's source/module/main/types/typings. It reads `file:`,
`link:` and `portal:` dependencies from every package.json in the
existing manifest walk: the root manifest's for every file, any other's
for the files under it, as npm installs them.
- The resolver takes the first declared target, `exports` before the
main-style fields, that the index holds or that is build output the
index lacks, read as the source it is built from (dist/, build/, lib/,
out/, es/, esm/, cjs/ mirrored under src/, then without a format folder).
Then the package's folder, as before, and a path into its build output
(`@acme/utils/lib/path`) read as its source. A file a local build left on
disk never wins over an indexed source, and no mapping links nothing.
- Sync: a full reconcile reads the manifests again and, when their digest
moved, re-opens every JavaScript-family reference, as a moved Python root
set does. A file added inside a package re-checks the imports of that
package that landed elsewhere in it. A full index drops the cached map.
Validation:
- __tests__/workspace-package-entries.test.ts (26 tests): 25 fail on main,
the control passes; all pass on the kernel and with CODEGRAPH_KERNEL=0.
Each of 17 rules removed in turn fails at least one test.
- Sync equals a fresh index in 15 CLI scenarios (entries, subpaths, a
member renamed, added or leaving the globs, file: deps added, removed or
retargeted, an entry's file appearing). On real repos: two vben manifest
edits (237 edge rows move) and a vite playground file: edit, synced graph
identical to a fresh index.
- A/B on 14 TS/JS repos and 2 controls: vben +2,955 edges, insomnia +2,222,
webdriverio +2,472, jest +1,700, nuxt +706, memfs +470, vite +251,
t3-turbo +47, social-app +40, vue3-antdv-admin +8, with name-matched
namesakes corrected; nitro, outline, pathe, c12, flask and gin are
byte-identical.
- Cost: fresh index within 3% (median of 3, interleaved); a full sync reads
the manifests, +30 to 160 ms on these monorepos.
Fixes #2502
Fixes #2456
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c1744e7e76 |
fix(prompt-hook): desktop reminders and other sessions' messages no longer drive the hook (#2798)
Claude Code's desktop app puts <system-reminder> blocks (which local sessions are linked, and similar status) in front of the prompt it submits, and UserPromptSubmit hooks receive them inside `prompt`. The hook gated, picked the project and explored on the whole string, so the reminder's words drove it: a linked session titled after its work, like "trace-order-flow", carries structural keywords, so every turn, a bare "yes" included, got a full explore of the reminder. A reminder in front of a task notification or a subagent hand-back also defeated the start-anchored envelope checks, and a message from another session (a <cross-session-message from=...> envelope) was never skipped at all. The hook now removes every complete <system-reminder> block before the envelope checks, the gate, project selection and explore, returns early when nothing is left, and skips a prompt that is entirely a <cross-session-message> envelope, as it already did for task notifications and subagent hand-backs. The scan stops at the last closing tag and the attribute run stops at `<`, so a pasted prompt full of unclosed tags stays linear (520 KB in under 1 ms; the `[^>]*` form took 83 ms on a tenth of that and grows quadratically). Validation: - Prompt shapes come from the Claude Code desktop transcripts on this machine (314 transcripts): reminders always lead, one to four of them, separated by one or two newlines, and cross-session messages follow one to four reminders. - frontload-hook.test.ts: 15 new tests (8 spawn the built hook with Claude Code-shaped payloads) fail on main and pass with the fix; the file's 48 other tests pass on both. - Built CLI, private HOME, 13 payloads: on main every prompt behind a reminder got a full explore of the reminder (2.1 to 5.0 KB); with the fix the 8 that are not user input inject nothing and exit in about 200 ms instead of 680 ms, and a question behind reminders injects byte-for-byte what the same question alone does. Issue #2635. Contributor PR #2642 (yang0228) was read for guidance. Fixes #2635 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
407efb1c65 |
fix(pascal): Delphi calls written without parentheses link to the routine (#2797)
Delphi calls a routine that takes no arguments without parentheses
(`Before;`, `X := FProvider.Value + 1`, `if Ready then`), and only a
statement-level `Obj.Method;` was recorded, so most call sites were
missing from callers, callees and impact. Bare names in arguments and
assignments were captured as function values and matched by name alone,
which linked locals and parameters to other classes' methods.
The extractor now records each name written without parentheses with
how it is used (a statement, a value, an event's method, a receiver),
the routine's class, and the declared type of a receiver the routine
declares. It leaves out what the routine declares, data the class and
the unit declare, names the class's code assigns or passes to Inc/Dec,
names Delphi's conventions give to data, and anything in a `with` body
but an event's method. A new resolver links such a name only to a method
or function Delphi's scope rules reach: the class and its ancestors, the
unit, then the units it uses, last first, or a method of a receiver's
declared type. Data of that name links nothing. A function runs (calls);
a procedure read as a value, an event's method, and a routine that needs
arguments written without them are method pointers (references). Where
the tree has errors, the class and declarations come from the source
text, and a routine's own header is no call. `class sealed (TBase)` and
`class abstract (TBase)` heads now name their ancestors. Pascal function
values capture only `@Handler`.
A/B against main
|
||
|
|
63ca5681db |
fix(db): a database SQLite can't open is reported as that, not as a Node version problem (#2796)
Root cause: createDatabase() caught everything the node:sqlite connection threw and reported it as "CodeGraph requires node:sqlite (Node.js 22.5+)", though the module had loaded and SQLite itself had failed: a database another program held open (SQLITE_CANTOPEN on Windows), a folder the user can't write, an I/O error on a network share. Failures in the connection's first statements (locked, not a database, a WAL I/O error) came out raw, without the path. The MCP server then told the agent "No CodeGraph project is loaded" for a default project whose index exists, or "Tool execution failed: ... requires node:sqlite" for a projectPath one. Meanwhile a runtime that really lacks node:sqlite never got the hint: isInitialized's schema probe loaded the module outside its try, so the CLI and MCP server died with "Unhandled rejection: No such built-in module: node:sqlite". Fix: - src/db/node-sqlite.ts loads node:sqlite in one place; NodeSqliteUnavailableError carries the Node 22.5+ hint, now only for a module that cannot be loaded. The schema probe treats it as "unknown", so the command goes on to the open and reports it from inside its own catch. - src/db/open-error.ts: DatabaseOpenError names SQLite's code and the path, with a hint for BUSY, PERM, READONLY, IOERR (network share), CORRUPT, FULL, CANTOPEN (folder, permissions, another program holding the file) and NOTADB, keeping code/errcode/errstr and the cause. The Windows/WSL shared-index guidance still wins for its case. - DatabaseConnection.open/initialize wrap the connection's first statements as well as the constructor, and close the connection on failure. - MCP: the default project's open failure is reported to a tool call instead of "no project loaded", and a DatabaseOpenError or a missing node:sqlite is answered isError with the retry-once note (a malfunction, per AGENTS.md), worded as what failed rather than "an internal codegraph error". Not-indexed projects keep their success-shaped guidance. Validation: - Windows, an indexed project whose codegraph.db another process holds open with no sharing (a real SQLITE_CANTOPEN): CLI status, sync and query, an MCP default-project call and a projectPath call all said "requires node:sqlite" on main, or "No CodeGraph project is loaded" for the default project; with the fix all name SQLITE_CANTOPEN, the path and the hint. With --no-experimental-sqlite (node:sqlite unloadable), main's CLI and MCP server died with an unhandled rejection; the fix prints the Node 22.5+ explanation and the MCP call answers with it. - New __tests__/sqlite-open-error.test.ts (21 cases): main's source 18 fail / 3 pass, fix 21 pass. Real CANTOPEN (a directory, a file held exclusively), NOTADB and BUSY opens, the CLI and the MCP engine, plus each SQLite code's message. 5 mutations each failed at least one case. - The POSIX-only wsl-shared-index case for a real I/O error outside WSL now expects the named error (DatabaseOpenError, SQLITE_IOERR, the path) instead of the bare "disk I/O error"; it is skipped on Windows, so it was not run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1040d09f26 |
fix(mcp): a daemon that never answers no longer stalls a new session for minutes (#2795)
Root cause: a launcher that found no answering daemon spawned one and then
polled for it 240 times, 25ms apart. A probe that connects but gets no hello
waits out the 3s hello timeout, so a process that holds the daemon's pipe
and never says hello made the poll take about 240 x 3s, plus the first
probe: about 12 minutes, with the session's tool calls held meanwhile. In
practice that holder is a daemon whose main thread is blocked: the OS still
accepts connections on its pipe, and the daemon the launcher spawns cannot
take its lock, so it exits.
Fix: the poll has a wall-clock budget instead of a probe count (the 6s it
was meant to be), and each probe's hello wait is capped by what is left of
it (pollForDaemon, connectAnyDaemonCandidate, a hello timeout on
connectWithHello). Spent, the session is served in-process exactly as when a
spawned daemon never binds: read-only while the stuck daemon still holds the
writer slot. The proxy's existing retry with backoff then reaches the daemon
again. The liveness watchdog kills a wedged daemon after its 60s window
(longer while its disk advances), and the next retry spawns the replacement
and proxies to it. The bound sits well inside that window, so a session
neither waits on the wedge nor gives up on the daemon.
Validation:
- A real daemon wedged by a preload's busy loop on Windows, then a second
session's codegraph_status:
- no watchdog on the wedged daemon: main answered after 733.2s; the fix
after 9.9s, and later calls in 0.5s while the daemon stayed wedged;
- the default 60s watchdog (it killed the wedged daemon at about 121s,
one deferral for its own off-thread writes): main answered after
125.2s; the fix after 9.9s in-process, then proxied to the replacement
daemon at 121.1s.
- New __tests__/daemon-silent-holder.test.ts (8 cases): main's source 0
pass, fix 8 pass. Fake-clock tests of the budget and the per-probe cap; a
real silent socket honouring the hello timeout; the real launcher against
a project whose pipe, lock and writer slot are held by a holder that
never says hello: first call served in-process in about 10s (main: none
in 45s); and after the holder dies, the session proxies to the daemon
that replaces it. 3 mutations each failed at least one case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
8073637d66 |
fix(db): reopening an interrupted index no longer gets the server killed at every start (#2794)
Root cause: a run killed inside a bulk-load window (a crash, an OOM kill,
the liveness watchdog itself) leaves that window's secondary indexes
dropped, and since PR #1562 every writable open rebuilds them in
DatabaseConnection.open(), one CREATE INDEX after another on the caller's
thread. Each one scans and sorts its whole table. On a large index the sort
writes nothing to the database or its WAL, so on an MCP server the liveness
watchdog saw a silent main thread with no disk progress, and killed the
process before the index committed. Each restart died at the same index,
and the proxy's in-process fallback died the same way on the request it
re-served.
Fix: an open on a watched event loop leaves the rebuild to a worker thread
with its own connection, and the project is served once the rebuild is
done.
- DatabaseConnection.open({ deferIndexRecovery }) records the missing DDL
instead of running it; recoverIndexesOffThread() runs it on a worker
(single-flight, rejects on failure so the next call retries; SQLite's
default temp store, so the sort spills to disk; the worker collects its
garbage and exits by itself, per worker-teardown.ts).
- CodeGraph.open() (the MCP default project, the CLI) defers and awaits it.
- openSync({ deferIndexRecovery }) plus cg.recoverIndexes() for the MCP's
synchronous opens: the ToolHandler's projectPath projects and the engine's
retry path. ToolHandler.execute awaits the rebuild before serving (not
time-boxed: reads without the indexes scan whole tables), the project
lifecycle awaits it before its catch-up sync writes, and the retry path
starts the query pool after it.
- Synchronous opens that ask for nothing keep rebuilding in line, as before.
Validation:
- Reproduced on main
|
||
|
|
b2a84873ed |
fix(daemon): a long sync of another project no longer gets the shared daemon killed (#2793)
Root cause: the daemon arms its liveness watchdog with its own project's index files (codegraph.db + -wal), and since PR #1231 a silent timeout defers the kill while those files advance. But the daemon also writes every other project a session opens with projectPath: the project lifecycle takes that project's writer lock, starts its watcher and runs the catch-up sync, all storing on the main thread. A long synchronous stretch of such a sync left the daemon's own files still, so the watchdog read it as a wedge and SIGKILLed a healthy daemon mid-sync; the next session reattached the project and the cycle repeated. A proxy serving in-process, armed with no files at all, had the same blind spot for its own project. Fix: addWatchdogProgressPaths() lets a process add files to its watchdog at runtime. The project lifecycle adds a project's db and -wal the moment this process becomes the project's writer, and drops them after it closes the project, so only indexes this process writes count, never one another process owns. Each change re-arms the watchdog with a new child whose argv carries the whole list; the old child is stopped after the new one starts. Added files take effect at once (a sync may block the loop right after); dropped ones wait a turn, so a server releasing its projects on the way out re-arms once or not at all. A message over the heartbeat pipe was tried first and dropped: Node's Writable holds a second write while one is still in flight, so the -wal line of a db + -wal registration never reached the child before a block that followed in the same tick (4 of 4 runs). Validation: - New __tests__/liveness-watchdog-projects.test.ts (11 cases, real watchdog child, lowered timeout): main's source 9 fail / 2 pass (the two guards), fix 11 pass. The lifecycle case arms the watchdog for project A, attaches B through the project lifecycle and stores into B's index on the main thread for 3s: killed at ~1s on main, runs to completion with the fix. - 8 mutations, each failed at least one case: no lifecycle registration, registration before the writer-lock check, no drop on release, no re-arm on add, no re-arm on drop, old child not stopped, drops re-armed at once, adds deferred. - Real daemon on Windows (watchdog timeout 5s, daemon keyed on a 2-file project, second project excalidraw 1d4dc2c with all 687 TS files edited, attached with codegraph_status projectPath), 3 interleaved rounds: main killed 3/3 about 13s after attaching B ("Main thread unresponsive for ~5s"), leaving B with 27,698 edges and 72,534 pending refs; the fix survived 3/3 through 7.8-9.3s main-thread blocks and finished B (61,120 edges, 0 pending), and 3/3 again on the final build (9.7-9.9s blocks). Fixes #2404 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8651990abd |
fix(js,ts): an import spelled with a trailing slash or a ./ segment links its file (#2792)
Issue 2577: `import('@/widgets/Chart/')`, `from 'src/widgets/./Chart'`, a
doubled slash, a `..` after an alias, and a workspace package's
`from '@acme/ui/'` lost the edges the plain spelling gets. The file import
was dropped, the names were matched by name alone, and the lazily loaded
component rendered the importing file's own `lazy(...)` constant.
Root cause: the fallback-alias, workspace, direct-path and own-config
branches of the import resolver join the specifier as written and probe
the result through tryWithExtensions. `fileExists` falls back to the disk,
which accepts any spelling of a file, so the resolver returned
`src/widgets/Chart//index.tsx` or `src/widgets/./Chart/index.tsx`, and every
lookup keyed by the indexed path missed it. The relative and tsconfig-path
branches go through path.resolve and were already fine.
Fix (import-resolver.ts): tryWithExtensions spells each candidate as the
index keys files (indexedSpelling): for the ES module family, a path with a
doubled slash or a `.`/`..` segment is posix-normalized before it is probed
and returned. That is how Node, tsc and the bundlers read a specifier. A
trailing slash still names only a folder, because `Chart/` plus `.tsx` is
no file. Other languages are untouched: a C include's `a/../b.h` follows the
folder on disk, as PR 2610 noted.
Validation:
- __tests__/import-path-spelling.test.ts: 4 of 5 fail on main (the plain
spelling passes). They cover the issue's spellings, a doubled slash, `..`,
a trailing slash beside a same-named file, and a workspace member. All
pass on the kernel and with CODEGRAPH_KERNEL=0. Dropping the
normalization, or its doubled-slash check, fails 4.
- Sync equals a fresh index (CLI) when the folder arrives after its
importer, when the importer is re-spelled, when the folder is deleted
and when the target is edited.
- A/B on 11 TS/JS repos plus 2 controls, both arms with the kernel:
pulse-sms-web (53 imports like '@/utils/') gains 65 edges and corrects
7, including 2 imports that had name-matched to same-named API classes;
react-one gains 11 file imports and corrects 2 bindings. All 85 are right.
The other nine (jvue-admin, vue-invoice-app, vue3-antdv-admin, v-ui,
vue-blog-admin, dreammart, vite, outline, social-app) and the flask and
gin controls are byte-identical.
- Cost: fresh index, median of 3 interleaved: vite 7.59 s main vs 7.49 s,
outline 16.63 s vs 16.94 s.
Fixes #2577
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
e69546e4a3 |
fix(js,ts): code outside tests resolves as if Jest manual mocks were not there (#2790)
Issue 2455: when production code imports a workspace package the resolver
cannot follow (`import { formatLabel } from 'acme-shared'`), its names fall
back to name matching, and a Jest manual mock beside the code
(`__mocks__/acme-shared.ts`) is the nearest namesake. So the call, the
import's names, type references, constructions and `extends` all linked to
the mock, and the real function lost its callers. The visibility gate keeps
test suites from production code, but `__mocks__/` is not one of them.
jsx-render, Vue templates and the event-emitter pass pick by name with
their own lookups, which no gate covers.
A manual mock stands in for a module only while Jest or Vitest runs, and it
re-declares that module's names by design. Rejecting it after the ranking
picked it (what the gate does for `__tests__/`) leaves the call unlinked even
where the real function is the only other candidate. It also leaves sync
behind a fresh index: deleting the mock re-opens nothing, while a fresh
index then links the real function (main has this today for `__tests__/`).
Fix:
- ReferenceResolver.resolveOne resolves a reference from code that is not a
test (isTestPath) on a view of the graph without manual mocks, when the
project has any. Every lookup by name, lower-case name, qualified name,
kind and method answers as if no `__mocks__/` file were indexed. Only
names a mock declares are filtered, and those answers are cached. The
view is swapped in as this.context for the call, so every strategy
(frameworks, imports, name matching, fuzzy, function refs) reads it, and
clearCaches drops it with the memos keyed by it. Test code keeps the
whole graph.
- jsxChild, the Vue template resolver and the event-emitter handler lookup
skip a mock's component or function for a non-test file
(isManualMockHiddenFrom).
- The synthesized-edge merge drops any edge from non-test code into a node a
manual mock declares, whatever pass made it.
- An import that names the mock's own path (`import x from
'./__mocks__/uuid'` in a vitest setup file) still links. Jest's
`moduleNameMapper` and automock are not read: they act only while Jest
runs, so they never change what production code calls.
Validation:
- __tests__/jest-manual-mocks.test.ts: 5 of 6 tests fail with main's
sources. Each of five rules fails a test when removed: the view (three
tests, the in-process sync case among them, which adds and deletes a
mock), the JSX, Vue and event lookups, and the merge guard.
- Sync equals a fresh index (CLI) when the mock or the real module arrives
later, when the production file is edited, and when the mock is deleted.
- A/B on 8 TS/JS repos plus 2 controls, both arms with the kernel. Edges
from production code into mocks: webdriverio 23 to 0, outline 21 to 0,
social-app 3 to 0, insomnia 19 to 13 (the 13 left are setup-vitest.ts
importing mocks by path). webdriverio gains 23 right edges and 142
confidence raises that the mocks had blocked. jest, nuxt, flask and gin
are byte-identical. On webdriverio, production code's edges equal main's
on a copy without the mocks, 20,885 of 20,885 with metadata. insomnia
matches the same way, apart from the 13 path imports.
- Cost: fresh index, median of 3 interleaved: webdriverio 12.67 s main vs
12.71 s, insomnia 12.84 s vs 12.60 s.
Fixes #2455
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
214ef50efe |
fix(explore): names the query spells that the index never saw are said, not glossed over (#2789)
Root cause: explore answers whatever the query's words match. When a name the query spelled is not in the index at all, the answer is built from the other words and says nothing about the missing ones. In the report, the tool projects under a .NET repo's root build/ folder are skipped by default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData and AddFeatures returned an unrelated Program.Main and a C++ Parser, and the agent read the wrong code. The miss diagnostics (issue #2026) ran only when nothing at all matched. Fix: - Query tokens written like code names (a camelCase hump, snake_case, or a qualified name) that resolve to no node at all are checked with a new bounded probe, getUnseenNames. It reports names the index has never seen: no node called that in any case, and no name, qualified name or path, signature or docstring word starting with it. - A qualified name is judged by its member. Express's app.handle is indexed as handle, so the receiver alone never makes a name missing. - Names that pass are listed in a note right under the summary line, success-shaped. The note points to codegraph status and never to Read. It names the .gitignore re-include for a default-skipped folder like build/ as the user's choice, since indexing scope is the user's call. - A single word (Program, Main, How) is never named: it may be English. - The total-miss answer gains the same note. - The seeding's query tokenizer moved into one shared function, unchanged. The skip itself is unchanged here. On main a root build/ folder is still skipped by default: PRs 2681 and 2687 re-opened build folders below src/ only. The documented opt-in, !/build/ in .gitignore, indexes it, and sync picks the opt-in up, verified on main and on 1.6.2. Validation: - New explore-unindexed-names.test.ts, 8 tests: 6 fail on main. - All 38 explore suites pass, plus the tool-allowlist and dynamic-boundary suites (466 tests). - The reporter's repo, newrelic/newrelic-dotnet-agent 418a0f2f, now commits the !/build/ workaround. With that line removed and the index synced, main answers the reporter's query with VersionFileGenerator/Program.cs Main and Sicily/Parser.cpp, saying nothing. The fix adds the note naming exactly the four missing names. - Ordinary queries: 996 on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical to main. - Miss probes: 120 queries mixing real names with invented ones, or only invented ones. Main never notes anything. The fix names every invented name in all 120 and no real name. The note costs about 5 source lines on a full response. Fixes #2462 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0953084bc5 |
fix(pascal): the receiver calls of a chained Delphi call are indexed (#2787)
A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.
The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.
Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).
Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
included): all 7 fail with main's sources. Without only the resolver
rule the typecast test fails; without only the chain-link gate the
fluent-chain test fails.
- A/B against main
|
||
|
|
7180fd0666 |
fix(js,ts): a dynamic import from Node or a package no longer links to a project namesake (#2788)
Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`,
bare `'fs'` and `require('node:fs')` bindings linked their calls to
same-named project functions. Main no longer does that for any static
form. PR 2120 (released in 1.6.2) declines a name match for a name the
file imports from outside the repository. PR 2437 made that hold in
projects with a catch-all tsconfig path alias, and PR 2671 extended it to
function values such as `promisify(readFile)`.
What still linked is the same binding written as a dynamic import. The
out-of-repo check reads the file's static import mappings only. The name
matcher deliberately treats `const { x } = await import('./m')` as an
import, not a local shadow, so the call fell through to a same-named
project function. vite's
`const { createServer } = await import('node:http'); return createServer(app)`
linked to vite's own `createServer`.
Fix (name-matcher.ts):
- isOutOfRepoBinding also asks dynamicImportSource for the module a
dynamic import binds the name from, where the reference sits.
- A per-file scan (FIFO of 64, cleared with the other name-matcher memos)
finds `const { a, b: c, default: d } = await import('x')` declarations
and `import('x').then(({ a }) => ...)` callbacks on the raw text. Only
files with a match are comment- and string-blanked, to drop matches in
comments and strings and to find where each binding's scope ends: the
enclosing block (a top-level await binds the whole module) or the
callback body.
- A binding counts only when the reference sits inside that scope after
it, and the innermost one wins, also over a static import of the name.
nitro's `async function createWebSocketProxy()` imports crossws's
`createWebSocketProxy` inside itself: only that inner call is dropped,
and the file's other calls still reach the local function.
- jsBlockEnd and jsBodyEnd are the forward forms of jsBlockOpenAtEnd and
jsBodyOpenAtEnd, which now call them.
Validation:
- __tests__/dynamic-import-binding.test.ts: 5 of 6 tests fail with main's
name-matcher.ts (the project-module control passes there); all 6 pass on
the kernel and with CODEGRAPH_KERNEL=0. Four mutations each fail their
test: no scope end, no callback form, no dynamic source, and no memo
clearing (the in-process sync test).
- Sync equals a fresh index (CLI) for adding and removing the dynamic
import, for the namesake file arriving later, and for the nitro shape.
- A/B on 9 TS/JS repos plus 2 controls, both arms with the kernel: 6 edges
removed, 0 added, 0 retargeted, all 6 wrong. vite has 3 (node:http
createServer, node:fs/promises readFile, rolldown watch), memfs 2
(node:util promisify), nitro 1 (the crossws self-loop). pathe, c12,
unbuild, fs-extra, unenv and nuxt are byte-identical, as are the flask
and gin controls.
- Cost: fresh index median of 3, interleaved: vite 9.17 s main vs 9.50 s,
nitro 7.43 s vs 7.12 s (noise on a shared box). The scan itself over
every vite source file takes 40 ms (24 files reach the blanking pass).
- Kernel parity is unchanged: 772/776 files byte-parity, 4 deferred,
over nitro/src and vite's src (this is a resolver-only change).
Fixes #2491
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c46523079f |
fix(context): the busiest file is found even when 20 test or generated files outrank it (#2785)
Root cause: getDominantFile and getTopRouteFile took the top 20 files in SQL and only then dropped test and generated files in code. Twenty test files busier than the app's busiest file left nothing, so getDominantFile returned null and the context builder's core-directory boost never applied. Nineteen left the dominant file without its runner-up, and nextEdgeCount 0 passes the "3x the next file" test for a file that is not dominant at all. getTopRouteFile returned null the same way, and measured its 30% share against the routes of the 20 files in the window, not the project's. Fix: - DOMINANT_FILE_SQL ranks every file. The window saved no work: ORDER BY needs every group, so the aggregate runs to the end either way, and only one row per file comes back. - computeDominantFile filters the ranking 20 rows at a time until it has the dominant file and its runner-up. The generated-file lookup stays a bounded probe per batch. - getTopRouteFile counts every route file, with ties broken by path. Validation: - New dominant-file-window.test.ts, 6 tests, all failing on main: the issue's layout with 20 and with 19 test files, 25 header-declared generated files (past the first batch), and three route shapes. - The dominant-file cache, plan and allocation suites and the route-file detector test pass. - Seven of the eight real repos give the same dominant file before and after (express, flask, excalidraw, django, airflow, grpc-go, etcd). The closest to the window was airflow, with the runner-up at rank 16. - envoyproxy/go-control-plane crosses the window: 254 generated and test files outrank its first hand-written file. Main returns null; the fix returns xdsmatcher/pkg/matcher/matcher.go (167 edges, runner-up 165). - getTopRouteFile on main reported django's admin/sites.py at 8 of 16 routes and airflow's ui router at 65 of 65. Counting every route file gives 8 of 32 and 65 of 332, both under 30%, so null as documented. - Cost, median of 3 interleaved rounds of 5: getDominantFile on airflow (8,564 ranked files) 581 -> 587 ms, django 189 -> 181 ms, go-control-plane 277 -> 284 ms. getTopRouteFile on airflow 1.3 -> 2.5 ms. - Explore A/B, main vs fix: 996 queries on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical. Fixes #2578 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
800e582d55 |
fix(go): conversions, external test packages and build-tag twins link as Go builds them (#2784)
Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.
A Go call whose target is a type is now a references edge. Two files of
one directory under different package clauses see each other's
top-level names only through an import: qualified, or bare after a dot
import and exported. A reference whose definition has build-constrained
twins in its package reaches the one that builds for the most typical
target its own file builds for: linux/amd64, then darwin/arm64,
windows/amd64 and the rest. Constraints are read from the file name's
GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as
go build sets them by default.
Validation: 9 new tests in three files, all failing on main, and each
part switched off fails its own file; the Go tests pass. A/B against
main
|
||
|
|
1890b10cc4 |
fix(explore): a named function too long for its share renders instead of the file's tail (#2783)
Root cause: an oversize cluster keeps its first-ranked member however big it is, and the ceiling windows it (CG-30). Ties on importance went to the shorter span, so in the issue's file, one 200-line function plus the line that calls it, the one-line `path = build_report()` took that guarantee. The search matches it through its signature, so it is an entry point too. The function the query named then no longer fit beside it and was dropped, and `codegraph_explore build_report` returned the file's last six lines under the verbatim-source header. A file asked for by path did the same, because every definition in it ranks below the bare edge lines. Where a body was windowed, nothing in the block said so. The footer's advice, ask for the symbol by name, returned the same window, and it suggested `codegraph_node`, which the default MCP surface does not list. Fix, all in the explore render path (query time only): - Rule 1: among cluster members of one importance, a definition the query named ranks ahead of one its search only matched, before the shorter span. - Rule 2: a fragment does not spend the oversize guarantee. When the kept members render under MIN_CHARS, the member they priced out is kept and windowed like a first-ranked one. That member must be one the query named, or a definition in a file pinned by path with nothing named in the cluster. - Rule 3: a body the ceiling window cuts ends with the per-symbol view's note, "... lines X-Y of `name` elided - codegraph_explore `file:X-Y` returns them" (one shared helper now). Each note is paid for by its own part's last lines, never a focus line, the member's first line or a member the part shows whole. So no section grows and no other file's budget moves. - Rule 4: the trim footer and the dynamic-boundary note name `codegraph_node` only when it is on the served tool surface. Validation: - New explore-queried-oversize-symbol.test.ts, 9 tests: 6 fail on main. Reverting any one rule fails that rule's own tests (2, 2, 3 and 1). - All 37 explore suites pass (414 tests). - Deterministic A/B: one index per repo, a fresh handler per query, five arms (main, then each rule added in turn). 820 queries per arm: the 50 longest functions by name, their files by path, caller/callee pairs, call chains, random names and flow questions. - The queried function's head (def plus 10 lines) renders, main -> fix: express 50 -> 50, flask 49 -> 50, excalidraw 33 -> 48, django 43 -> 47, airflow 43 -> 48, of 50 each. - Queried body lines rendered, main -> fix: excalidraw 6,615 -> 9,391, django 5,337 -> 5,662, airflow 6,320 -> 6,749. - Rule 1 changed 32 responses, each hand-triaged as the queried function now rendering. Only 4 of the 504 ordinary flow queries are among them. - Rule 2 changed none. - Rule 3 wrote 838 notes, all checked against source and index, at 2.8 to 3.9 source lines each. Bytes a note frees flow to lower files by the existing carry-forward. - Rule 4's 236 diffs are exactly the removed mention. - No response exceeds the 25,000-char inline cap. Fixes #2486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0c3e12ddbb |
fix(go): a call further along a chain links the method of the type the chain reaches (#2782)
A Go method called on a receiver the extractor keeps no name for, such as
a fluent chain's later link (model.WithContext(0).Where(1).First()), a
call on what a function returns (time.Now().UTC().Format(...)) or a
composite literal (GitOracle{}.Worktree()), reached the resolver as its
bare name at the column where the chain starts. Name matching then took
a project method of that name by proximity, or the only one: the issue's
GORM-style Where and First went to another query type, the standard
library's Format to the one project Format, and a literal's method to a
namesake type.
A new Go resolver step reads the chain at that column and types the
receiver link by link: a call's result, or the one a multi-value
assignment takes; a conversion's or a composite literal's type; a
generic call given its type arguments; an embedded field by its type's
name; a package-level variable from the call it is set to. A result
written with type parameters keeps them as parameters, read from the
declaration, since the indexed signature leaves them out, and the helper
that reads them now includes a one-line function's only line. The method
is the one Go selects on that type. A type from outside the project, or
one that can't be read, links nothing instead of a guess. When a chain
calls one name twice, the bare ref is the later call.
Validation: 8 new tests; 7 fail on main, and the repeated-name case
fails under a first-link rule; the 43 Go test files pass. A/B against
main
|
||
|
|
88ebefa135 |
fix(pascal): calls inside a Delphi for var x in loop are indexed (#2781)
The Pascal grammar's for-in rule takes a plain variable, so a loop that
declares its variable inline (Delphi 10.3 and later), like
`for var lItem in AList do lItem.DropIfEmpty;` or
`for var lHandle: THandle in lHandles do`, became an ERROR node. A
paren-less call in its body lost its statement shape and reached no
graph, so the routine was missing from the callee's callers.
The Pascal preParse added for issue #2545 now also blanks the inline
`var` and the loop variable's `: Type`, offsets kept, so the loop parses
as the grammar's `for x in` form. The type runs to the `in` at nesting
depth 0, so generic and dotted types are whole; `for var i := 0 to`,
which the grammar already parses, and `for var` inside comments or
strings are left alone. The resolver still types the loop variable: it
reads a receiver's declaration from the source as written, so
`lHandle.Close` reaches THandle's Close, not a namesake.
Validation:
- New __tests__/pascal-for-in-var.test.ts (6 tests, the issue's unit
included): 5 fail with main's pascal-preparse.ts, all pass with it.
- Every textual `for var x in` in the corpus is blanked except 9 that sit
in comments or strings (DelphiMVCFramework 47 of 49, DWScript 89 of
96), no other text is touched, and no file parses worse.
- A/B against main
|
||
|
|
929c65c83a |
fix(pascal): Delphi routines with a parameter attribute like const [ref] are indexed (#2778)
The Pascal grammar's parameter rule takes no RTTI attribute, so a
declaration like `procedure WithRef(const [ref] AValue: Integer; ...)` or
DelphiMVCFramework's `function CreatePerson([MVCFromBody] const Person:
TPerson)` broke the parse at the `[`. The method was lost, the parameter
after the attribute became a field of the class, every member declared
after it went missing until the next class, and in an implementation the
routine's locals became unit variables and its calls were credited to the
file node.
The Pascal extractor now has an offset-preserving preParse that blanks an
attribute group at the start of a routine parameter, before or after its
modifier (const/var/out/constref), in any routine header, procedural type
or anonymous method (`operator` counts only after `class`, since Delphi
doesn't reserve it). It is a linear hand-written scanner that skips
strings, comments and directives, reads generic and qualified routine
names, and leaves every other bracket alone (set defaults, array types,
member and method attributes, property index lists, code). A new
LanguageExtractor flag, signatureAsWritten, lets Pascal signatures read
the source as written, so `(const [MVCFromBody] Person: TPerson)` keeps
its attribute instead of showing blank space. Only Pascal sets the flag.
Validation:
- New __tests__/pascal-param-attributes.test.ts (8 tests, the issue's unit
included): 6 fail on main's wiring, all pass with the fix.
- A/B against main
|
||
|
|
57ce5b9cdc |
fix(upgrade): a global npm install is upgraded with -g from any folder, even home (#2777)
codegraph upgrade told a global npm install from a local one by whether the running file sat under the current folder. npm's global folder is often inside the home folder (Windows %APPDATA%\npm, nvm, fnm, ~/.npm-global), so run from home every such global install was taken for a local one: npm ran without -g, the global copy stayed on the old version and a second copy landed in the home folder. The same test sent a project's own install to npm -g when upgrade ran from one of its subfolders or from outside it, and installed into the parent folder instead when it ran from one. The scope now comes from npm's layout around the outermost node_modules. A global launcher beside it (<prefix>\codegraph.cmd on Windows, <prefix>/bin/codegraph next to <prefix>/lib on POSIX) means global. A project around it (node_modules/.bin/codegraph or a package.json) means local, and npm then runs in that project, wherever upgrade was run from; the version npm left there is read back, with a warning when it did not change. A pnpm, yarn or bun install (a .pnpm layout, or that manager's lockfile beside node_modules) is named and given that manager's command, since npm run in its folder would only add a second copy. Validation: on Windows, the real CLI was run with a private USERPROFILE, HOME, APPDATA and LOCALAPPDATA (npm config get prefix = the private AppData\Roaming\npm) against installs laid out the way npm lays them out, with a fake npm first on PATH that logs its command and folder. main ran the wrong npm command in 6 of 9 cases (a global install under %APPDATA%\npm or ~\.npm-global run from home, and project installs run from a subfolder, from home, from the drive root or from another project), the fix ran the right one in 9 of 9; an npx cache run and the bundled runtime are detected the same by both. On main's code 14 of the 91 upgrade tests fail on assertions (12 new ones, and 2 existing ones that now also expect the project folder); all pass with the fix. Fixes #2477 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c326b6bec1 |
fix(go): a function value keeps its link past a local of the same name (#2776)
In Go, a function used as a value, like `apply(run)`, was recorded once
per function and name, at its first use. Since PR #2658 the resolver drops
a use that a local or parameter of that name shadows. When a function's
first use was the shadowed one and a later use, outside the local's scope,
was the package function, that later use had no reference left, and the
function lost its references edge to the package function (an inner-block
`run := 2`, a closure's `func(run int)`, a case clause, an if header or a
range variable, then `apply(run)`). Issue #2596 predicted this.
Fix, in both extractors (function-ref.ts with tree-sitter.ts's
flushFnRefCandidates, and the kernel's go.rs flush_fn_ref_candidates):
- A Go function that binds the name itself records one use per scope
instead of one per function. Bindings are a func literal's parameters,
:=, var, const, range and type-switch variables and a select case's
value. Scopes are Go's: a block, an if/for/switch/select statement's
implicit block and each case clause. Within one scope a local declared
before the first use also covers every later use, so the first use
still speaks for the scope.
- The function's own receiver, parameters and results keep a single
reference: they cover the whole body.
- A func literal at file scope is its own unit.
- Every other function is unchanged. The scope and enclosing function are
read from the tree only for names a function uses at least twice, after
the name gate (reading them for every container made kernel extraction
of prometheus 2.4x slower).
Validation:
- go-shadowed-function-value.test.ts: 14 cases, kernel and wasm, LF and
CRLF. On main's sources and kernel 10 fail; with the fix 14 pass.
Per-scope keys without the binding check, and dropping range
variables, case clauses or func literal parameters, each fail 3.
- Kernel parity, Go: gin 99/99, etcd 1065/1105 (40 deferred, as main),
prometheus 746/746, kubernetes 17779/17952 (173 deferred, as main),
0 files with diffs.
- A/B against
|
||
|
|
7dfef8451f |
fix(installer): Hermes keeps your MCP entry and gets no platform_toolsets line (#2774)
Root cause, two parts:
1. Install and refresh appended `mcp-codegraph` to
`platform_toolsets.cli` in `~/.hermes/config.yaml`. It was never
needed. Hermes loads every enabled MCP server on a platform whose list
names none (hermes_cli/tools_config.py `_merge_mcp_servers`, a rule
present back in May 2026 when the target was written). The name
itself is harmful:
- Hermes up to 0.22.2 rejects it at startup ("Unknown toolsets").
- Hermes 0.22.4 maps `mcp-<server>` onto the server, so the list
becomes an MCP allowlist and every other server disappears from CLI
sessions.
A user who deleted the line got it back on the next upgrade.
2. When the `mcp_servers.codegraph` block differed from the template, it
was replaced wholesale. That reverted an absolute command path, the
timeouts, `enabled: false` and `env`. The line-based YAML code also
missed some shapes:
- a codegraph key written on one line or quoted, which got a second
codegraph key;
- a 4-space-indented `mcp_servers`, which came out unparseable;
- a one-line `mcp_servers: {…}`, which got a second top-level key, and
Hermes's PyYAML reader then dropped the user's servers.
CRLF files came back LF.
Fix:
- CodeGraph never writes `platform_toolsets`.
- A `- mcp-codegraph` line an earlier install left in
`platform_toolsets.cli` is removed, or becomes `- codegraph` when the
list is an allowlist naming other MCP servers (the spelling every Hermes
version reads as the server). An allowlist that leaves CodeGraph out
gets a note, not an edit.
- An existing `mcp_servers.codegraph` entry is the user's: nothing in it
changes. A block entry missing `command` or `args` gets just those, at
its own indent.
- A new entry goes in at the indent its siblings use, and a codegraph key
is found however it is written. `mcp_servers: {}` becomes a block.
- A one-line mapping that holds other servers makes install refuse with a
clear error and change nothing. One that holds codegraph is left as
written, with a note, and uninstall says it can't remove it.
- The file keeps its line breaks, and is written only when something was
edited.
- Uninstall also removes a `- codegraph` the refresh may have written.
Hermes references (NousResearch/hermes-agent v0.22.4, 64dbae64):
- hermes_cli/tools_config.py `_get_platform_tools` / `_merge_mcp_servers`
("mcp-<server> names the same server as its bare alias: either spelling
selects only that server").
- hermes_cli/toolset_validation.py `saved_toolset_resolver`.
- website/docs/reference/mcp-config-reference.md (command, args, env,
enabled, timeout, connect_timeout).
- hermes_cli/tools_config.py at 1b9c539c (2026-05-17) for the May 2026
rule.
Validation:
- installer-targets.test.ts: 13 new Hermes tests. 3 existing tests that
asserted the `mcp-codegraph` write are updated; the #456 same-indent
list case now proves the old line is removed cleanly. With main's
hermes.ts put back, those 16 fail, all on behavior. The 7 other Hermes
tests pass on both.
- The real CLI on this Windows machine against a private HERMES_HOME:
`install --yes`, a second `install --yes`, `install --refresh` and
`uninstall --yes --keep-cli`, for 14 config.yaml shapes. Each result
was loaded with PyYAML (the reader Hermes uses), checked for duplicate
keys, and run through a re-implementation of the cited selection rule.
Of 42 results:
- main: 2 unparseable, 10 with duplicate keys, 26 carrying
`mcp-codegraph`, 10 where Hermes 0.22.4 would load only codegraph in
CLI sessions, 3 converted CRLF to LF, and the customized entry
reverted.
- The fix: 0, 0, 0, 0, 0, the customized entry kept byte-for-byte, and
1 install refused by design (a one-line `mcp_servers` holding other
servers).
Fixes #2485
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
5a6917b437 |
fix(c): a function whose return type is a macro call is indexed (#2773)
In C, a function declared with its return type written as a macro call,
like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or
libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node.
blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call
that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`.
Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside
redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is
often the return type. Blanked together with its argument, it left
`cJSON_Print(...)` and a block, which parse as a call statement. The
function was lost, the calls in its body went to the file, its locals
and parameters became file-level variables, and a header's prototype
read as a call from the header.
C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter
reads it as a macro_type_specifier, when all of these hold:
- it is in column 0;
- its one argument reads as a type;
- only a function declarator follows: a name, its parameter list,
`__attribute__` or ALL-CAPS attributes, then `{` or `;`;
- the line right above leaves no declaration open. jemalloc writes
`size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`.
C++ is unchanged.
Validation:
- New __tests__/c-macro-return-types.test.ts (3 tests: the issue's
reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT,
REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the
blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel
and with CODEGRAPH_KERNEL=0.
- Pre-parse output compared file by file on 12 repos. The only changes
are macro text kept where main blanked it; no file's parse-error count
got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1,
APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq,
redis (with jemalloc), sqlite and curl, and on the C++ controls fmt,
glm, json and abseil-cpp.
- A/B on main
|
||
|
|
0de94978fc |
fix(installer): the Codex table is found however its config.toml writes it (#2771)
Root cause: the TOML helpers behind Codex's install, refresh and uninstall
found CodeGraph's table with two literal searches, `[mcp_servers.codegraph]`
at the start of the file or right after a newline, and detection used a
plain substring check. Some valid ways of writing that table went unseen:
- an indented header, which formatters that indent sub-tables write;
- spaces inside the brackets or around the dot;
- quoted or escaped keys.
Install then appended a second `[mcp_servers.codegraph]`. A table defined
twice is invalid TOML, so Codex loaded none of its config. Install and
refresh also appended a table after a definition written with dotted keys
or as an inline table, or under an `mcp_servers` that is an inline table or
an array of tables (both invalid too). The literal search also matched
header-shaped text inside a multiline string and spliced into the string.
Uninstall left the user's original table, a `[mcp_servers.codegraph.env]`
sub-table (a server with no command), and dotted or inline definitions in
place. CRLF files came back with mixed line endings.
Fix: a small structure scan of the file finds every statement (table
header, array-of-tables header, key/value). It decodes key paths the way
TOML reads them (bare, "basic" with escapes, 'literal', spaces around
dots), tracks the table each key sits in, and folds the lines of a value
that spans several into the key that opened it. On top of that scan:
- The `[mcp_servers.codegraph]` table is found however its header line is
written. Install and refresh replace the first one in place and remove
any further copies, so a file the old bug broke parses again.
Sub-tables such as `.env` are kept.
- A block ends at its last key, so a comment heading the next table stays
with that table.
- Uninstall removes the table, every copy, its sub-tables, and
definitions written with dotted keys or as an inline table, as whole
statements.
- An entry the user wrote with dotted keys or as an inline table is left as
written, with a note.
- Where a table can't be added (`mcp_servers` set as an inline table, a
value or an array of tables), install refuses with a clear error and
changes nothing.
- An entry inside an inline `mcp_servers` table is reported by uninstall,
never edited. The uninstaller now prints such notes.
- Everything is written in the file's own line breaks, and the existing
block is compared regardless of them.
Validation:
- 25 new tests in installer-targets.test.ts:
- 20 on the TOML helpers: 8 header forms, CRLF, the old bug's duplicate,
look-alikes (other names, header-shaped text inside strings and
arrays), sub-tables, comments, 4 dotted/inline/array definitions, 2
shapes where a table can't be added, an entry inside an inline table;
- 5 on Codex install, re-install, refresh and uninstall end to end.
With main's toml.ts, codex.ts and installer index.ts put back, all 25
fail: 24 on the behavior itself, and one, a header with a trailing
comment, only on the new detection helper, since main already handled
that form. The 13 existing TOML tests pass on both.
- The real CLI on this Windows machine against private homes: `install
--yes`, a second `install --yes`, `install --refresh` and `uninstall
--yes --keep-cli`, for 23 config.toml shapes, each result parsed with
Python's strict tomllib.
- main (
|
||
|
|
6bd217a263 |
fix(cpp): code that calls operator++ by its name no longer stops an index or sync (#2769)
Indexing or syncing a C++ project stopped at "Resolving refs" with
"Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/:
Nothing to repeat" as soon as its code called an overloaded operator by
name, as `this->operator++();` inside `operator++(int)` does. Choosing a
method's overload reads the argument list after the name, and
cppParenListIn and cppOverloadParameters wrote the name into that
pattern with only `~` escaped. `operator++` did not compile. `operator()`
matched the name's own empty parentheses as the arguments, `operator[]`
never matched, and `operator+=` matched the next `operator=`. The same
helper serves every language that overloads by arity, so Scala `++`
overloads and Kotlin names in backticks that hold a `(` failed the same
way.
The fix adds one escape helper, escapeRegExp in src/utils.ts, the same
body as the private copies elsewhere. It is used at every place in src/
where a node name, a reference name, a piece of one, or a word read
from source goes into a RegExp unchecked: 114 interpolations in 13
files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and
the 2 overload readers only `~`. An AST pass over all 490 dynamic
RegExp constructions followed each interpolated value to its sources.
The 72 it still flags were each read by hand. They are
identifier-checked first, built from escaped names, constants, or the
CLI/MCP glob converters. The kernel's one dynamic regex already
escapes.
With operator arguments now read, nlohmann/json's `p->operator[](0)`
reached the argument-type scorer and picked `template <typename T>
operator[](T* key)`: a `size_type` parameter scored as an unknown
class, a `0` as a null pointer. `size_type` and `difference_type` now
count as numbers, as `size_t` does.
Validation:
- New __tests__/operator-method-names.test.ts (5 tests: init and sync of
an operator fixture, operator() and operator+= overload choice, the
json operator[](0) shape, Scala and Kotlin). With main's sources,
4 fail (3 throw "Nothing to repeat"); the json shape fails only with
size_type taken out of the number types. All pass on the kernel and
with CODEGRAPH_KERNEL=0.
- Census with a preload that logs every throwing RegExp: main threw
only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in
all 12 repos below.
- A/B on main
|
||
|
|
1ef8866aae |
test(explore): the cross-call dedup test passes with LF and CRLF checkouts alike (#2772)
explore-cross-call-dedup's "never re-sends a line it already sent" case has failed on every LF checkout (Linux, macOS, CI) since PR #2655. It passed on Windows only because core.autocrlf writes the fixture with CRLF. Root cause. Since PR #2655 the payroll fixture's RunResult struct (cycle.go lines 31-37) references Payslip through a field, so call 1 renders it. Call 2 renders the file's head, and that 7-line held run comes back inside it: dedupeRange keeps a covered run shorter than MIN_COVERED_LINES (8) in the emit set on purpose, rule 3 of the range algebra, which its own unit test pins. The handler case counted any repeated line as a duplicate. On a CRLF checkout explore's per-file character budget also counts each line's carriage return, so cycle.go rendered fewer lines and the case never met the short run. Fix, test only: - The case groups the repeated lines into consecutive runs and fails on a run of MIN_COVERED_LINES lines or more, read from EXPLORE_DEDUP. A shorter run is the algebra's deliberate re-send. What agents receive is unchanged. - The suite gives its copy of the fixture LF line endings, so every checkout indexes and renders the same bytes. Validation on Windows, with the kernel and with CODEGRAPH_KERNEL=0: - main's test fails with an LF fixture ("call 2 re-sent internal/usecase/payroll/cycle.go lines 31,32,33,34,35") and passes with the CRLF one. The new test passes 26/26 with both. - With the normalisation and main's assertion, a CRLF checkout fails the way an LF one does (lines 31-37): both checkouts now test one scenario. - Teeth: an algebra mutated to re-send held runs of 8-23 lines fails the new case ("call 2 re-sent internal/usecase/payroll/cycle.go lines 42-51 (10 lines)"). - tsc on the test file is clean. Contributor PR #2758 by @danusha2345 reached the same diagnosis. This change was written separately and also normalises the fixture. Fixes #2756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0abae11959 |
fix(installer): a symlinked instructions or config file is written through, not replaced (#2768)
Root cause: every installer write goes through one helper that writes
`<path>.tmp.<pid>` and renames it onto the path. A rename replaces a
symbolic link itself, not the file it points to. So a `~/.codex/AGENTS.md`
or `~/.claude.json` linked from a dotfiles repo became a detached regular
copy on install: the shared file never got CodeGraph's settings, and later
edits to it no longer reached the agent. This held for every target (Claude
Code, Codex, opencode, Cursor, Gemini, Antigravity, Kiro, Hermes and the
Copilot family) and for install, the refresh that `codegraph upgrade` runs,
and uninstall. Uninstall also deleted a link whose file it had emptied,
leaving CodeGraph's entry behind in the shared file.
Fix:
- The write helper resolves a link to the file at the end of its chain and
writes and renames there, so the link stays exactly as it was, relative
or absolute.
- A link to a file that doesn't exist yet gets that file created, but only
in a folder that already exists, as writing through it in a shell would.
No folder is ever created where a link points.
- A dangling chain is followed hop by hop from the real folder holding each
link, so a relative link under a linked folder lands where the OS would
put it.
- When the linked file can't be written (a read-only file, a folder that
doesn't exist), the error names the link and its target and nothing
changes.
- Where uninstall used to delete a user's file it had emptied (an
instructions file, Codex's config.toml, Copilot CLI's mcp-config.json,
the legacy project .claude.json), a link is kept and the emptied content
is written through it.
- The refresh sweep catches a failure per agent, reports it, still
refreshes the others, and exits non-zero, so the upgrade warns.
Decided: links to CodeGraph's own legacy files (Cursor's codegraph.mdc,
Kiro's steering doc), which install only ever deletes, are still removed.
Validation:
- 28 new tests. 25 are in installer-targets.test.ts:
- every target and location: install and uninstall keep each of its
files that is a link (18 cases);
- the shared files' content through install and uninstall for Claude
Code, Codex, opencode (two agents sharing one AGENTS.md) and Cursor;
- relative links under a linked folder;
- a link into a missing folder;
- a link to a read-only file;
- uninstall emptying through links;
- a failing link in a refresh;
- a folder link guard.
The other 3:
- 1 in upgrade.test.ts, where the refresh runs the built CLI;
- 2 in a new installer-links-simulated.test.ts, which simulates the
link with spied lstat/realpath/readlink, so it runs on machines that
can't create file symlinks.
With main's sources put back, 27 fail; the folder-link guard passes on
both. All pass with the fix. The real-link tests are skipped, with the
reason in their names, where file symlinks can't be created (Windows
without Developer Mode or admin).
- This Windows machine has Developer Mode on, so the real-link tests ran
for real.
- The real CLI against a private home (`install --yes`, `install
--refresh`, `uninstall --yes --keep-cli`), 10 linked files for five
agents plus relative, dangling and read-only links:
- on main, all 29 link checks found the link replaced by a copy;
- with the fix, none did, every link's text was unchanged, the shared
files got CodeGraph's settings and lost them again on uninstall, and
the missing-folder and read-only cases exited 1 with the new message
and changed nothing.
Fixes #2721
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d6e8c91e0a |
fix(installer): upgrade keeps everything around CodeGraph's section in instructions files (#2765)
Root cause: the write that refreshes CodeGraph's marker-fenced section in an agent's instructions file (CLAUDE.md, AGENTS.md, GEMINI.md) located the section with two independent searches: the first start marker anywhere in the file and the first end marker anywhere in the file. Everything between them was treated as CodeGraph's and replaced. A stray start marker above the real section (what is left when part of an older section is deleted by hand, or the marker quoted in the user's own notes) therefore stretched the "section" over the user's text, and when that marker was the first line the file came back as just the block, which is the report. The `codegraph upgrade` refresh (`codegraph install --refresh`) and plain `codegraph install` both go through this write, for the Claude Code, Codex, Gemini and opencode targets. The same pairing made a lone end marker look like "no section", so every run appended another block, and left a second block stale. Uninstall (and the npm preuninstall hook) and the Cursor rules-file cleanup used the same pairing to delete the same span. Fix: one parser finds the well-formed sections. Each end marker closes the nearest start marker before it, and a marker counts only when it is alone on its line, as every block codegraph has written puts it. A stray start or end marker is left as plain text. The refresh rewrites only the bytes between the first section's markers, using the line breaks that section already has, so a CRLF file stays CRLF and a current CRLF block is unchanged. Further sections are duplicates and are removed, keeping the text around them. With no section yet the block is appended after the existing bytes, which are no longer trimmed. Uninstall and the Cursor cleanup strip only well-formed sections. Validation: - 18 new tests (17 in installer-targets.test.ts: exact bytes for a stray start marker on line 1, a stray start marker with no section after it, a marker quoted in prose, a lone end marker, two sections, CRLF, no final newline, appending, a BOM, uninstall; a refresh plus uninstall per target for Claude Code, Codex, Gemini and opencode; the Cursor rules file. 1 in upgrade.test.ts: an npm upgrade whose refresh runs the built CLI against a private home). 16 fail on main's sources (the other 2 are guards that hold on both), all pass with the fix. - The real CLI on this Windows machine against private temp homes: `codegraph install --refresh`, 14 file shapes x 4 targets x 2 runs. Main had 28 bad results of 112 (the user's text lost in 3 shapes, a block added on every run after a lone end marker, a stale second block kept), the fix 0. With the fix, the bytes outside the section are identical in every single-section shape, and the input stays a byte prefix in every append. - `codegraph install --yes` and `codegraph uninstall --yes --keep-cli` on the reported shape: main kept none of the notes (install left only the 802-byte block, uninstall left 19 bytes); the fix kept them all. Fixes #2506 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
efff745783 |
fix(go): a field typed by a dot-imported package resolves in that package (#2764)
A Go type written without a package qualifier was looked up only in the
file's own package. In a file that dot-imports a project package, like
harbor's config tests (`import . ".../controller/config"`, then a
`controller Controller` field), the type isn't there, so the call
`c.controller.AllConfigs()` fell back to name matching. Name matching
took the same-named type of a package the file doesn't import, the one
that sorts first: matchGoFieldChainCall used the struct's own directory
for a bare field type, resolveGoMethodInPackage found no such type there,
and resolveMethodOnType picked the first of every
`Controller::AllConfigs`.
The same gap affected calls through parameters, locals and package-level
variables of such a type (one set by a conversion), through an alias of
it, and through methods promoted from an embedded field. It also affected
method values, type references, and the embedding edge itself where the
namesake was as near.
The field chain, the typed-receiver and method-value package lookup,
embedded types, alias targets, package-level values and Go type positions
now ask goBareTypeDir. It reads a bare name as Go does: the file's own
package first, then a project package the file dot-imports that declares
it. An exported name that only a dot import of a package outside the
project's modules can supply (`. "github.com/onsi/gomega"`) names none
of the project's types. The exception is a type that one of the file's
functions declares, which the index holds no node for: name matching
still handles it, as before. A bare embedding is retargeted only when a
dot import decides, and files without dot imports resolve exactly as
before.
Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel
and wasm backends. 18 tests fail on main and 2 guards pass. All
go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test
files pass (65 files, 1,140 tests). tsc and the build are clean.
Validation compares main
|
||
|
|
bf6f1fa67e |
fix(dart): a typedef references the types it aliases (#2763)
In
class Report { final int score; const Report(this.score); }
typedef ReportCallback = void Function(Report r);
`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.
Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.
Validation, kernel path, arms built from
|
||
|
|
dbb6aa9ba9 |
fix(vbnet): link reads through a namespace or a bare member name, and run the parameterless overload (#2762)
Three kinds of VB.NET member read that #2355 and #2377 left unlinked: - A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`, a constructor's `Me.New` / `MyBase.New`) went to the first overload declared, so `Me.Load` reached `Load(path)` instead of `Load(Optional force = False)`, and a `Me.New` whose first overload was the calling constructor itself linked nothing. - A read whose first name is a namespace (`Billing.Invoice.MaxLines`, `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front, a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was looked up as a value or a type only. - A field, property or Module variable named bare before the dot (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was not counted as read: a bare name is no member access, so only what is read through it could link, and nothing when that is .NET's (`Text`). Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output unchanged): - A method a reference runs without parentheses resolves among its type's overloads to one that takes no arguments (none, or only Optional and ParamArray parameters, read from the declaration), never the member the reference is in. AddressOf / NameOf keep the first overload, as before. - A receiver name that is neither a value nor a type is looked up as a namespace the way VB.NET does: nested in a namespace around the read, outward to the root namespaces, then in the file's and project's Imports. Each further link is a type declared in it, a nested namespace, or a member of one of its Modules. The read links the type (`Billing.Invoice`) or the member (`Assets.Logo`), as #2355's `Outer.Mode` reads do. - A bare name before the dot that VB.NET binds to a field, property or Module variable (not a local or parameter, which hide it) is linked as a read alongside what is read through it. Not for a `With` block's `.Text`, a method run through the name (the call is the method's alone, as with parentheses), another project's Module, or a member declared with an outside type of its own name (`Responser As Responser`), whose Shared members and nested types VB.NET reads through the type; a project type of its own name follows #2377's Color Color rule. - membersNamed looked a type's members up in every project's same-named type and memoised the list by qualified name alone, so SCrawler's two plugin `UserData` classes shared one list ordered by whichever was asked first. It now keeps to the type's own project. Alone this leaves main's graph unchanged on SCrawler and staxrip; without it, 17 of the new bare-name reads in one plugin linked the other plugin's members. Dropped from the 2026-10-04 test set: a method named through its class also linking the class (#2355 treats it as a call, and its test pins that); `AppSession.SessionId` through a property typed OtherSession linking nothing (#2377 links OtherSession's field, pinned the same way); `SCrawler.My.Resources...` reads (#2355/#2377 send no path that starts with `My` or `Global`); and same-named classes picked by namespace, Imports and project, which main already does once the fixture declares its RootNamespace. Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on main and passes; a mutation that restores main's membersNamed fails the own-project test. The other VB.NET, C#, extraction, resolution, sync and framework suites pass; tsc and build clean. Validation, main vs this branch (indexes from each arm's own build; nodes unchanged; self-loops unchanged; serilog and gin dumps byte-identical): - SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet` re-pointed from the two-argument overload to `ColorsGet() As DataColor`. Added: 4,973 bare-name reads (3,731 own class, 957 Module variables, 285 inherited), 192 namespace reads, 12 reads through a Module member reached by namespace, and that 1 overload. - staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with the root namespace in front, and `box.Show` reaching `Function Show() As DialogResult` from its Shared overload. - Checked against source: every bare-name read's name starts the access, has no local of that name, and binds to its class, an ancestor (23 through a qualified `Inherits Instagram.UserData` the graph's own extends edge misses) or a Module of its project; every namespace read's path ends its target's full name and the target's project is the reader's or one it references; no edge crosses between staxrip and its AutoCrop tool; 48 sampled edges hand-checked, all correct. - Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall 5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall 7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the growth being the new edges (unresolved refs fall 47,074 -> 43,607 and 38,051 -> 36,165). Not covered: a call through a bare name (`Button1.Focus()`) or a function named before the dot, reads in a `With Button1` block, and `My.` / `Global.` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b5d1d543d |
chore(scripts): regenerate the Go standard-library method table from Go's source (#2760)
go-stdlib-methods.ts lists the methods of 130 standard-library types with their parameter and result counts. Its generator lived in a validation kit. scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's Go grammar, counts with the engine's own goSignatureArity, and prints (or --write's) the table for the types it lists, so a later Go release's methods are one command away. Against go1.27.3 it reproduces the table byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
847f1df77d |
fix(go): an interface or struct embedding a standard-library type counts its methods (#2759)
goImplementsEdges (#584) follows declared embeddings since #2402, but an embedding of a type outside the index resolves to nothing, so it added no methods: prometheus's TBRun (testing.TB plus Run) went to a struct with only a Run, QueryLogger (slog.Handler plus io.Closer) had no implementations, kapacitor's Diagnostic (a third-party Logger plus Log) took every struct with a Log, and the predeclared `error` was not recorded at all. - go-stdlib-methods.ts lists the methods of 130 standard-library types, each as `name:params/results`, generated from go1.27.3's source and counted the way goSignatureArity counts a stored signature. - Both extractors record an embedded `error`, which stays unresolved. The predeclared-type set is unchanged, so alias and defined-type references still skip it. - goImplementsEdges reads each Go type's failed `extends` rows and reads the package back off the line (goRefQualification, now exported). A standard-library type's methods join the method set, keyed as goMethodKey keys the project's (`std:<import path>` for an unexported one), and the arity gate holds them to the table's counts on both sides: a `sync.Mutex` makes a struct hugo's `Locker`, not etcd's gRPC `LockClient`. Any other outside type is met by a type embedding the same one, or by the only type with every other method the interface names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1f3a274c53 |
fix(go): a method a type declares in a file the index couldn't read is not its embedded type's (#2757)
kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB limit, so every API type's own String, Marshal, Unmarshal, Size and MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over embedded types found the method the embedded TypeMeta or ObjectMeta has and linked that, a method the call never runs. resolveGoMethodInPackage, which every typed Go receiver goes through (parameters, locals, fields, assertions, package-level values, indexed elements), now reads the method declarations of the files of the type's package that the index couldn't read, from their text, and links nothing when the type declares the method there. matchMethodCall's typed branch and matchGoPackageValueCall keep that null instead of falling through to name matching, which found the same promoted method. A method the type really gets from an embedded type, like GetName from ObjectMeta, still links. The check that #2755 added for indexed elements moves into the shared path. On kubernetes 4c05e667, 63 calls lose that link and nothing else changes; etcd, prometheus, express and flask are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a5fe6e1a27 |
fix(go): a call on an indexed element reaches its element type's method (#2755)
A Go call on an indexed element (kubectl's `schemas[ix].ValidateBytes(data)`, deepcopy-gen's `(*in)[i].DeepCopyInto(&(*out)[i])`, go-to-protobuf's `m.Items[len(m.Items)-1].Unmarshal(...)`) reaches the resolver by its bare name, at the column where the receiver starts, and nothing typed the receiver: a method one type declares was guessed by name, often onto a namesake, and a common one like `DeepCopyInto` got nothing. matchGoIndexedCall, an early hook beside matchGoAssertedCall, reads the receiver chain with goChainLinks and types it as Go does: the head from the declaration in scope (goLocalDecl; readGoScopes now records where each `:=` or `var` value starts and which name it is) or a package-level variable; values (composite literals, make, new, append, conversions, fields, elements, addresses, range elements); struct fields, promoted ones too; and the element type of a slice, array, array pointer, map or named slice type, with `(*p)` dereferenced. The method is the element type's own, its interface's or one embedding promotes, and a type parameter's comes from its constraint. An element type from outside the project, a predeclared one, a type literal or an outside alias links nothing, and so does a method the type declares in a file of its package the index couldn't read (kubernetes' core/v1 generated.pb.go), which is read from that file's text. What can't be typed (a call's result) is left to the other strategies. On kubernetes 4c05e667 the indexed-element calls go from 760 resolved and 2,284 unresolved to 2,666 resolved and 378 unresolved; every changed site in kubernetes, prometheus and etcd is such a call, nodes are identical, and the express and flask controls are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
553d5d101d |
fix(sync): an import a name match took follows its module once it resolves another way (#2752)
A named import that a name match linked to a declaration elsewhere while
its module did not export the name kept that link after an edit gave the
module the name. A barrel's `export { EmptyState } from './EmptyState'`
declares nothing, so no definition delta, failed-ref retry or barrel walk
saw it (the "retargets" PR #2744 left). A name match into the module
itself, a class exported only as its default and imported by name, kept
its link after the default was dropped, where a fresh index finds nothing.
reopenNameMatchedImports (module-importers.ts): for each module the sync
modified, its importers' binding edges that resolved some other way than
their import (getImportBindingsResolvedElsewhere, an indexed range per
importer), whose import statement names this module, are resolved again
against the graph as it is (ReferenceResolver.resolveNow, writing
nothing). When the answer differs, the importer's edges through the name
and into the members of what the name match took go back for the sweep.
Regression test sync-name-matched-imports.test.ts: 4 cases comparing the
synced graph with a fresh index of the final tree. Fresh indexes are
byte-identical on 8 repos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
bbf987e13b |
fix(explore): the named symbols take turns in the dynamic-dispatch links list (#2751)
Links of the same rank were listed in edge order, so one named symbol's long fan-out could fill the 6-line list before another named symbol's first link. prometheus's `refresh.Discovery.refresh` hands its func field to 28 discovery packages, and in `refresh.Discovery.refresh hetzner.refresher.refresh hcloudDiscovery.refresh` those links filled the list before `refresher.refresh` → `robotDiscovery.refresh`. Each link now has a turn: how many links of its rank the named symbol it hangs from already has. Links sort by rank as before, then by turn, then by edge order, so links of the same rank alternate between the named symbols. A token that names two interfaces gets both: prometheus's `Appender.Append` matches storage.Appender and chunkenc.Appender, and storage's implementations no longer wait behind six of chunkenc's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |