mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 18:23:25 +08:00
77ee3484becf256e68b19badadba05940e6a37d5
1717
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77ee3484be |
fix(aliases): a paths wildcard no longer matches an import that leaves nothing for its * (#2969)
A tsconfig/jsconfig `paths` key with text on both sides of its `*`, like "@x/*/index", matched an import that starts with the prefix and ends with the suffix only by counting one character twice: `@x/index` starts with `@x/` and ends with `/index` through its single `/`. applyAliases tested startsWith and endsWith independently and sliced an empty capture, so the import and every call through it linked to `widgets/index`, a file the import never named. The pattern is now skipped when the specifier is shorter than the prefix and the suffix together, which is TypeScript's own isPatternMatch rule. A less specific pattern can still match, and an empty capture at exactly prefix + suffix length stays allowed, as tsc allows it. Validation: new path-alias-overlap tests (4) fail 3 of 4 on main and pass with the fix; the exact-length case passes on both. A/B against main on bulletproof-react, excalidraw, outline and vben (all with paths aliases) and gin and flask: byte-identical, as no paths key in them has text after its `*` (0 of 44 configs with paths in our local clones). outline index median of 3 interleaved: 19,165 ms main, 18,546 ms fix. Full suite: 9,226 passed, 0 failed, 85 skipped. Reimplements PR #882 by @Dashsoap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7973df0d09 |
fix(java): a call resolves on its receiver's declared type, and a library type gets no guess
Java typed a call receiver from a local declaration or a plain field of
the enclosing class only. A field of an outer class read inside an inner
or anonymous class (mHandler.post(...) inside new Runnable() {...}),
Outer.this.f, a chain of fields (this.mOwner.mRepo.save()) and a local
whose type has type arguments fell through to the name-only strategies,
and so did every receiver whose declared type is a library class:
parcel.readInt() on an android.os.Parcel went to a project
VersionedParcel.readInt, list.size() to any project size().
The declared type is now read for each of those shapes, a chain one hop
at a time with each hop's own imports, keeping a written package or
outer class (Map.Entry, play.api.mvc.BodyParser). A receiver whose
declared type the project does not declare, or that an import or a
written package pins outside the project, gets no edge. When no import
decides between same-named types, a Java call prefers the Java
declaration over a Scala or Kotlin one.
Also, from review: a field the class around the call declares is read
as written before the member reader, which keeps only the last name
(PauseTest.Suite, a field written with its package); a local or loop
variable keeps the package its declaration writes; a field typed by a
type parameter goes to its bound, and a class the class head names as
a type argument (Comparable<ApplicationModule>) is not taken for one; a
nested type a superclass declares, or one an on-demand import of its
class names (import play.mvc.Http.*), counts as named; and a typed miss
on a type no project class or interface is named (a URI, an enum's
name()) gets no guess, as on main.
Validation:
- java-receiver-type.test.ts: 19 tests, the PR's 5 red on main; the
other 14 pass on main, and our 8 each fail without their change.
- A/B against main
|
||
|
|
72f398af6e |
fix(index): a video named .ts counts as skipped, so a full index isn't PARTIAL (#2943)
The full index's batch reader recognises an MPEG transport stream named `.ts` from its first bytes (Issue #1910) and skips it, but that branch counted the clip only as processed. Every other file the scan finds lands in filesIndexed, filesSkipped or filesErrored, and CodeGraph.indexAll checks those tallies against filesDiscovered, so the clip left the run one file short. `codegraph init` and `codegraph index` then warned "Index is missing 1 of N discovered files ... The index is PARTIAL", stored index_state = partial, which `codegraph status` reports, and left the indexed commit unrecorded, so status fell back to its full scan. The branch now counts the clip in filesSkipped, as the branch below it does for a file gone from disk. Validation: - mpeg-ts-not-typescript.test.ts: a new case (a clip beside one source file) fails on main's source with the tallies at 1 of 2, and passes with the fix: no index_partial warning, index_state complete. The file's other 11 cases pass. - CLI: on main, `init -y` of app.ts plus testdata/clip.ts prints the PARTIAL warning, status --json reads state "partial", and in a git repo indexed_at_commit stays empty. With the fix it prints "Indexed 1 files", the state is "complete" and indexed_at_commit is HEAD. Follow-up to Issue #1910, reported by @Hetox; found while fixing Issue #2904 (PR #2928). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5594843698 |
fix(go): a call on a value bound from a call or a conversion reaches its type's method (#2961)
A Go method call on a local bound from a call or a conversion, like
`r := newRing(); r.Reset()`, `v1 := router.Group(...); v1.GET(...)` or
`c, r := CreateTestContext(w); r.Use(...)`, was matched by the method's
name alone: inferLocalReceiverType reads a declaration's written type and a
few line patterns, none of which reads a call's result. The by-name guess
took a namesake (etcd's `lg := s.Logger()` linked `lg.Info(...)` to a
project logger's Info, prometheus's `NewWriteHandler(...)` results to the
read handler's ServeHTTP), or found nothing.
Main already types a value from its declaration for indexed and chained
calls (goDeclaredValueType, #2782). Plain receivers now use it too: a local
whose in-scope declaration has a call or a conversion as its value takes
that value's type (the k-th result of several, a method's result, a
conversion's type), read in the file that writes it (GoReceiverDecl.file),
for matchMethodCall, matchMemberFunctionRef and matchGoFieldChainCall. A
value of a type from outside the project links nothing, including one a
project function returns as an outside interface. Assertions stay with
goAssertedLocalType, and a type switch's guard is not typed from its value.
The typer itself, for every path that uses it:
- a trailing `.(T)` is a T whatever its prefix (`pool.Get().(*Conv)`);
- a call through a func-typed field gives the field's result type;
- a bare call a dot import brings in is typed (`package watch_test`
calling `NewStreamWatcher(...)`);
- a method of a struct or interface literal, of an outside generic type
(grpc's `ServerStreamingClient[Resp].Recv()`) or one the index doesn't
show on a project type gives an unknown type, not "outside", so name
matching runs as before.
Validation:
- go-receiver-type.test.ts (the PR's 23 cases, 2 expectations updated where
the call is now typed, plus 4 ours), go-std-method-receivers.test.ts and
go-chained-receivers.test.ts: on main 19 of 51 fail; all pass here. All
45 go-*.test.ts files pass (491 tests).
- A/B vs main
|
||
|
|
3f8e8b0f65 |
fix(rust): a module-path call links the function its module declares
A Rust path into a module, `crate::util::take(1)`, `super::util::take(1)`,
`util::take(1)` or a `use crate::util::take;`, went to the first node of
that name in the module's file, methods included, so an `impl Buf { fn
take }` above `pub fn take` caught every such call. A function written
inside another function, or in a `mod tests { ... }` above the module's
own item, could take it too, a path into the module's own file linked
nothing, and a path through a module a `use` takes from the standard
library (`fs::read` under `use std::fs;`) went to the project's own `fs`
module.
The path's item is now the module's own: never a method, an impl's or a
trait's associated item, or an item of a function body; one written at
the file's top level before one in a module the file declares inline,
which the path reaches through the module's re-export (`#[cfg(unix)] mod
imp { ... }` + `pub use imp::*;`). A path whose head a `use` binds from
the standard library is left to it (main's isBoundToOutOfRepoImport rule
for bare names), and a path into the module's own file resolves.
Validation:
- rust-module-path-call.test.ts: 8/8; on main 7 fail (method Buf::take,
Deep::get, Only::grab, no import edge, the inline-module namesake, the
re-export from its own file, std `fs::read` -> src/fs/mod.rs).
- A/B on main
|
||
|
|
69cddc0156 |
docs(changelog): credit the earlier pull requests for three unreleased fixes (#2956)
While closing first-time contributors' pull requests whose fixes had already landed, three were found to predate the fix that shipped, and their close comments promised changelog credit: - @meganemura's PR 1503 wrote installer config through symlinked files 2.5 months before the fix for issue 2721. - @rudycelekli's PR 2251 preserved Hermes settings after the MCP server block three weeks before the fix for issue 2485. - @equipados's PR 1641 linked unqualified paren-less Delphi calls six weeks before the fix for issue 2514. Changelog only; no code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
25776014e9 |
fix(resolution): a call on the caller's own class reaches that class's member, or what it inherits
Root cause: a bare call in Java or Scala, and a `self.`/`this.` call in Python, TypeScript and JavaScript, reaches matchByExactName as a bare name. With several same-named methods in reach, findBestMatch ranked them by file and line distance, so a sibling class's method declared nearer the call won over the caller's own class's or the one it inherits; above the 500 candidate ceiling the call was dropped. Fix (contributor): matchEnclosingScopeMember runs before the ranking. It reads the receiver back from the call line (bare, self/this, super) and walks the caller's scopes innermost-out: the own class, then what it inherits, then, for a bare call, the classes it is nested in. A member found there is the answer (0.9), and its overloads are ranked among themselves. A super call in a JS class expression is left to js-super-calls (PR 2497). Ours: - Inherited members first come from the declaration's own resolved supertypes (getSupertypeNodes), in the order its header lists them, a class further up before an interface that only declares the member; the PR's walk by name stays as the fallback when those edges reach nothing (an `extends` the index resolved to a namesake, as Minestom's Player to a nested EntityStatuses.LivingEntity). By name alone, Django's spatialite, oracle and mysql GIS editors reached the postgresql backend's DatabaseSchemaEditor.execute and _delete_index_sql (0.9), a class with two bases took either one's member (BaseDeleteView(DeletionMixin, FormMixin) got FormMixin.get_success_url), and SystemUI's RotationContextButton got RotationButton's declaration instead of ButtonDispatcher.getCurrentView. - Only `super`, `this`/`self` and Python's `cls` name the receiver. The PR also read `parent.`/`base.` as super and `cls.`/`static.` as self, but Java's extractor drops a receiver variable of those names too: protobuf's `parent.getFullName()` on an EnumDescriptor went to GenericDescriptor. - C++ is left to matchCppImplicitThisCall (PR 2479), which already looks members up in the class, its bases and its enclosing classes. The PR's C++ branch added the caller's own file's namespace functions, which beat the overload the arguments fit: fmt's 3-argument `copy<Char>(b, e, out)` calls went to format.h's 2-argument copy (0.9). - Tests for each, and the CHANGELOG bullet in the house format. Validation: enclosing-class-member-call.test.ts has 12 cases: 6 fail against main's resolver, and all 5 of ours fail against the PR as written. Kernel A/B against main |
||
|
|
760e31f2b4 |
fix(install): install.ps1 resolves the latest release without GitHub's rate-limited API (#2939)
install.ps1 resolved "latest" with one Invoke-RestMethod to the GitHub API, with no fallback, under ErrorActionPreference Stop. The API allows 60 unauthenticated requests an hour per IP and then answers 403, so `irm .../install.ps1 | iex` died with a raw "(403) Forbidden" on shared networks and in CI. A pinned CODEGRAPH_VERSION=1.6.2 was used as-is, so the download URL lacked the v that release tags carry and 404ed. install.sh (PR #336) and `codegraph upgrade` (resolveLatestVersion) already handle both; install.ps1 was never ported. Fix: step 2 is now a function between Resolve-CodeGraphVersion markers. It sends a HEAD to github.com/<repo>/releases/latest with System.Net.HttpWebRequest and AllowAutoRedirect off, reads the tag from the Location header with /releases/tag/([^/?#]+), falls back to the API inside try/catch, adds the v to a bare version, and keeps the friendly throw when both fail. Install-CodeGraphFiles is untouched (still equal to WINDOWS_SWAP_FUNCTION). Validation (Windows PowerShell 5.1.26100; pwsh 7 is not installed here): - Each arm's unmodified step 2, with Invoke-RestMethod shadowed so the API answers a local 403: main stops with "(403) Forbidden"; the fix resolves v1.6.2 from the real redirect with no API call. CODEGRAPH_VERSION=1.6.2: main builds .../download/1.6.2/... (HEAD 404), the fix .../download/v1.6.2/... (HEAD 302). With the API available, main makes one API call and the fix none. - New Windows-gated test runs the shipped resolver block in PowerShell against a local stand-in for github.com and the API: redirect read with one HEAD and no follow, API fallback for a 500 or a missing Location, the friendly error (no raw 403) when both fail, the v added to a pinned version with no request. 4 fail on main's install.ps1, 4 pass with the fix; upgrade.test.ts (the swap function pin) passes. - Both scripts parse with no errors in PowerShell 5.1. The install steps were not run: they always add the install to the user PATH. Reported in PR #560 by @hiyochi; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6162d872be |
fix(watcher): a deleted link target no longer pins a CPU core on Windows (#2938)
On Windows, libuv reports a watched folder that is being deleted by the folder's own absolute path (\\?\D:\...) and re-arms the read, which fails the same way at once: about 70,000 events in half a second until the stream is closed. The watcher gives each directory link in a project its own recursive stream, so deleting a link's target outside the project started that storm. handleDirEvent joined the absolute name onto the watched folder, got a path that doesn't exist, scheduled a full sync and nothing else. The stream was never closed, the server held a core until restart, and the storm kept re-arming the debounce, so no sync ever ran: the deleted files stayed in the index and later edits never auto-synced. The project root's stream had the same blind spot. Fix: handleDirEvent, which both stream callbacks use, now treats an absolute name as the watched folder itself reporting it is gone. The first such event on a stream arms one check 200 ms later; the rest of the burst finds it armed. The check closes a link's stream when the link no longer leads to the folder it watched (deleted, or pointed elsewhere) and re-reads the tree, whose full sync drops the link's files; a link that leads somewhere live is watched anew. A project root that is gone degrades the watcher with a "project folder was deleted or moved away" reason, which closes every stream. A normal event pays one path.isAbsolute check. Validation (Windows 11, Node 22.22, libuv 1.51): - Raw probe: fs.watch on a junction whose target is deleted gets 70,009 events named \\?\<the junction> in 500 ms (499 ms CPU), and none after close(). - Real MCP server over a project with a junction libs -> outside, outside deleted: main used 3484, 3406, 3391, 3281 ms of CPU per 3 s window and kept the linked symbol; an edit afterwards never synced. With the fix: 296, 0, 0, 79 ms; the linked symbol left the index; the edit synced. - New watcher tests: a fake fs.watch link storm (10,000 self-named events cost no fs call, the stream closes once, a full sync runs, the project stays watched), a fake root storm (degrades once, closes the stream), and a Windows-gated real junction whose storm must go quiet. All 3 fail on main's watcher.ts; watcher.test.ts passes 61/61. Reported in PR #2487 by @L0garithmic; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
31ec908224 |
fix(sync): a long-running session picks up a framework added while it runs (#2941)
The MCP server's watcher holds one CodeGraph for the whole session, and its framework detection was cached from the first time it ran: the orchestrator reset it only in indexAll, and CodeGraph.sync never re-initialized the resolver. A framework that arrived mid-session, like Express in a new package.json, was missing from every file synced after it, so a new route file got no route nodes until `codegraph index`. The resolver's declared-dependencies cache was also keyed by the indexed file COUNT (PR #2519): swapping one app for another of the same size, or editing a manifest, kept the old dependency set, even through reinitializeResolver(). Detection now records what it read: indexed source files, which a sync's change lists cover, and every other path it read or probed, with a size and mtime signature, including the per-app manifests the extractor gating reads. Before a sync extracts files, it detects again only when files were added or removed since, a source file detection read changed, or one of those paths changed on disk. A scoped sync takes the file list from the index plus the files it adds, so it never walks the project (the first sync in a process used to run a full scan for its first detection). What detection read is kept between detections and dropped by the same change lists and signatures, so detecting again reads only what changed. When the detected set differs from the resolver's, the sync re-initializes the resolver. Its initialize() now clears its caches before detecting, and clearCaches() forgets the declared-dependencies cache with the other per-context memos. Files indexed before a framework change are not re-extracted. A framework like Express applies to every file of its language, and re-reading them all inside a watcher's sync would cost what a full index does. Instead a full index records the frameworks it used, a sync records how detection moved since, and `codegraph status` (text, and --json with reindexRecommended and frameworksChangedSinceIndex) recommends `codegraph index` while they differ. Status only reads that record. Validation: - sync-framework-detection.test.ts (new, 8 cases): Express arriving with a new route file, by full and by scoped sync; a .svelte file added with no manifest change; an edited source file that detection read; a manifest-only edit picked up by the next sync, with the status record, cleared on revert and by a full re-index; a fresh process's scoped syncs detecting once, from the index, with no scan; the equal-size app swap; and the dependency cache forgetting on request. 7 of 8 fail on main's source (the 8th needs the new export). Ten mutations, one per mechanism, each fail a test. - The triage probes: main detects [] after Express arrives and routes.js gets no route; the fix detects express and extracts GET /hello, by full and by scoped sync. For the swap, main's reinitializeResolver() gives [react] and the fix [express]. - Cost on excalidraw (737 files, scoped syncs in one long-lived instance): the detection step takes 8 ms on a sync where nothing it read changed and 8-13 ms when it detects again, against syncs of about 260 ms (add) to 2 s (modify). The first sync in a process detects once (about 300 ms) and no longer scans the project. Reported in PRs #2516 and #2519 by @nk33-dev. This reimplements both fixes independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7cceba52b0 |
fix(resolution): library calls on untyped Python, Scala and Java values stay unlinked
Root cause: when nothing types a call's receiver, matchMethodCall's last
strategy links `recv.m()` to the one project method named `m` (0.7), or to
the best receiver-word match among several. A standard-library call on a
plain value - `options.setdefault(...)` on a dict, `m.getOrElse(...)` on an
Option, `data.put(...)` on a map in a `var` - then linked to an unrelated
project method of that name. Go, Rust, Kotlin, C#, VB.NET and Dart already
keep such a guess only when the receiver names the method's owner
(stdMethodNames); Python, Scala and Java had no list.
Fix (contributor): Python, Scala and Java standard-library method lists in
stdMethodNames. Ours: the PR also exempted a receiver named `cls` from the
rule; both extractors already strip `cls.` from Python calls, so it never
applied there, while in C#, Dart, Rust and VB.NET it let
`var cls = Make(); cls.ToString()` link to an unrelated Money.ToString.
Removed, with a C# test case. CHANGELOG bullet rewritten and moved next to
the Python built-in method bullets.
Validation: library-method-calls.test.ts: 3 of its 5 cases fail on main
(Python build, Scala go, Java untyped), all 5 pass; the C# case fails when
`cls` is put back. Kernel A/B against main
|
||
|
|
45a29b4685 |
fix(worktree): a linked worktree is recognized however Windows spells its path (#2937)
On Windows one directory has several spellings. VS Code and Cursor pass a
lowercase drive letter, NTFS ignores case, a subst drive is a second name
for a folder, and long names have 8.3 aliases. git answers with the
spelling on disk, but src/sync/worktree.ts compared its paths with the
caller's as strings, after a JS realpath that keeps the caller's spelling.
The main checkout's git common dir comes back relative (".git"), so it
took the caller's spelling while a linked worktree's came back absolute
in git's. A worktree inside the indexed checkout, spelled unlike git,
failed all four comparisons:
- detectWorktreeIndexMismatch found no mismatch, so codegraph status
(text and --json) dropped the borrowed-index warning;
- nestedRepositoryBelow took the worktree for a different repository, so
the MCP tools refused it as unindexed and told the agent not to call
codegraph for it again.
Fix: the four comparisons now ask whether two paths name one directory,
with the identity canonicalProjectRoot already uses (the native realpath,
lowercased on Windows), falling back to the JS realpath where the native
call fails. Returned paths keep the caller's spelling for display. A
nested repository below an index root outside git is still reported, as
before.
Validation (Windows 11, Node 22):
- New Windows-gated tests with a lowercase drive letter, at the helper
level and through ToolHandler (search and status, with and without a
default project): the 3 behaviour tests fail on main's worktree.ts and
pass with the fix. Both files: 32 passed.
- Fixture: a checkout with a worktree under .claude/worktrees and an
embedded clone, spelled four ways (on-disk, lowercase drive, other
capitals, subst drive). Helper verdicts wrong: 6 on main, 0 with the
fix. codegraph status warned in 2 of 8 runs on main, 8 of 8 with the
fix. The real MCP server refused the lowercase, capitals and subst
spellings on main; with the fix it serves all four with the
borrowed-index notice, codegraph_status warns, and the embedded clone
is still refused. An 8.3 short name (the contributor's case) went from
2 wrong verdicts to 0.
Reported in PR #2517 by @nk33-dev; this reimplements the fix
independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
475e56b209 |
fix(cli): read commands and the prompt hook open the index read-only (#2936)
Running query, files, node, callers, callees, impact, context, explore or affected while `codegraph index` rebuilt a project could make the rebuild fail with "database is locked", and so could the hidden prompt-hook, which Claude Code runs on its own for structural prompts. Each opened the index read-write, so its open ran the writer's repairs: during the rebuild's bulk load they recreated the secondary indexes the indexer had dropped, and the indexer's next write, whose transaction had already read, failed with SQLITE_BUSY_SNAPSHOT (517). PR #2930 fixed the same thing for status. These commands now open the index read-only, through one helper, and leave migrations, index recovery and WAL maintenance to the writer: - query, files, node, callers, callees, impact, context, explore, affected and prompt-hook are read-only. explore and node run the MCP tool handler in process without a lifecycle, so they never sync or open another project. - While another live process holds rebuild.pid they say so in one line on stderr (a --json answer stays parseable), as status does. - The prompt hook stays out of a running rebuild altogether: the index is half-built until it ends, the MCP tools answer "rebuild in progress" meanwhile, and not opening it keeps the hook from holding the file the rebuild replaces. Its gate records the new outcome noop-rebuild. - The hook's one write, filling an empty segment vocabulary (issue #1142), goes through a writer connection of its own, opened only once it holds the index lock, which every index and sync holds while it writes. - status (read-only since PR #2930), sync, index, init and uninit are unchanged; the last four write. A read-only open migrates nothing. QueryBuilder.hasSchema lets each read that needs a later table or column answer as that migration's defaults would: files.generated (v9), unresolved_refs.status and name_tail (v8), and name_segment_vocab (v7). The status pending count from PR #2930 uses it too. Validation (Windows 11, Node 22.22): - The issue's interleaving, each command in another process inside the writer's edge transaction: on main all 14 command forms make the insert fail with errcode 517 and bring the 5 dropped edge indexes back; with this change all 14 inserts succeed and the indexes stay dropped. - Real rebuilds of excalidraw 95307fa1 (736 files) with query and the prompt hook looping beside them, default settings and with the resolution window forced: main failed 6 of 14 ("store worker: database is locked" with an empty index, or "database is locked" partway through resolution), and 12 of its 109 query runs failed at open with SQLITE_BUSY. This change failed 0 of 14, with 0 failures in 299 query and 284 hook runs and the same graph every time (14,236 nodes, 61,210 edges). - Old indexes: 24 command forms on indexes at schemas 4 to 16 (11 schemas rebuilt from a current index, plus indexes built by 0.9.9, 1.4.0, 1.5.0, 1.6.0 and 1.6.2): output identical to main's, which migrates first, 384 of 384. The schema stays as it was, except where the hook fills the vocabulary of a pre-v7 index; that writer's open migrates it, as every read-write open did. - Timing on excalidraw at rest, 22 interleaved runs per command: medians within -5.8% to +3.3%, -0.9% in total. - read-commands-during-rebuild.test.ts: 38/38 pass. With main's sources 35 fail; with only the CLI opens reverted 33, only the schema fallbacks 19, only the read-only vocabulary fill 2. Follow-up to PR #2930. Fixes #2913 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cbd9817f7c |
fix(extraction): a damaged C #if group no longer turns into functions named if
tree-sitter-c parses every branch of a `#if … #elif … #else … #endif`
group as a run of block items, so a group is harmless only when each
branch is complete on its own. Three shapes in real C are not: a branch
that begins with `else` (an arm of an if-chain kept behind `#ifdef`), a
branch that ends with a bare `if (…)` header whose body follows the
`#endif` (ST's HAL latency tables), and a branch whose braces don't
balance (a function head that differs per build). Each leaves a
K&R-shaped `if (…) { … }` that the grammar reads as a function named
`if`, filed inside the enclosing function, and the functions after it
were nested under it or lost. Separately, a block macro in capitals in
statement position (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`) was skipped by the
statement-macro blank, which matched lowercase iterator macros only.
blankCUnbalancedConditionalBranches runs at the end of preParseCSource,
after the directive restore, since it edits directive lines. A group
where a branch shows one of those shapes, or is a fragment of an
expression or a list (`|| (h4 == NULL)` inside an `if (…)`), loses its
directive lines, offsets kept. When a branch can't stand next to the
others, one branch is kept verbatim: the `#else` of a positive test
(`#ifdef X`, `#if defined(X)`), the first of `#ifndef`, the live one of
`#if 0`. The statement-macro blank also takes ALL_CAPS names (PascalCase
still excluded, for C++ constructors). Both engines see it: the kernel
gets the pre-parsed bytes from the TS route point.
Ours on top:
- A group on a flag whose branches each balance (braces and parens)
keeps every branch, only its directive lines blanked: read in a row
they are still C (`} else if (a) {…} else if (b) {…}`, list entries,
`|| x || y`). Dropping one lost real code of builds the project uses:
betaflight's failsafe.c arms under `#if ENABLE_RESCUE_PLAN` (default-on
in common_post.h), and adc_stm32h7xx.c's adcTagMap, whose inner MCU
chain kept its `#else #error` branch. Unbalanced and literal groups
keep one branch, as above. The PR's first unit test now expects both
arms of its balanced else-led group.
- The group is read on a copy with comments and literals masked. A brace
inside a block comment over several lines counted as code, so Lua's
`** {======` banners made loadlib.c's platform group look damaged, and
its dlopen, Windows and dyld implementations were dropped for the stub;
on betaflight it dropped 19 CMSIS-DSP MVE functions.
- A line splice is read on the raw line: a `\` at the end of a comment
line inside a #define body still continues the #define (masking hid
it, which made jemalloc's tsd.h include guard look damaged).
- Only a directive's own `\` continuations are blanked with it. The PR
blanked every directive line right under a collapsed group's `#if` or
`#else`, so a kept branch lost a leading `#include` or `#define`.
- `#if 1` keeps its first branch; it kept the dead `#else`.
- Unit tests for each, mutation-checked; CHANGELOG bullet rewritten
without numbers.
- Tried and dropped: keeping the feature branch of a flag the file
#defines itself (the same-file evidence PR #2615 uses). Applied per
group, it broke dtoa's coordinated groups in jq (`#ifdef
Avoid_Underflow` kept its branch while a later `#ifndef
Avoid_Underflow` still kept its `}`), cutting jvp_strtod in half.
Validation, main
|
||
|
|
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
|