mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
16f2d357bc49f4e54702d0c26df38c4a7c02da87
1149
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
16f2d357bc |
fix(go): a project package named like a standard-library package is not the standard library
A Go reference written through a package qualifier was dropped as a standard-library reference whenever the qualifier matched a name in the built-in list (errors, log, types, parser, encoding, template, ...). The check looked at the name alone, never at what the file imports under that name, so a project with its own package of such a name lost nearly every call into it: harbor's errors.New(...) through its lib/errors, prometheus's parser.Inspect(...) through promql/parser. What the file imports now decides before the name does. A qualifier the file imports, by its last path segment, the name goimports assumes for an unaliased import, or an alias, from a package of one of the project's own modules is a project package, and the reference goes on to normal resolution. A file that imports the standard library's package of that name, and a qualifier the file doesn't import at all, are skipped as before. Validation: the new test file has 7 cases, 4 of which fail on main; all pass here, with the native kernel and with CODEGRAPH_KERNEL=0. A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889 edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both, nodes unchanged. Every added edge was checked against its source: the line spells qualifier.Name and the file imports the target's package directory under that qualifier from inside the module (3,343 of 3,343), and a sample of 10 was read by eye. gin and cobra, and the controls express and flask, are byte-identical. Index CPU on harbor, median of 3 interleaved: 40.0 s on main, 39.9 s here. Original PR #2368 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
449eefa7da |
fix(js,ts): a call on an element of a class field reaches the element's type
A TypeScript or JavaScript method called on an element of a class field, like this.handlers[type].handle() or this.items[0].save(), kept the bare method name: a member chain rooted at this collapses to it so the resolver can read the owner off the enclosing class. A subscript in the chain leaves the resolver nothing to read, so the bare name exact-matched any project method of that name, often another class's (this.listeners[type].push(h) reached an unrelated UndoStack.push). Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain, or under window, emits nothing, as arr[0].run() already did. The resolver reads the element type the field's declaration writes (X[], Array<X>, ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and resolves the method on it or leaves the call unresolved: a type parameter of the declaring class, a union with another type, an array of arrays or a non-plain element hold no one type. The class type-parameter reader now reads from the keyword to the list's own >, over several lines and through braces in constraints. Our changes: kept escapeRegExp from PR #2769 in the conflict; the type-parameter reader also accepts the type keyword, since the T['p'] path passes type-alias owners and the PR as written read State<Item>['current'] as the project class Item (new case in ts-array-annotated-receivers); CHANGELOG covers JavaScript and asks for a re-index. Validation: the element tests fail 10 of 11 on main with both engines; the type-alias case fails on the PR as written and its brace half on main. A/B against main, kernel built per arm: flask and gin byte-identical. TS: pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9 wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a merged mixin member, an index-signature type, an any field); typeorm 3 wrong removed, 1 right lost, 2 retargeted to the declared interface; excalidraw 1 wrong removed; vscode's base and editor 19 added, 24 retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by name: a getter, an index-signature field, deep chains, a T[] parameter, a union alias). JS: three.js, playcanvas and pdf.js lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM calls on project classes). Kernel parity holds on every repo with deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s, three.js 64.7 s to 67.9 s, medians of 3 interleaved). Original PR #2766 by @inth3shadows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5c5bfd4051 |
fix(php): a namespace alias matches whatever case it is written in
PHP reads namespace names without regard to case, so after
use App\Fields as Field; the name field\FirstName is the same class as
Field\FirstName. The qualified-name resolver compared the alias as
written, found no import, and read the name relative to the file's own
namespace instead, so new field\FirstName() stayed unlinked. The alias
is now matched without regard to case.
This lands the first half of the contributor's PR. Its second half
refused an alias that two namespace { } blocks of one file bind to
different namespaces. That also dropped the first block's correct edge,
because the use map is file-wide, and the case doesn't occur in real
code: across 8 PHP repos (33,820 files), 185 files declare more than
one namespace, and none binds one alias twice outside strings. The fix
for those files is to scope each use statement to its namespace block,
which needs block positions the index doesn't keep yet: a braced
namespace gets no namespace node, and neither does a second unbraced
one, so it is left for its own change.
Validation: the new test fails on main and passes here, with the
native kernel and with CODEGRAPH_KERNEL=0. Graphs of php-parser,
composer, psalm, monica, laravel and phpunit are byte-identical to
main, as are the controls express and flask. Index CPU on laravel,
median of 3 interleaved: 53.3 s on main, 52.9 s here. The PHP and
Laravel suites pass in both engines (10 files, 67 tests).
Original PR #2369 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
33dc2ae72b |
fix(viewer): the routes list's limit counts routes, not one route's calls
The routing manifest has a row per (route, symbol it reaches), and the routes list behind Entry points and the Steps picker folds those rows to one per route. To have rows to fold, it fetched limit * 4 + 1 of them, so an inline handler, which has a row per call in its body, could use up the window by itself, and the routes after it were never fetched. On an Express app with one 24-call inline handler before two routes, limit=3 listed only GET /dense. getRoutingManifest takes a perRoute flag that applies its limit to routes, in the same file and line order, and returns every row of those routes. Without the flag the query and its result are unchanged. Added on top of the contributor's change: the window is still over-fetched, four routes per route shown, because some routes are dropped only after their rows come back (a test or generated handler, an inline route written in a test). A window of exactly limit + 1 routes could hold only those. On hono, whose routes are nearly all registered in test files, Entry points then listed 0 routes instead of main's 28, and the routes list 21 instead of 32. With the over-fetch, every route main lists is still listed at each limit tried (3, 12, 60, 200 and 300), and hono's Entry points list 32. truncated also keeps main's check that the window came back full. Validation: the new limit=3 test fails on main (only GET /dense) and passes here. A project whose test-file routes sort first fails with the PR as first written (it reads as unrouted) and passes here and on main. Routes listed on Wiki.js (132 routes) and realworld-express are identical to main, and on hono they differ only by the additions above. The ui suites and the other route consumers: 34 files, 723 passed, 0 failed, 3 skipped (environment-gated). The changelog line is in docs/viewer-launch-changelog.md, since the viewer is not released yet. Original PR #2370 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2def0a6d6d |
test(kernel): skip the kernel-only suites when CODEGRAPH_KERNEL=0
CODEGRAPH_KERNEL=0 sends every language through wasm, and running the suite with it is how the wasm path is exercised on a machine that has the kernel built. Three kernel-only blocks failed in that run instead of skipping, because they skipped only when the .node file was missing: kernel-deep-nesting asserts in beforeAll that some language is routed to the kernel, kernel-retry-materialize asks for a raw kernel result and gets null, and the C++ brace-scope kernel parity block in cpp-brace-scopes gets no kernel result either. Each of the three describe.skipIf conditions now also skips when CODEGRAPH_KERNEL is '0', the same outcome as a checkout without a kernel. The kernel-independent self-heal block in kernel-retry-materialize still runs. Validation, the three files: on main with CODEGRAPH_KERNEL=0, 5 tests fail and kernel-deep-nesting fails at beforeAll; here, 13 pass and 13 skip. With the kernel (CODEGRAPH_KERNEL_EXPECT=1), 26 of 26 pass, nothing newly skipped. All 75 test files that reference the kernel pass with CODEGRAPH_KERNEL=0 on this commit (1,832 passed, 0 failed), so these were the only kernel-only blocks that failed that way. Test-only, so no CHANGELOG entry. Original PR #2372 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
679c4e095a |
test(windows): the windowsHide guard reads quoted and computed keys, and named options
The static guard in child-process-windows-hide.test.ts, which checks that
every spawn under src/ sets windowsHide: true, matched the key by its
source text. A quoted key was a different property to it, so
{ windowsHide: true, 'windowsHide': false } passed as hidden and
{ 'windowsHide': true } was flagged. A computed key written after
windowsHide: true, like [key]: false, was skipped, so it passed too. And
(file, options) with the args array omitted was recognized only when the
options were an object literal, so execFileSync('git', opts) was read as
passing no options and flagged.
A quoted key is now the same property, a computed key makes the options
unprovable from that point on, and with exactly two arguments the second
one is what gets checked, so an args array there is still an offender.
Validation: with main's guard, exactly the 4 new table rows fail and the
other 29 pass; with this change all 33 pass, including the scan of src/,
which flags nothing on either side. Test-only, so no CHANGELOG entry.
Original PR #2371 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
9c4794d694 |
fix(rust): a call keeps its whole receiver chain, and links through method calls in it (#2846)
A Rust method call made through more than one field, or on what another
method returns, was recorded by its bare method name in both extractors:
self.services.store.put("a") became `put`, as did self.store().put("m").
Only a single field kept its receiver (self.store.put). The edge's
refName then said nothing of which value the call goes through, and a
call on a method call's value was never typed, so self.store().put("m")
stayed unresolved. PR #2831 had already made the field-chain calls link
by reading the receiver from the source text, but their names stayed
bare.
Both extractors now keep a receiver that is a chain of fields and method
calls on `self` or on a name, a call written `name()` with its arguments
and turbofish dropped, up to 16 links: self.services.store.put,
self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that
already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are
unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer
chain rooted at a path keep the bare name. The kernel's receiver_chain
mirrors the wasm extractor's rustReceiverChain byte for byte.
Resolution reads such a call as the bare-named call was read before
(rustChainedCallByName, at the top of resolveOneIn), so the frameworks,
the strategies and the scope gates see what they saw, and the edge keeps
the call as written. The chain goes along: the written-receiver hook
reads the receiver the chain names where the call is written
(rustChainedReceiver), so a method call's value is typed from that
method's signature, and a later link that calls the same name again is
told apart instead of being left to a guess by the name.
Validation:
- New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's
workspace verbatim plus self.store().put("m"), chains whose links call
the same name through self, a tuple field and a parameter, a call on a
method's std return value, the recorded shapes and the 16-link cap,
and a sync that adds the missing method. Red on main's sources, 7 of
10 failing on both the wasm and the kernel path; green with the fix.
The Rust kernel parity suite gains a chain case. The extraction test
that pinned the old collapse now pins the chains.
- A/B against main
|
||
|
|
865ddf4ada |
fix(library): indexFiles keeps other writers out until its files are stored (#2845)
CodeGraph.indexFiles returned the orchestrator's indexing promise from inside the try whose finally releases the cross-process write lock. A returned promise leaves the try at once, so the finally ran as soon as indexing started, and the lock was gone while the files were still being extracted and stored. Another process's sync or index of the same project, such as a git hook, the CLI or an MCP server, could then write to the index at the same time. indexAll and sync await inside their try and hold the lock to the end; the in-process mutex already covered the whole call. Fix: await the indexing inside the try, so the lock is let go only once the files are stored, or when indexing fails. Validation: - New __tests__/index-files-write-lock.test.ts (2 cases). A second handle on the project stands in for another process, and the real indexing waits at a gate the test opens. While indexFiles runs, the other handle's sync must be refused with LockUnavailableError. With main's src/index.ts it ran to completion instead (red); with the fix it is refused, and admitted once the files are stored (green). When indexing fails, the lock is let go (passes on both). - The lock and sync test files pass alongside: sync.test.ts, cli-sync.test.ts, cli-unlock.test.ts, orphaned-refs-sweep.test.ts and sync-rebuild-convergence.test.ts, the last two of which call indexFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
23eb392915 |
fix(ts): a re-export's import node keeps export type * as the file writes it (#2844)
PR #2837 made `export type * from './x'` parse by blanking its `type` to spaces before either engine parses the file. Both engines read an import node's stored statement from the parsed bytes, so every such re-export stored `export * from './x'` (883 nodes over five repos). Nothing in resolution reads that text, but it is the statement's record in the graph. The source as written now reaches every place that builds those nodes: - TreeSitterExtractor keeps it whenever preParse changed the text, and a kernel-deferred file hands it over with the preParsed bytes; - a language opts in per file with `reExportAsWritten` (TypeScript: when its `export type *` blank changed something), and an `export … from` import node then reads its statement from the source as written; - the kernel's decoded result takes those nodes' signatures from the original source by each node's span, and the undecoded worker fast path steps aside for such files, as it does for C++ files with macro methods. Pascal's `signatureAsWritten` is unchanged: getSignature reads the source as written only for an extractor that asks. Validation: - ts-export-type-star.test.ts gains three tests and a signature check in each of its eight barrel cases (18 tests). 11 fail with main's four source files restored. Breaking the deferred-file hand-over, or the raw-path opt-out, fails exactly its own test. All pass with the fix. Pascal's as-written signature tests and the C, C++ and C# preParse parity suites pass unchanged. - A/B against main |
||
|
|
d833827503 |
perf(resolution): skip receiver typing for JS/TS calls of a method no node is named (#2840)
A JS/TS call of a method that no node in the index is named, like
`list.push()`, `fs.writeFileSync()` or a test library's matcher, still
went through matchMethodCall's receiver-type inference, the TS binding
and import readers, and the class strategies. Every one of them ends in a
lookup by the method's name, so for such a name they can only answer
null. The one exception is the object-literal reader, which follows a
member to the binding the literal gives it: `const api = { alias:
renamed }` makes `api.alias()` a call of `renamed` (Issue #1932).
Root cause: matchMethodCall reads the calling file for the receiver's type
before anything tests the method's name.
Fix: the resolution context answers isAbsentName(name) from the set of
node names warmCaches already loads for the pre-filter. The answer is
exact, like getNodesByName, and false whenever the set isn't loaded. For a
JS-family call whose member is absent, matchMethodCall asks only the
object-literal reader, exactly as the full path would. When the reader
answers, the full path runs unchanged; otherwise the call returns null at
once. CodeGraph.indexFiles now drops the resolver's caches after it stores,
as indexAll and sync already do before they resolve, so a later resolution
never reads the name set from before the new files.
Validation:
- New __tests__/absent-member-calls.test.ts (6 cases). The skip reads
nothing of the calling file (red on main). The name set answers only
once loaded. Every path that links a member to a symbol of another name
is pinned with the member asserted absent: a literal alias, a shorthand
member bound to a renamed import, an `as const` literal, a holder in the
same file and one imported, and a renamed re-export read through a
namespace import. A method added through indexFiles after indexAll still
links (main links it; the skip without the cache drop did not). A
fixture indexes identically with and without the skip.
- Graphs byte-identical to main (scripts/dump-graph.mjs: nodes, edges with
metadata, refs, files), kernel built per arm, on express,
vue-element-admin, excalidraw and this repository (JS, TS, Vue, Svelte,
Astro) and on the controls flask, gson, gin, ripgrep and slim (Python,
Java, Go, Rust, PHP). Default mode (pooled on the two largest) for all
nine, single-threaded for the JS four, and again after rebasing onto
the current main. Main run twice is identical too. An incremental sync
adding a file that declares formerly absent names (1,088 failed refs
retried) is identical between the arms, with and without git, and
matches a fresh index.
- Survey of main (instrumented build, nine repos, every language): no call
whose member no node is named was resolved by matchMethodCall. The skip
fires on exactly the calls the survey found (12,354 here).
- Profile counters, single-threaded: receiver inference runs 38,626 times
instead of 50,980 on this repository, 6,460 instead of 9,306 on
excalidraw, 2,488 instead of 3,586 on express. Outcome counts are equal.
- Cost, medians of 5 interleaved runs: the resolve loop on this repository
4.54 to 4.41 s pooled and 16.85 to 16.29 s single-threaded, faster in 5
of 5 paired rounds each; resolution phase -2.1% and -2.3%; whole index
-1.3% and -2.2%. excalidraw's loop -1.6%, express's -5.0%; the five
controls within noise (3 runs each).
Fixes #2829
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6500a3782b |
fix(cfml): a <cfquery> or <cfscript> block that fails to parse no longer flags the file (#2843)
Both delegates in cfml-extractor.ts read a block with its own grammar
through a fresh TreeSitterExtractor and copied its errors onto the file.
That extractor warns "parse produced no symbols (tree has errors) - the
file is indexed but contributes nothing to the graph" when its tree has
errors and it found no symbols: true of the block, not of the file, and a
`<cfquery>` body never declares a symbol. So `codegraph init` printed the
warning and `codegraph status` counted the file among those whose symbols
are missing, though its component, methods and calls were indexed.
extractTagExpressions already drops the warning for the same reason.
Fix: the delegates record that warning as one naming the block and its
line, with code partial_parse, so `codegraph files --json` shows which
block failed while init and status, which key on parse_error, no longer
report the file. Other errors pass through unchanged.
The issue's own example SQL parses cleanly with the bundled cfquery
grammar, so the reporter's trigger was not reproduced; any query body the
grammar can't read (here `EQV` in a <cfif> condition) and any
statement-only <cfscript> block with a syntax error showed the same
warning.
Validation:
- New cfml-embedded-parse-warning.test.ts: the repro DAO keeps its class,
both methods and the call inside the failing query, and records one
partial_parse warning naming <cfquery> at line 6; a failing <cfscript>
block likewise; getIndexHealth lists neither file. All 3 fail on main's
extractor and pass. status-index-health.test.ts passes.
- On the repro, main prints the whole-file warning during init and status
says 1 file could not be parsed so its symbols are missing; with the fix
neither does.
- A/B vs main
|
||
|
|
ef05970bb4 |
test(daemon): the OS process lookup tests pass on a loaded Windows machine (#2842)
daemon-lock-holder.test.ts > "reads a running process from the OS" failed under load on Windows, and so did the seven CLI tests in the file that look a process up. describeProcess gives each OS query 5 s: on a busy machine PowerShell takes longer than that to start and run its CIM query, so the lookup falls back to tasklist (the image name only, or nothing once tasklist runs out too) and the command line is null. The in-process test's toMatch(/ping/) then fails, and the CLI the other tests spawn sees a Node holder it can't classify, keeps the lock, and says so. In production that fallback is the intended fail-safe: an undescribed holder stays "unknown" and its lock is kept. The per-query budget is still 5 s unless CODEGRAPH_PROCESS_LOOKUP_TIMEOUT_MS says otherwise, and nothing outside the tests sets it. The test file sets it to 60 s for itself and for every CLI it spawns (an env var reaches both), gives the lookup tests time to match (240 s per CLI run, 30 s for a killed holder to go), and adds a Windows-only test that a lookup that runs out says nothing, so the holder stays "unknown". Validation (Windows, 16 logical CPUs, a 48-thread CPU burn running): - describeProcess on a live PING.EXE: the 5 s budget answered nothing in 8 of 8 lookups (about 11 s each); a 60 s budget read the command line in 6 of 6 (median 12.6 s, max 13.6 s). - The reported test alone: main 0 of 4, each "expected '' to match /ping/i"; this branch 2 of 2, and 6 of 6 with an earlier revision that passed the same 60 s budget as an argument. - The whole file: main 3 of 11, the 8 failures being exactly the tests that look a process up; this branch 12 of 12, twice. - No load: 12 of 12 in 20 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f406fe0064 |
fix(cfml): createObject("component", path) links its component, and .init() on it (#2841)
`new a.b.Batch()` made an instantiates edge, but
`createObject("component", "a.b.Batch")`, the form older CFML uses most,
was extracted as a call to the engine function createObject, which never
resolves. A call chained onto either construction, `createObject(...).init()`
or `new a.b.Batch().init()`, was extracted as a bare `init`, which the
name matcher bound to whichever component's init came first (0.4), or to
none when there are many.
Fix:
- cfscript extraction (tag expressions included): a component
createObject, with two arguments, one, or named ones, makes an
instantiates reference under the dotted path as written, not a call.
Java and other object types, and names built at runtime, are unchanged.
- A call on a construction is extracted as `<component>().<method>`.
- The resolver reads a dotted instantiates path with
resolveCfmlComponentPath, the rules extends uses, and resolves a chained
call to the method of that component or one it inherits (matched
ignoring case; inheritance is wired before calls). A bare component name
is the one in the calling file's folder, else the only one so named.
Without both, the call gets no edge rather than a guess by name.
- resolveCfmlComponentPath also accepts a component at the project root
for a path with one folder segment, its file named like it:
`wheels.JobWorker` in Wheels' own source, where `wheels` maps onto the
root. Without it Wheels' core lost calls like
`new wheels.JobWorker().processNext()` that name matching had found. It
ranks below any folder match; extends gains the same.
Validation:
- New cfml-create-object.test.ts (5 cases) and a root-component case in
cfml-inheritance-resolution.test.ts: 5 of the 6 fail on main (the sixth
pins the variable typing that already worked), all pass.
- A/B vs main
|
||
|
|
d90f6abec7 |
fix(cfml): <cfinclude template> and cfinclude() link the code to the file they include (#2838)
Only cfscript's `include "x.cfm";`, written outside any function, linked
the including code to the template. CfmlExtractor never read
`<cfinclude template="...">`, which the grammar parses as a
cf_selfclose_tag whose name is only in the text after `<cf`, so the tag
form made nothing anywhere. cfscript's include hook read only the string
form, `cfinclude(template="x.cfm")` went through the call extractor as a
call to `cfinclude`, and the function-body walker never extracts import
statements, so an `include` in a function was dropped. Resolution went
through the generic pipeline: the file-name matcher links a relative path
like `../shared/helpers.cfm` only when no other file shares its name, and
past it the symbol strategies could take another template's import node
of the same path, or the including file itself.
Fix:
- `<cfinclude template="...">` is extracted like the script form: an
import node in the scope it sits in (function, component, or the
template's file) and an imports reference from that scope.
`cfinclude(...)` and `include template="...";` make the same import and
no call, and an `include` in a function body counts, from the function.
- A path built at runtime (`#dir#/x.cfm`) is no import, in every form.
- A CFML include of a template path resolves as CFML does: relative to the
including file's directory, or from the project root for a leading `/`
(less sure, 0.85), to the file at that path, ignoring case when one file
matches. A path that names no file there goes to the file-name matcher,
as before, but only ever to a file, and never to the including file.
Validation:
- New cfml-include.test.ts: the tag form at component level (same folder,
../, upper case, unquoted), in a function body inside <cfif>, in a
template and from the web root; the script forms; runtime-built paths,
cfinclude(template=variables.x) and <cfmodule> (no import); a relative
path with a namesake elsewhere; an unmatched path that another template
also includes, and a path whose only namesake is the includer. 5 of 7
fail on main, all 7 pass.
- A/B vs main
|
||
|
|
cf3e6bc525 |
fix(ts): a type-only re-export like export type * from './types' now parses (#2837)
TypeScript 5.0's type-only re-export of a whole module, `export type * from './x'` and `export type * as ns from './x'`, has no rule in the vendored tree-sitter-typescript grammar (v0.23.2, the revision the kernel's crate compiles) or in upstream master: `type` parses as an ERROR inside the export statement. The graph was already right, since the import resolver reads the statement from the file (PRs #1955 and #2526), but the error tree meant a barrel of re-exports got the "parse produced no symbols" warning and a parse_error row, `codegraph status` listed it among the files it could not parse, and the native kernel deferred every file with such a line to wasm. The TypeScript extractor's preParse (.ts and .tsx) now blanks that `type` to four spaces where a statement starts (a line start, or after `;`, `{`, `}` or a block comment) when `*` is followed by `from` or `as`, so the line reads as the `export *` it is to the graph. Offsets are kept. The kernel route applies the preParse before calling the kernel, so both engines parse the same bytes and no Rust change is needed. A grammar patch would mean vendoring and regenerating the grammar for both dialects, two wasm rebuilds and a kernel off the crates.io crate, for a statement `export *` already models. A literal pre-check skips other files: 16 ms over strapi's 4,659 TS files. The only node change is an import node's stored statement text, which now reads `export * from './x'`. Import nodes are filtered from search, explore and the context builder, and nothing in resolution reads it. status-index-health.test.ts used `export type * from` as its unparseable file (issue #2336); it now uses a file with merge-conflict markers. Validation: - New ts-export-type-star.test.ts, 15 tests: 13 fail with main's typescript.ts, 11 with the blank unwired, all pass with the fix (kernel and wasm, .ts and .tsx, LF and CRLF). - A/B, kernel engine, main vs fix, dumps by natural keys: masto.js, kysely, TanStack/query, graffle and strapi keep every edge and ref (162,421 and 192,248 on strapi). The only changes are 883 import signatures, one per `export type *` statement, and 76 cleared warnings (masto.js 11, kysely 1, query 4, graffle 28, strapi 32). The Go and Python controls gin and requests are byte-identical. - Kernel parity, typescript and tsx: 0 diffs on every sweep. Deferrals drop masto.js 11 to 0, graffle 203 to 13, strapi 105 to 69, query 32 to 27, kysely 14 to 12; the rest are other grammar gaps. - Index time, median of 3 interleaved: graffle 12.7 s vs 13.1 s, query 11.2 s vs 11.4 s, within noise. Fixes #2474 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
51c4124dad |
fix(cfml): a dotted extends finds its component when a test suite mirrors its path (#2835)
A CFML component that extends another by its dotted path, like
extends="contenscms.core.basecore", got no extends edge when a test
suite held a file of the same name in a folder of the same name, such as
a TestBox spec at tests/testbox/tests/specs/unit/core/basecore.cfc.
resolveCfmlComponentPath counts the path's folders that match a
candidate's, from the right. contenscms is a server mapping onto
contens/, so only core matched, for the component and the spec alike,
and a tie gives no edge. The rule the resolver applies elsewhere since
PR #2209, that production code never names a symbol in a test suite,
never ran on this path: it returns before the name matcher's visibility
gate.
Fix:
- A component in a test suite is no candidate for a reference from
production code (isTestSuitePath, now exported from the name matcher).
This also keeps application code from extending a test file that
happens to match more folders.
- A test's tie between a component and a suite file that mirrors its
path goes to the component outside the test suites. Two production
components still tie.
Validation:
- cfml-inheritance-resolution.test.ts: 2 new cases (the issue's tree in
script and tag form, a test extending the component, a mirror that
matches more folders, a path only a test suite holds, a test naming its
own suite) fail on main's resolver; all 12 pass with the fix.
- The issue's repro: page and tagpage now extend contens/core/basecore.cfc
(qualified-name 0.9); on main both stayed unresolved.
- A/B vs main
|
||
|
|
16a8e77c46 |
fix(ruby): a call reaches the class method or the instance method its receiver calls (#2834)
A Ruby class method (`def self.build`, `def Widget.build`, or a def inside
`class << self`) was indexed exactly like an instance method: is_static 0,
under the same qualified name as an instance method of that name
(Widget::build twice). So every call took whichever of the two the class
declared first. Widget.build could reach `def build`, and widget.build on an
instance could reach `def self.build`. In the repro for issue 2503,
`card = Billing::ChargeCard.new(...); card.call` linked to `def self.call`
rather than `def call`.
Fix:
- Extraction, both engines: the Ruby isStatic hook (isRubyClassMethod in
languages/ruby.ts, is_class_method in codegraph-kernel/src/ruby.rs) marks a
singleton_method, or a method whose parent past one body_statement is a
singleton_class, as static. Every other def is marked not static, the same
way on both, so the graph only changes for class methods.
- Resolution, Ruby only, where the call says which kind it is:
rubyMethodsOfKindFirst orders a call's candidates with the wanted kind
first. A call on a constant (Strategy 1, its inherited walk, and the
namespaced path in ruby-constant-paths.ts) wants the class method. A
receiver typed by `x = Widget.new` (resolveMethodOnType) wants the
instance method. A bare call splits only a real pair, one qualified name
among its candidates as both kinds, keeping the caller's kind: a block
may run on another object, so nothing else moves. A receiver known only
by its name (Strategies 2 and 3) says nothing of its kind and is left as
it was: rubocop's `cop.cop_name` runs over cop classes. With only one
kind present, every path picks what it did before.
Validation:
- New __tests__/ruby-singleton-methods.test.ts (6 cases, each on the
kernel and on wasm) and a parity case in kernel-ruby-parity.test.ts. On
main 9 of the 18 tests in the two files fail, every one that checks the
new behaviour; the guards and the old parity cases pass. All pass with
the fix.
- A/B against main
|
||
|
|
f7820b584f |
fix(dart): calls to a private name in another part of the same library link again (#2832)
Dart keeps a `_name` to its library: the library file and every file its `part` directives name. isVisibleAcrossFiles still listed Dart in PRIVATE_IS_FILE_LOCAL (added with PR #1745 for Kotlin, Java, C#, Swift, Scala and PHP), and both Dart extractors mark a `_` function, method or class `private`. So once the name-matching pipeline had picked a private candidate in another file, the pick was declined and the reference stayed unresolved. A library split into parts, generated `.g.dart` and `.freezed.dart` parts included, lost the calls and type uses between its files. Fix: - dartPrivateVisible (dart-libraries.ts) says whether a file belongs to the reference's library, read from `part` / `part of` alone. Unlike inSameDartLibrary it needs no pubspec.yaml, since a part names its library by a path or a library name. When the reference's library can't be found, the name stays file-local, as before. - isVisibleAcrossFiles takes Dart out of PRIVATE_IS_FILE_LOCAL and lets a private Dart candidate through when it is in the reference's library. - matchByExactName drops another library's private Dart candidates before the ranking, as it does for C#, PHP, Java and Go names the file can't see. Otherwise another library's namesake can rank first, be declined, and take the library's own declaration down with it. Without a pubspec nothing else filters a top-level `_name`, and a member's class hierarchy is matched by type name, which can reach another library's class of the same name (the SDK's wasm and VM `_ImmutableLinkedHashMapMixin`). Validation: - New __tests__/dart-part-privacy.test.ts, run with and without a pubspec: the issue's library plus a receiver call, a generated part's `_$UserFromJson` and `_$Counter`, another library that imports the one declaring the names, a namesake beside a part, a part named by library name, and a part whose library is missing. 6 of 12 fail on main's resolver, 12 pass. With the pre-ranking filter disabled, 1 fails (the namesake without a pubspec). - A/B vs main |
||
|
|
8fdc8a1efb |
fix(rust): calls on field chains, struct literals and outside-type paths link by type (#2831)
Issue #2614 reported four Rust calls that linked to a same-named project
method. Case 1 (a slice's contains) was fixed on main by the typed-receiver
work. The other three still reproduced on main in both engines:
vis.map.get("a") on a HashMap field went to Ext::get, J::from(i) under
`use serde_json::Value as J` to a local `impl From<io::Error> for Fail`, and
Xyz { x: 1.0 }.to_val() to Stat::to_val.
Root cause. The extractor keeps one receiver level on a call, so a deeper
field chain (vis.map.get, self.a.b.m, self.0.m) or a struct literal reaches
the resolver as the bare method name, and the typed-receiver reading only
ran for x.m and self.field.m refs. The name strategies then took the one
project get, or one of the two to_val impls. J::from matched no qualified
name, J names no project type, and a single capital letter is read as a
possible generic parameter, so Strategy 3 took the crate's one from. The
same family: under an unrenamed `use serde_json::Value`, Value::default()
linked by qualified name to a project Value's impl.
Fix.
- matchRustWrittenReceiverCall, an exclusive hook beside the as_mut one:
for a bare-named call, read the receiver from the text at the ref's
column. Only a field chain rooted at a local, a parameter or self, or a
struct literal (alone or in parentheses) is read; it is typed with
rustValueType and the call resolved with rustTypedMethodCall. Any other
receiver, an unreadable one, or a type no node names (one a macro
declares) is left to the name strategies. A provided method of a project
trait the type derives counts (axum-extra's #[derive(TypedPath)]).
- rustFieldValue reads an element of a tuple-typed field (self.pair.0).
- isRustPathCallReachable in gateRustScope: a call through a type path
whose type the file's use, or the written path, names from std or a
dependency reaches only the project's impl of a trait for that type, or
for a trait's path the project's impls of that trait. A path dependency
the project holds is the project's.
Validation.
- New __tests__/rust-written-receivers.test.ts (12 tests: the issue's
crate verbatim, chain/literal/derive/path variants, guards): 9 fail on
main's sources, all pass with the fix. The 27 Rust test files plus
resolution and cross-language resolution pass.
- A/B against main
|
||
|
|
dcb985f1d2 |
fix(ruby): a call on a namespaced class links its caller to the class and its method (#2827)
A Ruby call on a constant written with its namespace, like Billing::ChargeCard.call(...) or ::Foo.call, recorded no edge. Both extractors (the kernel's ruby.rs and the wasm one) emitted the receiver's references ref only for a bare `constant` receiver, so a scope_resolution receiver got none. The call ref itself, Billing::ChargeCard.call, matched no resolver strategy either: the method-call patterns reject a `::` in the receiver. A class used only through such calls showed no callers and no dependents. Fix: - Both engines treat a constant path (a constant, or a scope_resolution of constants, a leading `::` included) as a constant receiver and emit the references ref under the receiver as written. A path through a value (obj::Thing, self.class::LIMIT) still emits none, and .new stays an instantiation. - A new resolver step, src/resolution/ruby-constant-paths.ts, resolves a Ruby references ref written as such a path, and a calls ref on one, the way Ruby looks the constant up: the class or module around the reference, then each namespace outside it, then the top level; a `::`-rooted path at the top level only. A call reaches the class's or module's own method, else the first one its ancestry declares (rubyAncestry, now exported). The step runs before the pre-filter, which dropped `::`-rooted names, and it is exclusive: a path the project doesn't declare (ActiveRecord::Base) or a class without the method links nothing, never a namesake by its last name, and production code never reaches a class only a test declares. Validation: - New __tests__/ruby-namespaced-receiver.test.ts (8 cases, each on the kernel and on wasm, plus a sync case) and a parity case in kernel-ruby-parity.test.ts. On main 16 of the 22 tests in the two files fail (the rest are guards and the old parity cases); all pass with the fix. Reverting only the resolver step, or only the wasm extractor, turns 13 and 10 of them red. - A/B against main (kernel path), every edge triaged: sinatra +23 edges, rubocop +168, mastodon's Ruby tree (4,363 files) +676; 0 removed, nodes identical. 866 of 867 are right: references by the path as written or under an enclosing namespace (each such case checked against the call site's own nesting), calls to a class method, a module_function method, or an inherited class method. 1 is wrong: Paperclip::Transcoder.make reaches the instance method `make`, since a singleton method and an instance method share one qualified name today; a bare Transcoder.make does the same on main. Controls express and flask are byte-identical. - kernel-parity.mjs --lang ruby: sinatra 147/147, rubocop 1799/1801 (the same 2 deferred on main), mastodon 3333/3333, 0 diffs. The fix arm's kernel and wasm graphs are byte-identical on sinatra and rubocop. - Index time, median of 3 interleaved runs: +0.4% to +1.7%, the same as the controls (+0.8%, +1.3%). Fixes #2503 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5b47bc4963 |
fix(js,ts): .tsx and .jsx classes link their overrides as .ts and .js classes do (#2828)
The interface-impl pass links a base class's or an interface's method
to each subclass's override, so a call through the base reaches the
method that runs. It took a class's methods, and its supertype, by
language: typescript and javascript were in its language set, and tsx
and jsx were not. A class in a .tsx or .jsx file got no override
edges, and neither did a .ts class whose base or interface was in a
.tsx file. The pass's own gate, the static/instance split of #2761
and the other JS-family lists already count tsx and jsx, and the
set's history (#494, #708) gives no reason for leaving them out.
tsx and jsx are now in the set, so these classes get the edges that
.ts and .js classes get, statics paired with statics.
Validation: the new test fails 4 of 4 on main
|
||
|
|
58eb57a324 |
fix(nuxt): navigateTo('/x') links to the page it opens, as router.push does (#2826)
Nuxt's navigateTo was in the Vue resolver's list of Nuxt auto-imports,
which answers each with a sink: an edge from the calling node to itself
at confidence 1. The Vue resolver runs before Vue Router's, so the call's
destination was never read. Every navigateTo, in pages and route
middleware alike, became a self-pointing calls edge, and no navigates
edge was drawn; the sink also kept sync from revisiting the call when a
page appeared (a navigation waits as a failed reference).
Fix:
- navigateTo leaves the auto-import list, so Vue Router reads it.
- A new optional hook, FrameworkResolver.ownsReference: a reference a
framework answers alone stops at the frameworks' answer, and no import
or name strategy guesses past it. nuxtResolver owns a bare navigateTo
that no declaration or import in the file binds. One whose destination
is computed, names no page or is an external URL stays unresolved,
rather than linked to a project function that only shares the name
(elk declares a navigateTo of its own inside a plugin). A file that
declares or imports its own navigateTo calls that one, as before.
- Vue Router's call reader keeps only destinations that start with "/",
as its markup reader does: navigateTo(url, { external: true }) leaves
the app, and the shared reader had matched an absolute URL's path.
- framework-coverage.md: the Vue Router / Nuxt row and a new trap 21.
Validation:
- nuxt-navigate-to.test.ts (6 tests) and a Nuxt case in
sync-navigation-retry.test.ts (the page appears later; sync equals a
fresh index). All 7 fail on main. Without the ownership check the
unresolved case fails (a composable's call reaches a namesake); without
the "/" rule the external URL navigates to "/".
- A/B against main
|
||
|
|
9ee6718ad1 |
fix(vue): a <script lang="tsx"> block is read with the TSX grammar (#2824)
A Vue component's <script lang="tsx"> went to the JavaScript grammar:
the Vue extractor knew only lang="ts" and "typescript". That grammar
stops at the first type. A script holding only
withDefaults(defineProps<{ title?: string }>(), ...) was reported as
"parse produced no symbols", and a script with any other code failed the
same way without a warning, because the warning fires only when nothing
survives: its interfaces and type aliases were missing, a typed arrow
function was indexed as a constant, error recovery made nodes named after
a type or a keyword (a constant "CSSProperties", a function "if"), and
calls were credited to the component instead of the function they are in.
Fix:
- sfcScriptLanguage (sfc-script.ts) reads a block's grammar from its
attributes: ts or typescript is TypeScript, a Vue tsx block is TSX, and
anything else JavaScript, whose grammar reads lang="jsx" already. The
Vue and Svelte extractors and the viewer's syntax regions all ask it, so
the viewer highlights a component with the grammar it was indexed with.
- A Vue project preloads the TSX grammar (expandGrammarLanguages). It came
along only with a .tsx or .js file in the project; without it a
lang="tsx" file is never parsed, and nothing of it is stored.
Validation:
- vue-tsx-script.test.ts indexes a project of .vue files only. On main 4
of its 5 tests fail; with only the preload reverted 3 fail (two because
the files are not stored at all), with only the extractor reverted 2,
with only the viewer reverted 1. 19 related test files, 879 tests, pass.
- A/B against main
|
||
|
|
013b91c1be |
fix(js,ts): a named class expression is a class with its own members (#2825)
A named class expression, like register(class Widget extends Base {})
or module.exports = class Router {}, was not indexed as a class, as
issue #2499 found. Its methods became free functions, its fields and
supertypes were dropped, and calls in it had no class to start from,
so its this-calls, super calls and own static calls went unresolved or
to a by-name guess, and calls elsewhere guessed into its methods.
Both engines now index a named class expression as a class, with its
methods, properties, extends and implements. An anonymous one is still
passed over. Its head (class Widget extends Base) is its signature,
which marks it as an expression, and it is never exported by itself.
JavaScript binds the name only inside the expression, and the resolver
now scopes it the same way. A reference written outside every class
expression resolves in a view of the graph that hides class
expressions and their members, so new Widget() elsewhere links
nothing, and a same-named class, function or binding keeps its links.
A name only class expressions carry is unknown there. A reference
inside one sees the whole graph, unless it names another class
expression. A class a CommonJS module exports, module.exports = class
Router {} or exports.Store = class Store {}, is the module's value,
which require hands to every importer, so it is not scoped: it reads
as a declaration does, and a require of it reaches its static methods
and its instances' methods. The reader of CommonJS named exports takes
exports.X = class Y {} too. Super calls in a class expression now
resolve through the super-call matcher.
Validation: the new test fails 11 of 13 on main
|
||
|
|
23a80a847a |
fix(python): a from-import of a module with no dot in its name links the module's file (#2823)
In Python, `from loader import X` with loader.py at a package root left the module reference `loader` unresolved, so the importing file had no imports edge to loader.py. `import loader` and `from pkg.loader import X` both link their module's file. The from-import's names (X) linked, but `codegraph affected` and impact could miss the file's dependency on the module. v1.6.2 behaves the same. Root cause: resolvePythonAbsoluteModule resolves only dotted module refs. It leaves bare names to resolveModuleImportToFile, because a bare imported name like `authentication` in `from app.api.routes import authentication` is ambiguous on its own. But resolveModuleImportToFile matches an import by its local name, which for `from loader import X` is X, never `loader`. So the module of an undotted from-import was never looked up by its path. When the file also bound the module's name some other way, the generic import loop resolved it through that binding: next to `from django.core import mail`, the module of `from mail import ...` became django/core/mail/__init__.py. Django's tests/mail/__init__.py is the module meant, and the statement's own names already resolve there. Fix: resolvePythonAbsoluteModule also takes the module a from-import names (isPythonFromImportModule), dotted or not. It resolves it from the package roots, as `import loader` is resolved. Imported names keep their path, and a module from outside the project still links nothing. Validation: - New __tests__/python-from-import-module-file.test.ts, 7 tests: a root-level module, a top-level package, a src layout, a module named on a continuation line, a submodule imported by name, sync after the module file is added (matching a fresh index), and an outside module with a same-named nested file (links nothing). With main's import-resolver.ts the 6 positive tests fail and the guard passes. Seven assertions in python-module-resolution.test.ts listed a file's import edges exactly while the module edge was missing; they now include the module's file. What they test is unchanged: where the imported name binds, and no cycle. - A/B against main |
||
|
|
628f69e275 |
fix(kotlin): repository().pending() links the method of the type repository() declares (#2822)
Issue #2805: in a Kotlin test class with `private fun repository():
RealRepo`, `repository().pending(1)` went to a test double's
`FakeRepo.pending` in another package, and `val repo = repository();
repo.pending(1)` to the interface `Repo.pending`. The extractor records a
call on an unqualified call's result by the outer method's bare name (a
capitalized inner, `Foo().bar()`, is kept as a chain), so the name matcher
took whichever `pending` came first; and the local's type was read only
from a constructor or an annotation, so the receiver's name guessed it.
The name matcher now reads such a call where it is written
(`repository().pending(1)`, `this.repository()…`, `nullable()?.pending()`)
and finds the inner function the way Kotlin does: a method of the calling
class, of a class it inherits from or one around it, read from their
declarations, else a top-level function the file can name. The call is
resolved on the type that function's signature declares, including what
the type inherits and extensions on it, or links nothing when the type
lacks the method: the #750 rule, as for `Foo.factory().bar()`. A local set
from such a call alone (`val repo = repository()`) takes that type for
the existing typed-local path. Edge cases the A/B surfaced:
- a nested type is the one the function names, `Request.Builder` or a
plain `Builder` written in `Request`, matched by whole names
(`MockResponse.Builder` is no `Response.Builder`);
- an extension is the one the calling file can name (Exposed declares
`Expression<T>.year()` in three modules);
- a type from outside the project, a call inside an extension on one,
and an extension the index keeps unattached (on a nullable receiver, or
`fun Dns.Call.x()` filed under `Dns`) keep the name match as before.
Validation:
- New kotlin-call-result-receiver test (9 cases): 7 fail on main, the two
that guard unchanged behavior pass on both; all pass with the fix on the
kernel and on the wasm path. Mutation checks confirm the owner-path,
scoped-nested-type, whole-segment and extension-visibility rules are
each exercised.
- A/B on main
|
||
|
|
d5fc1fbb21 |
fix(python): calls like reg.get() on a typed local or parameter reach the class's method (#2821)
In Python, a call of a method named like a builtin dict, list, string or file method (get, update, items, pop, close and the rest of the builtin-method list) was dropped by the builtin filter before any strategy could read its receiver. Only a receiver whose capitalized name is a project class, or an imported project module, got past it. So `reg = SystemPackRegistry(); reg.get(k)`, `registry = load_registry()` with `def load_registry() -> SystemPackRegistry`, and a parameter `reg: SystemPackRegistry` gave no edge, while `self.reg.get()` linked through the attribute reader of PR #1926. A call the filter drops now gets one way back, by evidence only: matchPythonTypedLocalCall. It reads the def that binds the receiver where the call reads it (the calling def, or one it is nested in). It types the name only when every binding there gives it the same project class: - a parameter gives what its annotation names ("T", Optional[T], T | None); - an assignment gives its own annotation, or what its whole value makes: a constructor, or a factory typed by its return annotation or by its returns (the attribute reader's pythonValueEvidence and pythonFactoryType); - `x = None` gives nothing either way; - each class is the one the file's imports name (pythonClassNamed), so a class imported from outside the project is none of the project's; - the method must be the class's own or a declared base's. Everything else stays dropped: any other binding of the name (a loop, with, except or comprehension target, a tuple or augmented assignment, a walrus, an import, del, a nested def or class of the name, a nonlocal write from a nested def), a value that names no class ({}, json.load(p), Foo().data(), a union), a rebound callee, a module-level name. These are the shapes the reporter's follow-up found making wrong edges with a looser reader. Like an attribute call, the edge records the other files its answer read (typeFrom), so a sync that edits the factory or a base re-opens it. Validation: - New __tests__/python-typed-builtin-method-calls.test.ts, 20 tests: - the issue's three calls; annotated, quoted and Optional parameters; items, pop and close; a module-qualified constructor; typed, untyped and awaited factories; a None placeholder; a local annotation; a closure; typeFrom; two same-named classes; an inherited method; - the reporter's trap shapes, which stay unlinked; - three sync cases, each matching a fresh index. With main's source the 13 positive tests fail and the 7 trap tests pass. - A/B against main |
||
|
|
e32021f6fb |
fix(java,kotlin,csharp,scala): an overload links only the overrides with its parameters (#2820)
The override pass (interface-impl) grouped a class's methods by name and
linked every overload of an interface or base-class method to every
same-named method of the class. Issue #2663's ExampleService got four edges
for two overloads: putTairValue(String) also reached
putTairValue(List<String>, double), and the other way round, so flows,
callers and impact walked into overloads a call never runs. Java, Kotlin,
C# and Scala make an override repeat the parameter list of what it
overrides, and the stored Java and Kotlin signatures already told the
overloads apart; nothing compared them.
A new override-parameters module picks, of a class's same-named methods,
the ones that can override a base overload. The pass asks it only when a
name is overloaded on either side, so a name declared once on each side
links as before. A pair with another parameter count never links. Of those
with the base's count, the ones whose parameter types are written alike
win: qualifiers, annotations, final, default values and C#'s BCL names for
its keywords aside, varargs as arrays. When none is, the ones whose types
can't be told apart are kept: a type parameter of the base method or of a
type around it matches any type in its place (save(T) implemented as
save(String), List<T> as List<String>), a raw or nullable spelling of the
same type may be it, and a method written in another language than the
base is compared by count alone. An override's own type parameter matches
no concrete type of the base, as these languages rule. Java, Kotlin and
Scala lists come from the stored signatures; C# stores none, so its list
is read from the declaration past attributes, comments and string
literals, as the resolver's call-site overload choice already reads one.
Extraction is unchanged, so both engines give the same input.
Validation:
- New override-overload-pairing test (12 cases, Java, Kotlin, C#, Scala):
11 fail on main, the single-method case passes on both; all pass with
the fix on the kernel and on the wasm path.
- A/B on main
|
||
|
|
3f58e34383 |
fix(cli): index no longer blocks on a daemon lock naming a process that isn't CodeGraph (#2819)
codegraph index told you to run `codegraph daemon stop`, which the CLI
rejects ("too many arguments"), whenever it could not verify the process
named in .codegraph/daemon.pid. In that state `codegraph daemon` lists
nothing, because it lists only daemons that answer their socket. And
`codegraph unlock` kept an old-format lock whose process was alive (the
rule from issue #1834), so nothing the CLI offered got past it. The
writer-lock message gave the same wrong advice.
Root cause: a live pid in the lock was the only evidence, and a pid
proves nothing once the OS gives it to another program. Every CodeGraph
daemon since the first one (v0.9.5) runs
`<runtime> ... codegraph.js serve --mcp --path <root>`, so the holder's
command line settles the question.
Fix:
- New src/mcp/process-identity.ts reads a process's command line and
executable name from the OS: Win32_Process through PowerShell (tasklist
as fallback) on Windows, /proc on Linux, ps elsewhere. Each lookup is
capped at 5 s and never throws. A holder is a CodeGraph MCP server,
provably something else, or unknown, and unknown is never evidence.
- stopDaemonAt treats an unverified live holder that is provably not a
CodeGraph server like a dead one. Its lock is cleared, under the same
writer-slot claim, and the process is never signalled.
clearStaleDaemonArtifacts (codegraph unlock) does the same for an
old-format lock. A holder that may be a daemon keeps its lock, as
before (issues #1553 and #1834).
- daemonStopFailureMessage builds index's error from what remains: the
pid, what it runs, and a command that clears it (taskkill /PID N /F or
kill N), or which lock file to delete when only that will do.
- writerLockHeldMessage names `codegraph daemon` instead of the missing
`codegraph daemon stop`.
Validation (Windows 11):
- New daemon-lock-holder.test.ts (11 tests) and a daemon-registry test.
With main's daemon-registry.ts, writer-lock.ts and codegraph.ts put
back, 11 tests fail, including all 8 CLI cases and the writer-lock
wording; with the fix all pass. Four existing tests used generic
processes to stand in for a daemon that doesn't answer. Those stand-ins
now carry a CodeGraph server's command line, and pass on main and here.
- Real CLI with a private HOME, 4 cases: an old-format lock naming a live
PING.EXE; an old-format lock naming a live Node process that isn't
CodeGraph; a current-format lock naming that kind of Node process with
a pipe nobody listens on; an old-format lock naming a Node process whose
command line reads `codegraph.js serve --mcp`. On main all 4 index runs
exit 1 with the old advice, `daemon stop` is rejected, `daemon` lists
nothing, and unlock clears only the third. With the fix the first 3
rebuild (exit 0), each holder stays alive, and unlock clears the first
too. The 4th exits 1, naming the pid and `taskkill /PID <pid> /F`;
after that command, index exits 0.
Contributor PR #2373 (@danusha2345) changes the two strings only; I read
it for guidance.
Fixes #2770
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
b8c3c032be |
fix(bundle): bundle Node 24.22.1, the October security release, and verify the download (#2817)
Release bundles were set to ship Node 24.21.0. The release workflow runs build-bundle.sh with no version, so the script's default decides the Node every bundle carries, and that default predates Node's October 2026 security release. v24.22.1 (2026-10-20) fixes 8 CVEs, 4 rated High (TLS, DNS, Brotli), and ships OpenSSL 3.5.9. It keeps V8 13.6.233.17, libuv 1.52.1 and module ABI 137, and it carries 24.20.0's fix for the Windows abort at exit (issue #2617). The script also never checked the Node download against the SHA-256 sums nodejs.org publishes. Fix: - The default and the usage example move to v24.22.1. The comment and a new test hold the default at 24.22.1 or later, on 24.x. - Step 1 downloads the release's SHASUMS256.txt and refuses the archive, before unpacking anything, unless its SHA-256 matches the listed one. The zip and tar.gz downloads now share that one code path (sha256sum, or shasum on macOS). - The unreleased CHANGELOG bullet for issue #2617 now names 24.22.1 and the check. BUNDLING.md describes the check. Validation (Windows 11 x64): - bundle-launcher.test.ts: 4 new tests (the version floor; a matching download is unpacked; a mismatched or unlisted one is refused before unpacking). With main's build-bundle.sh all 4 fail; with the fix the file has 7 passed and 3 POSIX-only skipped. - build-bundle.sh win32-x64 with the new default: the check passed (sha256 d2046d28...8b95fa5, as listed). The bundled node.exe is byte-identical to the official v24.22.1 one and prints v24.22.1 (OpenSSL 3.5.9). Its launchers from Git Bash, cmd and PowerShell, with no system Node on PATH, ran --version, init, status, query, callers and sync on a small TypeScript + Python fixture: all exit 0. - Node's own reproducer for the exit abort, 300 runs per mode, 4 at a time: the bundled v24.22.1 exited cleanly 300/300 in all three modes. The official v24.16.0 control aborted 299/300 with 0xC0000409 when the child ran directly. - 13 runtime-facing test files (sqlite, wasm flags, worker and stdin teardown, watcher, launcher, shim) run on the bundled node.exe: 136 passed, 22 skipped, 0 failed. Fixes #2780 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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
|