mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 05:59:11 +08:00
c30696703dcc01814d2531e24481fcbdd33572f7
1703
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c30696703d |
fix(cfnptr): a C function-pointer table links only to C and C++ functions
resolveFn in the C function-pointer pass looked a registered handler's
name up across every language, filtered only to function and method
kinds. In a repository that also holds Rust or Python, a C table could
link to a same-named Rust or Python function, and when that function
sorted first it also took the place of the real C handler.
The fix keeps candidates in C and C++ files only, using the pass's own
C_CPP_EXT, the file set it scans for tables and dispatch. Headers the
language detector labels Objective-C still count. Objective-C `.m` and
`.mm` files stay out, like they are out of the scan: a table in a `.m`
file is never read, and allowing them would let an Objective-C method
named `init` or `read` stand in for a C handler of that name.
On GLFW, which mixes C and Objective-C, main links 354 table entries,
none into a .m file; its one C table naming a .m function
(_glfwConnectCocoa) is an anonymous-struct table the pass doesn't
bridge on main either.
Validation, main
|
||
|
|
1cb4aab913 |
fix(status): a project below its repository root sees its uncommitted changes (#2932)
When the indexed project is a folder of its git repository (a monorepo package, an app/ folder beside a server/ one), `codegraph status`, `status --json` and the freshness check behind codegraph_status reported the index up to date while `codegraph sync` found the very same changes. `git status --porcelain` prints paths from the repository root whatever folder it runs in, and reports the whole repository. Change detection joined `app/src/index.js` under the project, found nothing at `app/app/src/index.js`, and dropped it. The dirty paths a sync records for the restore check (from PR #1878) were stored the same way, so a file put back with git after a sync was missed too. And a project in a folder its repository gitignores got a permanently clean answer from git, which says nothing about an ignored folder. Change detection now asks where the project sits in its repository (`git rev-parse --show-prefix`, kept per folder, and skipped when the folder has its own .git). Below the root it runs git status with a `-- .` pathspec, takes the prefix off every path and leaves out anything outside the folder. The project's ignore rules match those project-relative paths, the recorded dirty paths are project-relative, and a project its repository ignores declines the git path as the scan already did, so callers take the full scan. A project at its repository root runs exactly the git command it ran before. Validation: - git-status-subdir-project.test.ts (new, 7 cases): edited, added and deleted files in a subdir project, matching sync; edits outside the project left out, with the pathspec asserted; the restore case, across a reopen; the project's own anchored .gitignore rule; an embedded repo inside the project; the gitignored-project fallback; and a repo-root project running exactly today's status command with no prefix call. 6 fail on main's source; the repo-root control passes on both. Four mutations (no pathspec, ignore rules on repo-relative paths, no fallback, no .git shortcut) each fail the test. - CLI repros: after an edit and an added file in app/, status --json reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in a gitignored project; a subdir restore reads 0 on main and 1 modified with the fix (a repo-root project reads 1 on both). - The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added + 1 modified with the fix. Reported in PR #1636 by @innoxxicide. This reimplements the fix independently, on top of the restore check that landed after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0174f804c3 |
perf(resolution): read a bare JS/TS call's local binding once per match — graph byte-identical
matchByExactName asked isLocallyBoundJsName for every cross-file candidate of a bare JavaScript or TypeScript call, though the answer depends only on the reference's name and file, which are the same for every candidate. The helper memoizes it per file and name, but each call still built the key and read the memo. The answer is now kept in a lazy local for the one match, at the same place in the filter, so candidates, order, scoring and confidence are untouched. Validation: fresh indexes of codegraph's own source, excalidraw, vben, express and zod dump byte-identical to main's (nodes, edges with their metadata, unresolved refs, files), and express stays identical through sync after a JS file is added, edited and deleted. A CPU profile of indexing codegraph's own source (all threads, two runs per side) puts isLocallyBoundJsName's self time at 150-173 ms on main and 45-46 ms here; the scans it guards (bindsAtSites) are unchanged. Whole-index CPU on vben, 10 interleaved runs, moves less than its noise. The PR's 8 tests pin the matcher's behaviour and pass here and on main. Original PR #2871 by @luojiyin1987. Fixes #2870 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2afee00cb0 |
fix(viewer): a file with % in its name opens as itself (#2933)
codegraph ui couldn't show a file whose name contains a %: 100%.ts was refused as an unusable path, and a%20b.ts was served as changed on disk with no lines. resolveProjectFile, the chokepoint for every file the viewer reads, percent-decoded its path. But that path is either one the index stored or a query value the URL parser had already decoded once, so this was a second decode. It threw on 100%.ts and turned a%20b.ts into a b.ts, while resolveRequestedFile looked the file up in the index by the name as given: the bytes of a b.ts were hashed against a%20b.ts's record. The chokepoint no longer decodes. It refuses raw control bytes, and on Windows a backslash, then checks containment as before. Raw request paths (the viewer's own assets) are still decoded where they are read. The File view's /api/file/<path>, whose path the HTTP layer also decodes once, had the same double decode and is fixed by the same change. The POSIX half of PR #2009 (backslashes kept in names on POSIX) is left out, as it needs a wider change and a POSIX run. Validation on Windows 11, with a fixture holding src/a%20b.ts, src/a b.ts and src/100%.ts: - codegraph ui over HTTP, /api/source with the query a browser sends: main serves a%20b.ts as drift=true with no lines and answers 403 for 100%.ts; the fix serves all three files as themselves, drift=false. - Tests: 2 new tests (the chokepoint, and /api/source plus /api/file/<path> over HTTP) fail on main's source and pass with the fix. Two existing cases that fed an encoded ..%2f and %00 straight to the chokepoint now go through the URL parser's single decode, as requests do, and are still refused. Reported in PR #2009 by @shrankr; this reimplements the viewer half of the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
983c4df374 |
fix(index): a deleted file git still lists is skipped, not reported as unreadable (#2928)
A full index reads every path the scan lists, and `git ls-files` keeps listing a tracked file deleted from the working tree until the deletion is staged. So `codegraph init` and `codegraph index` stat'ed the missing file, got ENOENT and reported "1 files could not be read" with an errors.log entry. The errored file also kept the run from recording the commit it indexed, so `codegraph status` fell back to a full scan afterwards. The batch reader in indexAll and the single-file indexFile now take ENOENT and ENOTDIR to mean nothing is there: the file is skipped and counted as skipped, so the run reads as complete rather than PARTIAL. A symlink that points at nothing is skipped the same way, as the directory walk for projects without git already did. Every other read failure (EACCES, EIO, a folder where a file was) is still reported. On POSIX a path under a file fails realpath with ENOTDIR, which validatePathWithinRoot rejected as if it escaped the root; it is now judged like any missing leaf, by its nearest existing ancestor, so it reaches the reader and is skipped too. Discovery is unchanged, with no extra stat per tracked file. Validation: - index-gone-tracked-files.test.ts (new): all 5 cases fail on main's source and pass with the fix: committed and staged deletions, a folder replaced by a file, a dangling symlink, and EACCES still reported by both the full index and indexFiles. Mutating the single-file path, the skipped tally or the EACCES boundary fails the test each time. - security.test.ts: a path under an existing file validates, and one under a symlink to a file outside the root is still refused. - The issue's CLI repro: main prints "1 files could not be read" and writes errors.log; the fix prints "Indexed 1 files", writes no errors.log, and status reports nothing pending. Reported in PR #2910 by @xianzhi-xzw, who also filed issue #2904. This reimplements the fix independently, at read time rather than with an lstat of every tracked file during discovery. Fixes #2904 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f93e89a82d |
fix(paths): a project at a Windows drive root indexes its files (#2931)
A project at the root of a drive, such as a mapped network drive Z:\, a
subst drive or a UNC share root, indexed nothing on Windows: every file
was refused as "Path traversal blocked", and code reads under the root
were refused too. The containment check appended a separator to the root
before comparing, and a root such as C:\ already ends with one, so the
prefix became C:\\ and no path under the root matched. The dead code
report's default file reader had its own copy of the same check, so it
treated every file of such a project as unreadable.
The check now adds a separator only when the root doesn't already end
with one. The dead code reader uses the shared lexical containment check
instead of its own copy.
Validation on Windows 11:
- subst R: onto a two-file fixture, then codegraph init -y --force R:\:
main indexes 0 files ("all 2 files had errors", 2 "Path traversal
blocked"), and validatePathWithinRoot(R:\, a.ts) is null. The fix
indexes both files (6 nodes, 6 edges) and getCode returns both
functions' source. The mapping was removed after each run.
- Dead code report against one R:\ index: main's reader drops the unused
function as unreadable; the fix reports it.
- Tests: 3 new tests fail on main's source and pass with the fix: a file
under the filesystem root (this drive's root here, / on POSIX), path
math under Q:\ and \\server\share\, and read-only realpaths under C:\.
Reported in PR #415 by @Kamilslaoui93; this reimplements the fix
independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0187366697 |
fix(cli): codegraph status opens the index read-only, and says when a rebuild is running
Status opened the database read-write, so its open ran the writer's
repairs: migrations, the FTS-trigger and secondary-index recovery an
interrupted bulk load leaves behind, and WAL maintenance. During a
`codegraph index`, that recovery recreated the indexes the indexer had
dropped for its bulk load, and the indexer's next write, whose
transaction had already read, failed with SQLITE_BUSY_SNAPSHOT (517):
"Failed to index: database is locked".
Status now opens with `readOnly: true` and leaves every repair to the
writer, as the MCP reader connections already do. When another live
process holds `rebuild.pid`, it says so: a warning in the text output and
`rebuildInProgress` in `--json`, the only new key. `isRebuildInProgress`
is the check `assertNoRebuild` already made.
Added when landing it:
- A read-only open migrates nothing, so status on an index built by
1.4.0 or older (schema 7, no `unresolved_refs.status`) failed with "no
such column: status". The pending-reference count counts every row
when the column is missing, as the v8 migration marks them pending.
- While a rebuild runs, status skips the advice meant for an index at
rest: the interrupted-run references, "run sync", "re-index", and the
oversized-WAL warning, which mid-rebuild all describe the live run.
- Status no longer reclaims a killed session's oversized WAL, so the
warning names the next `codegraph sync` instead of status itself.
- Tests: the issue's exact interleaving (status inside the writer's edge
transaction, as in PR #2916's test), a pre-v8 index, and the
mid-rebuild advice.
Validation (Windows 11, Node 22.22):
- The issue's reproduction with our build of main
|
||
|
|
5e0c42cb63 |
fix(security): the sensitive-folder guard refuses links, case variants and \?\ paths (#2929)
The guard that keeps ~/.ssh, ~/.gnupg, ~/.aws, ~/.config and system folders such as /etc and C:\Windows from being opened as a project compared the path as typed. A junction or symlink to one of them, a different capitalization on Windows or macOS (~/.SSH), or a \\?\ or \\.\ spelling on Windows passed, so an MCP projectPath, `codegraph ui` and the viewer's per-file reads all served what was inside. The MCP gate also skipped the guard for a path that doesn't exist and then walked up from it, so ~/.config/no-such-dir reached an index of ~/.config. validateProjectPath now checks the path as spelled (with a Win32 namespace prefix stripped) and its fs.realpathSync.native form. Both are compared with the system set and the system folders' own real paths (macOS /private/etc), and with the home folders as spelled, under the home's real path and by their own real paths (a linked ~/.ssh). Case is folded on win32 and darwin. A path that can't be resolved keeps the lexical check. It stays at one realpath per call: the system folders' real paths are read once and the home folders' are cached for two seconds. The MCP handler also checks the root its up-walk lands on. Validation on Windows 11, with a private home holding a fake token: - Probe: on main 11 of 16 sensitive spellings were allowed, and the token was served through 6 MCP projectPath shapes and 2 viewer roots. With the fix all are refused. Controls (an ordinary folder, its upper-case and \\?\ spellings, the home itself) are still allowed. - codegraph ui end to end: main served the token for 4 of 6 spellings and started the viewer for a fifth; the fix refuses all 6 at startup. - Tests: the 9 new tests that run on Windows fail on main's source and pass with the fix. The POSIX and macOS cases are gated and were not run here. - Cost: validateProjectPath 48 -> 130 us per call, the viewer's per-file resolveProjectFile 700 -> 780 us (medians of 5 rounds, 3 runs). Reported in PR #1930 by @mixxer; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7ff2841e37 |
fix(resolution): ties that decide a link break by code unit, not the host's locale
Several resolution tie-breaks sorted paths or ids with a bare localeCompare, which follows the host's locale, so two machines could index the same repository differently: under Czech collation `ch/` sorts after `h/`, under Danish `aa` sorts after `z`, and locale collation puts `alpha/` before `Beta/` where code-unit order does the opposite. The PR fixes the nearest-declaration tie in tsFieldCall, for `this.field.method()` on a type declared in two equally near apps. On top, the same bug at the three other sites that decide resolution: - cpp-type-aliases: the declaration an alias resolves to when two headers declare it differently, for a C++ base class named through it; - frameworks/react-native: the Swift copy of a module's class that a JS-to-native call reaches; - go-func-field-synthesizer: the last tie-break of wiringOrder, which decides the values a capped channel keeps (its site comparison already names 'en' and stays). All compare by code unit now. Comparators that only order output (CLI, MCP and viewer listings, dead-code and hierarchy order, the context builder's copybook entries) are left alone. The CHANGELOG bullet moves from the released 1.6.1 section to Unreleased. Validation: a test each for the TS, C++ and React Native sites; all 3 fail on main and pass here. Indexing `ch/` vs `h/` fixtures for those three under a simulated English and a simulated Czech host (a localeCompare preload that reaches the worker threads), main links different targets in all three, this commit the same in both. A/B, main vs this commit on an English-locale host, 12 repos (excalidraw, zod, vben, bulletproof-react, jq, cobra, gin, etcd, prometheus, okhttp, with flask and spring-modulith as controls): every dump byte-identical. Indexed again under a simulated Danish host (the Go repos) and Czech host (the TS repos), both arms match their English dumps, so these repos reach no tie the locale decides; the fix matters where one does. Original PR #1983 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4eb8c2a5f2 |
perf(synthesis): line numbers from a newline index, not a split per match
Two quadratic idioms in callback-synthesizer.ts. Twelve passes computed
each match's line as `safe.slice(0, m.index).split('\n').length`, which
copies and splits the file's prefix for every match, and `sliceLines`
split the whole file again for every node a pass looked at (7 callers,
one per node of the same file in a row).
The twelve sites now call `lineOf(src, idx)`, which keeps the newline
index of the last text (built with synth-utils' `makeLineAt`, which
counts the newlines strictly before `idx`, exactly like the old idiom),
and `sliceLines` keeps the last file's lines.
Ours on top: `releaseLineCaches()` at the end of synthesizeCallbackEdges.
The pool workers' copies go with the workers (pool.destroy() after
synthesis), but a sync runs synthesis on the main thread, so a long-lived
MCP server or `watch` kept the last file's text, its lines and its newline
index until the next synthesis. And a celery test with two files of the
same length scanned in a row, which fails if the line index is reused
across texts (checked by keying the cache on length).
Validation, main
|
||
|
|
0b194448dc |
fix(resolution): a call a method hands on to another object no longer links back to it
matchMethodCall's strategy 3 guesses a method by its name when the
receiver's type is unknown. With one method of that name in the project
it was taken outright, so a wrapper handing its call on, like Play's
`def acceptsURL(url) = proxied.acceptsURL(url)` or vben's
`form.validate()` inside `FormApi.validate()`, linked to the method
making the call, which then looked recursive in callers and impact.
The single-candidate branch now gives no edge when that method is the
caller itself and the receiver is another object (not this, self, Self,
super or cls), as the word-overlap branch already did.
On top of the PR: Go is left out. A Go method's receiver and struct
fields are typed before this point, so a Go call that still reaches the
guess is on a local the resolver could not type, mostly a range variable
over the type's own children, which is real recursion: cobra's
`sub.checkCommandGroups()` over `c.commands`, etcd's `child.Repr(...)`,
prometheus's `subParameter.toLabels()`. Also a TypeScript wrapper test, a
Go range-variable recursion test, and a CHANGELOG bullet.
Validation, main (
|
||
|
|
b6379e6d5e |
fix(scala): an Outer.Inner(...) call reaches the Scala case class or object it builds
Scala qualified names carry no package (Outer::Inner), so a call written Outer.Inner(args), a case-class or companion apply, found no type by its written name. It fell through to the name strategies, and under a return type annotation (def f(): WebSocket = WebSocket.Accepted(flow)) the local-declaration reader took WebSocket for the receiver's type and handed over an unrelated Java constructor, Other::Accepted::Accepted. Before the receiver-word strategy, a Scala call whose member is capitalized now resolves to the Scala type Outer::Inner when that is unambiguous: every match in one file (a class and its companion), or one in the call's own file; a class or trait wins over a same-named object. It is recorded as instantiates for a class, as a call for an object. The typed-receiver path no longer hands a Scala call another language's constructor. Real Java static methods are still reached from Scala. Validation: - resolution.test.ts: 5 cases, 4 red on main; the fifth (a Scala call to a real Java static) passes on both and guards the Java side. - A/B against main |
||
|
|
81559dcc37 |
fix(resolution): a call reaches the caller's own copy of a duplicated module
A module kept in several copies, like two Android build flavors that each
hold one package, gives every name in it one candidate per copy.
Directory proximity is capped at six shared folders, so in a deep tree
the copies scored the same and the first indexed won: a call in one
flavor could land on the other flavor's copy. And since the name is not
in doubt among copies, only which copy, scoring the right pick by
proximity (0.7, or 0.4 in a shallow tree) understated it.
findBestMatch now breaks a tie among copies of one symbol (same
qualified name, kind and file name) toward the copy whose file shares
the longest directory prefix with the reference, when that copy is
uniquely closest. matchByExactName scores that pick 0.8: above a
proximity guess, below a unique name or the file's own definition (0.9,
which keeps precedence).
On top of the PR: the copies are judged among the candidates
findBestMatch weighed, so a namesake in another language no longer
blocks the 0.8; a match of the caller itself keeps the proximity score,
as it does for a same-file match; and Kotlin and Java flavor tests, since
the PR's Python fixture calls a function it never imports.
Validation, main (
|
||
|
|
69af19ace2 |
fix(build): building from source with bun finishes instead of starting itself over
build:ui and build:lib ran npm run build --workspace ui. Under bun, a package script's npm run becomes bun run, and bun ignores --workspace, so build:ui ran the ROOT build again, which ran build:ui again. bun run build:ui on main nests without end: with bun 1.3.8 here, 13 bun processes deep after 62 s, when a watchdog killed the tree. npm is unaffected, which is why it went unnoticed. The scripts now name the workspace by directory: cd ui && npm run build (and cd ui && npm run build:lib), which runs the same ui script under either tool. Validation: with the fix, bun run build, build:ui and build:lib finish by themselves in 16 s, 6 s and 6 s, and check-ui-build and check-ui-package pass. npm run build plus npm run build:lib give the same output before and after, under npm's default script shell (cmd.exe here) and under bash (npm --script-shell bash): dist/ (1,260 files) and ui/dist (171) are identical by SHA-256 in all four runs, built in one checkout from clean outputs, and bun's build gives that same tree. check-ui-build and check-ui-package pass in every run. scripts/pack-npm.sh still runs npm run build:lib --workspace ui; that runs only under npm, in the release workflow, so it is left as is. Original PR #1764 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4c572e07b6 |
fix(play): a Class.method reference reaches a member of the class it names
The Play resolver claims every Class.method reference in a Play project, in
routes files and in ordinary Java and Scala code alike, and took the first
method of that name anywhere in the class's file. In a file holding several
classes (Java nested classes, a Scala class with its companion and
siblings) that was usually another class's method: MediaType.parse(...)
went to Cookie.parse, a route controllers.Application.index to a nested
Helper.index, and Scala RequestAuthority.parse(...) to AuthorityPort.parse.
A method now counts only when its qualified name is the named type's own,
cls::method. A Scala object is an owner too, so a companion's own members
are found through it, and for a call in code so are a Java interface's and
enum's static methods. For a call written in code, the types of the
caller's language are tried first: Play declares RequestAuthority both as a
Scala case class and as a Java record, and each language's callers mean
their own; the other language's types still follow, since Play's Java API
calls its Scala classes. Among same-named types the caller's own file comes
first (Scala names carry no package), an overloaded member gives the
overload the call's arguments fit, and a Java file's import of the name
from outside the project leaves the call to the generic resolver. A route
keeps the plain order and takes a class or object. An inherited member is
left to the generic resolver, which finds it through declared supertypes.
Validation:
- frameworks-integration.test.ts: the PR's case (nested Http.Cookie /
MediaType, Scala sibling objects, a route beside a nested Helper.index),
red on main; ours, Scala and Java callers of a RequestAuthority declared
in both languages (red on main and with the PR's own resolver) and an
inherited route handler; and a guard for overloads, same-named objects
in two packages and a Java interface's static method.
- A/B against main
|
||
|
|
cde4cd74eb |
fix(resolution): a lone same-file definition scores as a strong match
matchByExactName scored a winner among several same-named candidates by directory proximity alone: 0.7 when the files share two folders, else 0.4. findBestMatch already ranks the definition in the reference's own file first, so the target was right, but a shallow file's own function read as an uncertain guess (below 0.6), and because the resolver keeps the most confident strategy, the same-file definition lost to a framework's naming guess in another file (ASP.NET's PascalCase and Service rules at 0.7 and 0.85). isDecisiveSameFileMatch now scores it 0.9, as a lone candidate, when it is the only candidate in the file, is not the caller itself, and, as a member of a type, module or namespace, belongs to one the caller is in. The PR checked that last rule for methods only. We extended it to every member kind whose owner the file declares, other than a function, so a sibling enum's entry or a sibling class's field is not raised. Validation, main vs this commit, 21 repos in 11 languages (flask, click, fastapi, gin, cobra, etcd, ripgrep, jq, Slim, serilog, eShopOnWeb, nowinandroid, okhttp, http4s, playframework, spring-modulith, express, zod, bulletproof-react, excalidraw, vben): - no edge added or removed; - 9 targets moved, all in eShopOnWeb and all right: C# properties and fields named after their type that the code reads (Color Color), which ASP.NET's naming rule had linked to the class in another project; - 6,927 edges keep their target and gain confidence, 535 of them from 0.4; in 874 a framework rule's 0.7-0.85 now loses to the same target by exact name; 30 end in another file, Go type positions the type gate moves to the package's own type; - as written, the PR also raised 77 edges onto another type's member to 0.9, about 48 of them wrong (okhttp's InterceptorOverridesTest enum entries, playframework's sibling providers' `get`); they keep main's confidence. Index CPU, median of interleaved runs: okhttp 32.3 vs 32.4 s, etcd 33.4 vs 33.1 s. Tests: 3 of 7 fail on main; the PR as written fails the sibling-member case. js-class-expressions pinned the old 0.4 for `this.run()` reaching the class's own `run` in a shallow file; it now expects 0.9. Original PR #1938 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0b238452a3 |
fix(mcp): explore's maxFiles text gives the real default, not 12
codegraph_explore's maxFiles parameter said "(default: 12)" and its
schema carried default: 12, but the handler picks 4, 5 or 8 files by
project size (clamped to 1 to 20), and nothing applies 12. A client that
fills in schema defaults sent maxFiles 12 and overrode the size-based
default. The description now gives the real default and says total
output is also capped in characters by project size, and the schema
default is gone. The query description no longer tells the agent it
needs no prior codegraph_search, a tool not on the default list.
explore-output-budget's small-tier test asserted that the output does
not contain "Complete source code is included above", text the
completeness note no longer has, so it could not fail. On this fixture
the note reads "Complete source for 1 file is included above", which
the PR's replacement ("files is included above") misses too. It now
checks "is included above", and the medium-tier test checks the note is
there, so a rewording can't leave the absence check behind again. With
the small tier's completeness note switched on, main's assertion and
the PR's both still pass, and this one fails. The interface comment
names the note by its current wording.
Left out of PR #1972: its rewrite of the server instructions' "Trust
codegraph's results" line, which is agent steering and needs an A/B
first; and its codegraph_node hint and footer changes, which main
already has (PRs #2783, #2077).
Original PR #1972 by @bompus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
de2b80e921 |
fix(mcp): a project opened for projectPath is released on its own idle deadline
The handler keeps one idle timer, armed for the oldest open project a
trim can release, and it skips a project whose catch-up is still
running. scheduleIdleRelease returned early while a timer was armed.
So when a project's catch-up settled after a newer project had armed
the timer, the trim found the older project not yet idle and kept the
newer one's deadline. The older project stayed open, writer lock
included, until then: up to nearly twice the idle timeout. With the
10-minute default, a project last used at 0:00 whose catch-up settles
at 9:50, while another was used at 8:20, went at 18:20, not 10:00.
Each trim that gets as far as scheduling now replaces the timer,
re-armed for the oldest releasable project. Trims skip scheduling
while calls are in flight, and the last call to finish trims again, so
a settled catch-up still gets its own deadline. There is still one
timer and no polling.
Validation: the new fake-timer test drives that order through
execute(). On main
|
||
|
|
3a93f57ae1 |
fix(java): a record is indexed as a class, with its components' fields and accessors
Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.
The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.
An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.
Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.
Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
main; the sealed-permits and name-only-guess cases are each red without
their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
sweeps at full parity with deferrals unchanged (jextract 218/218,
spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main
|
||
|
|
a565f215f1 |
fix(viewer): saved trails stay inside the project when a folder or trail file is a link
validatePathWithinRoot returned the lexical path whenever the path did not exist yet (ENOENT). With .codegraph/ui a link to a folder outside the project and no trails folder yet, the trails folder passed the check, mkdir followed the link, and the trail was written outside the project. The save still answered 403, but only because the list it sends back re-checked the folder that now existed. The trails list also joined each file name onto the checked folder and read it with statSync and readFileSync, which follow links, so a trail file linking to a trail-shaped file outside the project was listed and served. validatePathWithinRoot now judges a missing path by its nearest existing ancestor, and returns null when the root itself is missing. The list reads every file through the same per-file check as a single read, opens it with O_NOFOLLOW | O_NONBLOCK where the platform has them, and checks the open file with fstat. Windows has neither flag; there the per-file check is what refuses a planted link. A save re-checks the folder after mkdir and writes its temp file under a random name with flag wx and mode 0600. The trail author is cached per project instead of per process, and a trail file that declares an unknown format version is skipped. Added on top of the contributor's change: the link tests also run on Windows (a junction and a directory symlink at .codegraph/ui, a file symlink in the trails folder), each test removes its own links before deleting its folder, the helper gets a junction case and three controls (an in-root link above a missing leaf, allowSymlinkEscape above a missing leaf, a missing root), its doc comment names the new missing-path rules, and the viewer changelog lines are reworded and moved next to the other saved-trails line. Validation: 8 tests fail on main and pass here (the helper below a symlink and below a junction, a missing root, a save below a junction and below a symlink, a linked trail file, the per-project author, the format version). Through the real CLI (codegraph ui), main wrote the trail outside the project through a junction and through a directory symlink, and listed a linked outside file with its note; this branch refused all three and wrote nothing outside. Graph dumps of express, zod, excalidraw, flask and gin are byte-identical to main's, and 55 CLI read commands and 45 MCP tool calls answer the same (codegraph_status differs only by its last-indexed time). Index time is unchanged (gin over 9 interleaved rounds: 2645 ms on main, 2687 ms here). Listing 200 saved trails, the list's cap, costs 108 ms on main and 236 ms here on Windows; 10 trails cost 6 ms and 14 ms. Original PR #1667 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3750921486 |
test(resolution): calls on nested receivers keep their evidence and guess no target
A member call on a nested receiver, like holder?.values.get(readKey()), has failed both ways. Before PR #1790, each such call was linked by its bare method name, to any project method called get. PR #1790 stopped that by dropping every call on a chain rooted at a name, which also dropped the call sites effects are drawn from (issue #1794). PR #1862 brought the dotted chains back as unresolved references; shapes its chain pattern rejects, like holder[0].get(), are still dropped. The test indexes eight receiver shapes in .ts, .tsx, .js and .jsx: optional access, computed and numeric keys, a quoted key, a comment inside the receiver, a non-ASCII name, an imported root and a namespace import. It checks that the readKey() argument call survives, that nothing links to Collision.get or to the imported object, and that the three dotted chains (holder.values.get, imported.values.get and data.holder.values.get) are still in the index, unresolved. That last check is ours. As first written, the test passed on PR 1790's commit |
||
|
|
d02a88d052 |
fix(java,csharp): a call on a new object reaches the class the file imports, not a namesake (#2864)
PR #2854 sends Java's and C#'s no-argument new Foo().bar() calls, which
the chain conformance pass retries, to the constructed-receiver resolver.
That links them (mybatis-3 +265, Newtonsoft.Json, serilog and jellyfin
+36, all right), but the hint for which Foo it passed was a file path,
which the resolver reads for these languages as a dotted package name,
and C#'s using names namespaces, not classes. So of two classes named
Foo the first indexed one won, whatever the file imported: import b.Foo;
new Foo().bar() linked a.Foo::bar at 0.9, and so did a call the language
itself cannot resolve, after import a.*; import b.*;.
When several project classes share the name, Java and C# now pick as
the language does: Java the written package (new a.Foo()), the file's
single-type import, else the caller's own package; C# the one type the
file's namespaces and usings can see. Only that class's own method links,
otherwise nothing. A name only one class has resolves as before,
inherited methods included.
Validation: the new test fails 4 of 4 on main
|
||
|
|
43a4f5b9f9 |
fix(graph): the type hierarchy returns a type's subtypes, not only its ancestors
getTypeHierarchy() walked a type's ancestors and then its descendants with one visited set. The ancestor walk marks the focal node first, so the descendant walk returned at its visited guard before reading a single incoming edge: no type ever had subtypes or implementors, in any language. Each direction now keeps its own visited set. The same walk feeds the hierarchy step of findRelevantContext, which codegraph_explore and codegraph context build on. That step promised subclasses within a budget of a quarter of maxNodes, but checked the budget only between entry points, which was harmless while the walk returned ancestors alone. With descendants coming back, one widely implemented interface brought its whole tree: vscode's IDisposable, in src/vs/base and src/vs/editor, has 613 types in its hierarchy, and codegraph context IDisposable then traded the interface's own dispose for an arbitrary implementor. Added on top of the contributor's fix: the budget now holds per node, and only edges between kept nodes are added. Validation: graph.test.ts gains a test that getTypeHierarchy() returns a base class's subclass and an interface's implementor; it fails on main. context.test.ts gains one that the hierarchy step adds an interface's implementors within its budget (12 nodes, no traversal: 1 to 3); main gives 0, and the traversal fix without the cap gives 10. On vscode's base and editor subtree, codegraph context and codegraph_explore keep main's size and speed (0.8 to 1.3 s), and the hierarchy they show is now the queried type's own: StandaloneCodeEditor and EmbeddedCodeEditorWidget for "ICodeEditor CodeEditorWidget". On prometheus, 3 of 4 explore answers are identical, and the fourth names more of storage.Appender's implementors. The contributor's codegraph implementations command and its test-code helper are not part of this change. This is the getTypeHierarchy() half of issue #2833. Original PR #2839 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a278c2ee12 |
fix(scope): a folder git ignores matches only itself, not every folder with its name (#2858)
The scope matcher that the indexer, sync and the file watcher share also takes in the folders `git ls-files -o -i --exclude-standard --directory` reports as ignored (PR #1754, for issue #1728). They were added to it as gitignore patterns. git reports them relative to the root, but a pattern of one segment, like `src/`, has no anchor in gitignore syntax and matches at any depth. So an ignored root `src/`, holding only a gitignored jib agent, hid every module's `src/` in a Maven monorepo, and the index came up empty. As patterns, folder names were also read as syntax: `[draft]` as a character class, `a[b` as a regex that throws on the first match (the git listing then fell back to a filesystem walk, and the watcher threw on every event), `#tmp` as a comment and `!keep` as a negation. The reported folders now stay root-relative paths in a set, and a path is out of scope when it is one of them or lies inside one. Nothing is parsed, so there is nothing to anchor or escape. Escaping was not an option: in ignore 7.0.5 an escaped `?` never matches a literal `?`, an escaped backslash before a `.` turns the dot into a wildcard, and every match is case-insensitive. git prints these paths relative to its cwd, so for a project in a subfolder of a repo they are relative to that subfolder, the root the matcher is asked about. The CLI hint for gitignored nested repos wrote their paths into codegraph.json `includeIgnored` the same unanchored way, so an opt-in for `child/` also took in any later `archive/child/`. It now writes a root-anchored pattern with `*` and `[` escaped. Validation: - New git-ignored-dirs-scope tests (9): all fail on main and pass with the fix. They cover the issue's layout (scan, index, scoped sync), the lib/, build/ and dist/ variants, a `[draft]` folder, the watcher with a `scratch[1` folder, a project in a subfolder of its repo with `#tmp` and `!keep` folders, and the CLI hint end to end. - A/B with the kernel in both arms, on real repos after writing the root folders a local build writes, against git's own view (ls-files --cached --others --exclude-standard, less codegraph's defaults). Files indexed of those in scope, main then fix: stencil 1015 and 1707 of 1707, Sentinel with the reporter's jib layout 104 and 1423 of 1423, ant-design 2851 and 3138 of 3138, wger 767 and 770 of 770. The fix misses no file in scope and adds none outside it. - For Sentinel, ant-design and wger, the fix's graph on the built tree is byte-identical to main's on a clean checkout. ant-design's 53 changed call sites move `useLocale` from the docs site's hook to components/locale/useLocale.ts. In stencil, every edge the fix adds on the built tree is also in main's graph on a clean checkout, and the 62 name matches it no longer makes are unresolved there as well. - Controls gin and flask: byte-identical graphs. - Index time, median of 3 interleaved: gin 2572 and 2545 ms, flask 2400 and 2378 ms; the repos above take longer only by their restored files. The scope step with 486 and 599 reported entries (a `.DS_Store` in every folder): main 206 and 416 ms, fix 105 and 155 ms. Fixes #2849 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
44adef821b |
fix(erlang): a comment in an argument list no longer counts as an argument
Since PR #1615 made arity part of an Erlang function's identity, arity is counted from the argument list's named children. tree-sitter reports a comment as a named child, so a comment at the top level of a parameter or argument list raised the count by one. A call written plain( % note followed by two arguments was looked up as plain/3 and linked to nothing, a definition with a comment between its parameters was indexed one arity too high, and the MFA list of spawn(M, f, [A, B]) with a comment between A and B gave f/3. A comment nested deeper, inside a tuple, a record or a list argument, was never affected. The three counts now go through one namedArgCount helper that leaves comments out: a clause's and a spec's arity, a call site's arity, and the MFA list's. Conflict resolution: the import block of tree-sitter.ts moved on main since the PR; main's imports are kept, with namedArgCount added. Validation: the new test fails on main (its fixture gets no call edge at all) and passes here. A/B against main: rebar3 97e9367 gains 2 edges (rebar_compiler.erl:403 and rebar_release_SUITE.erl:247, both calls whose argument lists hold a comment, both checked against the source), 0 removed, nodes unchanged; cowboy 6475600 and ranch 7ec436d are byte-identical, as are the controls express and flask. Erlang has no kernel extractor, so there is no parity to run. Index CPU on rebar3, median of 3 interleaved: 9.9 s on main, 9.3 s here. Original PR #1619 by @Dshuishui. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
90c5dc6fc9 |
fix(extraction): a Go method carries the exportedness of its name
Every Go method was indexed as unexported, whatever the case of its name, in both engines: extractMethod never set isExported, and the kernel's go.rs mirrored the omission. Plain functions carried the flag, so dead-code results listed exported methods like Close as dead. A method that is a top-level declaration (methodsAreTopLevel, only Go) now gets the extractor's isExported, as a function does, and go.rs sets is_exported from the same name-case rule in extract_method. Added on top of the contributor's change: findGoPackageMember, which resolves pkg.Name through a project import, matched any exported node of that name in the package's directory, and methods stayed out only because none was exported. With the flag set, a package-level call like harbor's log.Errorf(...) went to the method Logger::Errorf declared beside the function, and prometheus's embedded storage.Appender resolved to a method named Appender, which dropped its implements edges. A method is no member of its package, so it is now skipped there. Two comments in name-matcher.ts that said the flag is unset on every Go method now say why the name's case is still read. Validation: the contributor's test fails on main and passes here, and the new package-call test fails with the PR as first written and passes here and on main, in both engines. A/B against main on harbor 296f304, prometheus a2fda0e, gin 0f09c3a and cobra adbc881: nodes differ only in is_exported 0 -> 1 on uppercase Go methods (5,704, 3,880, 391 and 141). Edges are identical on prometheus and cobra; 22 retargets remain, all name-only guesses between an interface's method declaration and the struct method implementing it, which now win: gin's engine().GET and its siblings reach RouterGroup's methods, and harbor's jobservice routes reach DefaultHandler's. The PR as first written had retargeted 1,384 harbor calls and removed 60 prometheus implements and dispatch sites. The dead-code report lists 3 Go symbols on harbor instead of 167, and 0 on prometheus instead of 11. Kernel parity: 2,493 of 2,493 Go files byte-identical. The controls express and flask are byte-identical. Index CPU on harbor, median of 5 interleaved: 40.8 s on main, 39.8 s here. EXTRACTION_VERSION stays: main already moved it to 28 after 1.6.2, so 1.6.2 indexes get the re-index hint anyway. Original PR #1734 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8f8f04b7fc |
fix(java,ts): a parameterized supertype links its type, like implements Store<String>
A Java class implementing a parameterized interface (implements
Store<String>), one extending a generic base (extends Base<Integer>), a
Java interface's extends Store<T>, and a TypeScript class's implements
Store<string> got no implements or extends edge. The grammars wrap the
type in a generic_type node, and the supertype reader took its whole
text, which no node is named. Without the edge the interface-impl
synthesizer had nothing to walk, so a call on the interface stopped there.
Both engines (extractInheritance on the wasm path, the kernel's java.rs
and tsjs) now name a generic_type by its first child that is not
type_arguments; any other supertype node is its own text, as before.
Validation: the new tests fail 3 of 3 on main with both engines and pass
here. A/B against main
|
||
|
|
428b3c487d |
fix(js,ts): a nested function takes calls to a name the file imports from a package
When a JavaScript or TypeScript file imports a name from outside the
repository, like import { resolve } from 'node:path', matchByExactName
returns no edge for a bare call of that name (the early return main added
for issue 2101). A call to a function declared inside the calling function
under that name, which shadows the import, lost its edge with it.
The candidates for such a call are now narrowed to the calling file's
declarations nested in a function or method (innerScopeShadows), instead
of giving up; a top-level one is the import's own binding. Lexical
reachability, applied later, keeps only the nested declarations visible
where the call is.
Our changes: tests for a sibling function's nested declaration and one
nested a level below the call, which neither may take the call (both hold
through the reachability rule), and for a call from a function nested in
the declaring one; the CHANGELOG bullet moved next to the JS/TS import
bullets, with credit.
Validation: the tests fail 2 of 6 on main and pass here. A/B against main
on 8 repos, kernel per arm: flask, gin, excalidraw, codegraph, pixijs,
pdf.js and three.js byte-identical; vite gains 1 right link, scanImports'
call to its nested scan() beside import { scan } from
'rolldown/experimental'. Index CPU
time on codegraph, test files full of vitest imports, within noise:
81.7 s to 81.9 s (median of 3, interleaved).
Original PR #1715 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0d46dba08f |
fix(js,ts): a call on a new C() receiver resolves through C
A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.
Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.
Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR #2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.
Validation: the new tests fail 4 of 6 on main
|
||
|
|
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
|