mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
214ef50efe9fb53a64623ab8c6678b6ef99cf293
1621
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
214ef50efe |
fix(explore): names the query spells that the index never saw are said, not glossed over (#2789)
Root cause: explore answers whatever the query's words match. When a name the query spelled is not in the index at all, the answer is built from the other words and says nothing about the missing ones. In the report, the tool projects under a .NET repo's root build/ folder are skipped by default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData and AddFeatures returned an unrelated Program.Main and a C++ Parser, and the agent read the wrong code. The miss diagnostics (issue #2026) ran only when nothing at all matched. Fix: - Query tokens written like code names (a camelCase hump, snake_case, or a qualified name) that resolve to no node at all are checked with a new bounded probe, getUnseenNames. It reports names the index has never seen: no node called that in any case, and no name, qualified name or path, signature or docstring word starting with it. - A qualified name is judged by its member. Express's app.handle is indexed as handle, so the receiver alone never makes a name missing. - Names that pass are listed in a note right under the summary line, success-shaped. The note points to codegraph status and never to Read. It names the .gitignore re-include for a default-skipped folder like build/ as the user's choice, since indexing scope is the user's call. - A single word (Program, Main, How) is never named: it may be English. - The total-miss answer gains the same note. - The seeding's query tokenizer moved into one shared function, unchanged. The skip itself is unchanged here. On main a root build/ folder is still skipped by default: PRs 2681 and 2687 re-opened build folders below src/ only. The documented opt-in, !/build/ in .gitignore, indexes it, and sync picks the opt-in up, verified on main and on 1.6.2. Validation: - New explore-unindexed-names.test.ts, 8 tests: 6 fail on main. - All 38 explore suites pass, plus the tool-allowlist and dynamic-boundary suites (466 tests). - The reporter's repo, newrelic/newrelic-dotnet-agent 418a0f2f, now commits the !/build/ workaround. With that line removed and the index synced, main answers the reporter's query with VersionFileGenerator/Program.cs Main and Sicily/Parser.cpp, saying nothing. The fix adds the note naming exactly the four missing names. - Ordinary queries: 996 on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical to main. - Miss probes: 120 queries mixing real names with invented ones, or only invented ones. Main never notes anything. The fix names every invented name in all 120 and no real name. The note costs about 5 source lines on a full response. Fixes #2462 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0953084bc5 |
fix(pascal): the receiver calls of a chained Delphi call are indexed (#2787)
A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.
The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.
Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).
Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
included): all 7 fail with main's sources. Without only the resolver
rule the typecast test fails; without only the chain-link gate the
fluent-chain test fails.
- A/B against main
|
||
|
|
7180fd0666 |
fix(js,ts): a dynamic import from Node or a package no longer links to a project namesake (#2788)
Issue 2491 was reported against 1.6.1: `import { resolve } from 'node:path'`,
bare `'fs'` and `require('node:fs')` bindings linked their calls to
same-named project functions. Main no longer does that for any static
form. PR 2120 (released in 1.6.2) declines a name match for a name the
file imports from outside the repository. PR 2437 made that hold in
projects with a catch-all tsconfig path alias, and PR 2671 extended it to
function values such as `promisify(readFile)`.
What still linked is the same binding written as a dynamic import. The
out-of-repo check reads the file's static import mappings only. The name
matcher deliberately treats `const { x } = await import('./m')` as an
import, not a local shadow, so the call fell through to a same-named
project function. vite's
`const { createServer } = await import('node:http'); return createServer(app)`
linked to vite's own `createServer`.
Fix (name-matcher.ts):
- isOutOfRepoBinding also asks dynamicImportSource for the module a
dynamic import binds the name from, where the reference sits.
- A per-file scan (FIFO of 64, cleared with the other name-matcher memos)
finds `const { a, b: c, default: d } = await import('x')` declarations
and `import('x').then(({ a }) => ...)` callbacks on the raw text. Only
files with a match are comment- and string-blanked, to drop matches in
comments and strings and to find where each binding's scope ends: the
enclosing block (a top-level await binds the whole module) or the
callback body.
- A binding counts only when the reference sits inside that scope after
it, and the innermost one wins, also over a static import of the name.
nitro's `async function createWebSocketProxy()` imports crossws's
`createWebSocketProxy` inside itself: only that inner call is dropped,
and the file's other calls still reach the local function.
- jsBlockEnd and jsBodyEnd are the forward forms of jsBlockOpenAtEnd and
jsBodyOpenAtEnd, which now call them.
Validation:
- __tests__/dynamic-import-binding.test.ts: 5 of 6 tests fail with main's
name-matcher.ts (the project-module control passes there); all 6 pass on
the kernel and with CODEGRAPH_KERNEL=0. Four mutations each fail their
test: no scope end, no callback form, no dynamic source, and no memo
clearing (the in-process sync test).
- Sync equals a fresh index (CLI) for adding and removing the dynamic
import, for the namesake file arriving later, and for the nitro shape.
- A/B on 9 TS/JS repos plus 2 controls, both arms with the kernel: 6 edges
removed, 0 added, 0 retargeted, all 6 wrong. vite has 3 (node:http
createServer, node:fs/promises readFile, rolldown watch), memfs 2
(node:util promisify), nitro 1 (the crossws self-loop). pathe, c12,
unbuild, fs-extra, unenv and nuxt are byte-identical, as are the flask
and gin controls.
- Cost: fresh index median of 3, interleaved: vite 9.17 s main vs 9.50 s,
nitro 7.43 s vs 7.12 s (noise on a shared box). The scan itself over
every vite source file takes 40 ms (24 files reach the blanking pass).
- Kernel parity is unchanged: 772/776 files byte-parity, 4 deferred,
over nitro/src and vite's src (this is a resolver-only change).
Fixes #2491
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
c46523079f |
fix(context): the busiest file is found even when 20 test or generated files outrank it (#2785)
Root cause: getDominantFile and getTopRouteFile took the top 20 files in SQL and only then dropped test and generated files in code. Twenty test files busier than the app's busiest file left nothing, so getDominantFile returned null and the context builder's core-directory boost never applied. Nineteen left the dominant file without its runner-up, and nextEdgeCount 0 passes the "3x the next file" test for a file that is not dominant at all. getTopRouteFile returned null the same way, and measured its 30% share against the routes of the 20 files in the window, not the project's. Fix: - DOMINANT_FILE_SQL ranks every file. The window saved no work: ORDER BY needs every group, so the aggregate runs to the end either way, and only one row per file comes back. - computeDominantFile filters the ranking 20 rows at a time until it has the dominant file and its runner-up. The generated-file lookup stays a bounded probe per batch. - getTopRouteFile counts every route file, with ties broken by path. Validation: - New dominant-file-window.test.ts, 6 tests, all failing on main: the issue's layout with 20 and with 19 test files, 25 header-declared generated files (past the first batch), and three route shapes. - The dominant-file cache, plan and allocation suites and the route-file detector test pass. - Seven of the eight real repos give the same dominant file before and after (express, flask, excalidraw, django, airflow, grpc-go, etcd). The closest to the window was airflow, with the runner-up at rank 16. - envoyproxy/go-control-plane crosses the window: 254 generated and test files outrank its first hand-written file. Main returns null; the fix returns xdsmatcher/pkg/matcher/matcher.go (167 edges, runner-up 165). - getTopRouteFile on main reported django's admin/sites.py at 8 of 16 routes and airflow's ui router at 65 of 65. Counting every route file gives 8 of 32 and 65 of 332, both under 30%, so null as documented. - Cost, median of 3 interleaved rounds of 5: getDominantFile on airflow (8,564 ranked files) 581 -> 587 ms, django 189 -> 181 ms, go-control-plane 277 -> 284 ms. getTopRouteFile on airflow 1.3 -> 2.5 ms. - Explore A/B, main vs fix: 996 queries on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical. Fixes #2578 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
800e582d55 |
fix(go): conversions, external test packages and build-tag twins link as Go builds them (#2784)
Three Go shapes recorded edges the source doesn't make. A conversion
like Category(s), (*Pair)(p) or model.Kind(3) became a calls edge to
the type, or an instantiates edge to a struct. A bare call in a
package's external tests (package a_test) could bind to a same-named
helper of package a in the same directory, which a_test can't see. And
a name a package defines once per build target, like sep in sep_unix.go
and sep_other.go, linked to whichever definition came first, so a Linux
caller reached the !unix file.
A Go call whose target is a type is now a references edge. Two files of
one directory under different package clauses see each other's
top-level names only through an import: qualified, or bare after a dot
import and exported. A reference whose definition has build-constrained
twins in its package reaches the one that builds for the most typical
target its own file builds for: linux/amd64, then darwin/arm64,
windows/amd64 and the rest. Constraints are read from the file name's
GOOS/GOARCH suffix and the //go:build or // +build lines, with tags as
go build sets them by default.
Validation: 9 new tests in three files, all failing on main, and each
part switched off fails its own file; the Go tests pass. A/B against
main
|
||
|
|
1890b10cc4 |
fix(explore): a named function too long for its share renders instead of the file's tail (#2783)
Root cause: an oversize cluster keeps its first-ranked member however big it is, and the ceiling windows it (CG-30). Ties on importance went to the shorter span, so in the issue's file, one 200-line function plus the line that calls it, the one-line `path = build_report()` took that guarantee. The search matches it through its signature, so it is an entry point too. The function the query named then no longer fit beside it and was dropped, and `codegraph_explore build_report` returned the file's last six lines under the verbatim-source header. A file asked for by path did the same, because every definition in it ranks below the bare edge lines. Where a body was windowed, nothing in the block said so. The footer's advice, ask for the symbol by name, returned the same window, and it suggested `codegraph_node`, which the default MCP surface does not list. Fix, all in the explore render path (query time only): - Rule 1: among cluster members of one importance, a definition the query named ranks ahead of one its search only matched, before the shorter span. - Rule 2: a fragment does not spend the oversize guarantee. When the kept members render under MIN_CHARS, the member they priced out is kept and windowed like a first-ranked one. That member must be one the query named, or a definition in a file pinned by path with nothing named in the cluster. - Rule 3: a body the ceiling window cuts ends with the per-symbol view's note, "... lines X-Y of `name` elided - codegraph_explore `file:X-Y` returns them" (one shared helper now). Each note is paid for by its own part's last lines, never a focus line, the member's first line or a member the part shows whole. So no section grows and no other file's budget moves. - Rule 4: the trim footer and the dynamic-boundary note name `codegraph_node` only when it is on the served tool surface. Validation: - New explore-queried-oversize-symbol.test.ts, 9 tests: 6 fail on main. Reverting any one rule fails that rule's own tests (2, 2, 3 and 1). - All 37 explore suites pass (414 tests). - Deterministic A/B: one index per repo, a fresh handler per query, five arms (main, then each rule added in turn). 820 queries per arm: the 50 longest functions by name, their files by path, caller/callee pairs, call chains, random names and flow questions. - The queried function's head (def plus 10 lines) renders, main -> fix: express 50 -> 50, flask 49 -> 50, excalidraw 33 -> 48, django 43 -> 47, airflow 43 -> 48, of 50 each. - Queried body lines rendered, main -> fix: excalidraw 6,615 -> 9,391, django 5,337 -> 5,662, airflow 6,320 -> 6,749. - Rule 1 changed 32 responses, each hand-triaged as the queried function now rendering. Only 4 of the 504 ordinary flow queries are among them. - Rule 2 changed none. - Rule 3 wrote 838 notes, all checked against source and index, at 2.8 to 3.9 source lines each. Bytes a note frees flow to lower files by the existing carry-forward. - Rule 4's 236 diffs are exactly the removed mention. - No response exceeds the 25,000-char inline cap. Fixes #2486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0c3e12ddbb |
fix(go): a call further along a chain links the method of the type the chain reaches (#2782)
A Go method called on a receiver the extractor keeps no name for, such as
a fluent chain's later link (model.WithContext(0).Where(1).First()), a
call on what a function returns (time.Now().UTC().Format(...)) or a
composite literal (GitOracle{}.Worktree()), reached the resolver as its
bare name at the column where the chain starts. Name matching then took
a project method of that name by proximity, or the only one: the issue's
GORM-style Where and First went to another query type, the standard
library's Format to the one project Format, and a literal's method to a
namesake type.
A new Go resolver step reads the chain at that column and types the
receiver link by link: a call's result, or the one a multi-value
assignment takes; a conversion's or a composite literal's type; a
generic call given its type arguments; an embedded field by its type's
name; a package-level variable from the call it is set to. A result
written with type parameters keeps them as parameters, read from the
declaration, since the indexed signature leaves them out, and the helper
that reads them now includes a one-line function's only line. The method
is the one Go selects on that type. A type from outside the project, or
one that can't be read, links nothing instead of a guess. When a chain
calls one name twice, the bare ref is the later call.
Validation: 8 new tests; 7 fail on main, and the repeated-name case
fails under a first-link rule; the 43 Go test files pass. A/B against
main
|
||
|
|
88ebefa135 |
fix(pascal): calls inside a Delphi for var x in loop are indexed (#2781)
The Pascal grammar's for-in rule takes a plain variable, so a loop that
declares its variable inline (Delphi 10.3 and later), like
`for var lItem in AList do lItem.DropIfEmpty;` or
`for var lHandle: THandle in lHandles do`, became an ERROR node. A
paren-less call in its body lost its statement shape and reached no
graph, so the routine was missing from the callee's callers.
The Pascal preParse added for issue #2545 now also blanks the inline
`var` and the loop variable's `: Type`, offsets kept, so the loop parses
as the grammar's `for x in` form. The type runs to the `in` at nesting
depth 0, so generic and dotted types are whole; `for var i := 0 to`,
which the grammar already parses, and `for var` inside comments or
strings are left alone. The resolver still types the loop variable: it
reads a receiver's declaration from the source as written, so
`lHandle.Close` reaches THandle's Close, not a namesake.
Validation:
- New __tests__/pascal-for-in-var.test.ts (6 tests, the issue's unit
included): 5 fail with main's pascal-preparse.ts, all pass with it.
- Every textual `for var x in` in the corpus is blanked except 9 that sit
in comments or strings (DelphiMVCFramework 47 of 49, DWScript 89 of
96), no other text is touched, and no file parses worse.
- A/B against main
|
||
|
|
929c65c83a |
fix(pascal): Delphi routines with a parameter attribute like const [ref] are indexed (#2778)
The Pascal grammar's parameter rule takes no RTTI attribute, so a
declaration like `procedure WithRef(const [ref] AValue: Integer; ...)` or
DelphiMVCFramework's `function CreatePerson([MVCFromBody] const Person:
TPerson)` broke the parse at the `[`. The method was lost, the parameter
after the attribute became a field of the class, every member declared
after it went missing until the next class, and in an implementation the
routine's locals became unit variables and its calls were credited to the
file node.
The Pascal extractor now has an offset-preserving preParse that blanks an
attribute group at the start of a routine parameter, before or after its
modifier (const/var/out/constref), in any routine header, procedural type
or anonymous method (`operator` counts only after `class`, since Delphi
doesn't reserve it). It is a linear hand-written scanner that skips
strings, comments and directives, reads generic and qualified routine
names, and leaves every other bracket alone (set defaults, array types,
member and method attributes, property index lists, code). A new
LanguageExtractor flag, signatureAsWritten, lets Pascal signatures read
the source as written, so `(const [MVCFromBody] Person: TPerson)` keeps
its attribute instead of showing blank space. Only Pascal sets the flag.
Validation:
- New __tests__/pascal-param-attributes.test.ts (8 tests, the issue's unit
included): 6 fail on main's wiring, all pass with the fix.
- A/B against main
|
||
|
|
57ce5b9cdc |
fix(upgrade): a global npm install is upgraded with -g from any folder, even home (#2777)
codegraph upgrade told a global npm install from a local one by whether the running file sat under the current folder. npm's global folder is often inside the home folder (Windows %APPDATA%\npm, nvm, fnm, ~/.npm-global), so run from home every such global install was taken for a local one: npm ran without -g, the global copy stayed on the old version and a second copy landed in the home folder. The same test sent a project's own install to npm -g when upgrade ran from one of its subfolders or from outside it, and installed into the parent folder instead when it ran from one. The scope now comes from npm's layout around the outermost node_modules. A global launcher beside it (<prefix>\codegraph.cmd on Windows, <prefix>/bin/codegraph next to <prefix>/lib on POSIX) means global. A project around it (node_modules/.bin/codegraph or a package.json) means local, and npm then runs in that project, wherever upgrade was run from; the version npm left there is read back, with a warning when it did not change. A pnpm, yarn or bun install (a .pnpm layout, or that manager's lockfile beside node_modules) is named and given that manager's command, since npm run in its folder would only add a second copy. Validation: on Windows, the real CLI was run with a private USERPROFILE, HOME, APPDATA and LOCALAPPDATA (npm config get prefix = the private AppData\Roaming\npm) against installs laid out the way npm lays them out, with a fake npm first on PATH that logs its command and folder. main ran the wrong npm command in 6 of 9 cases (a global install under %APPDATA%\npm or ~\.npm-global run from home, and project installs run from a subfolder, from home, from the drive root or from another project), the fix ran the right one in 9 of 9; an npx cache run and the bundled runtime are detected the same by both. On main's code 14 of the 91 upgrade tests fail on assertions (12 new ones, and 2 existing ones that now also expect the project folder); all pass with the fix. Fixes #2477 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c326b6bec1 |
fix(go): a function value keeps its link past a local of the same name (#2776)
In Go, a function used as a value, like `apply(run)`, was recorded once
per function and name, at its first use. Since PR #2658 the resolver drops
a use that a local or parameter of that name shadows. When a function's
first use was the shadowed one and a later use, outside the local's scope,
was the package function, that later use had no reference left, and the
function lost its references edge to the package function (an inner-block
`run := 2`, a closure's `func(run int)`, a case clause, an if header or a
range variable, then `apply(run)`). Issue #2596 predicted this.
Fix, in both extractors (function-ref.ts with tree-sitter.ts's
flushFnRefCandidates, and the kernel's go.rs flush_fn_ref_candidates):
- A Go function that binds the name itself records one use per scope
instead of one per function. Bindings are a func literal's parameters,
:=, var, const, range and type-switch variables and a select case's
value. Scopes are Go's: a block, an if/for/switch/select statement's
implicit block and each case clause. Within one scope a local declared
before the first use also covers every later use, so the first use
still speaks for the scope.
- The function's own receiver, parameters and results keep a single
reference: they cover the whole body.
- A func literal at file scope is its own unit.
- Every other function is unchanged. The scope and enclosing function are
read from the tree only for names a function uses at least twice, after
the name gate (reading them for every container made kernel extraction
of prometheus 2.4x slower).
Validation:
- go-shadowed-function-value.test.ts: 14 cases, kernel and wasm, LF and
CRLF. On main's sources and kernel 10 fail; with the fix 14 pass.
Per-scope keys without the binding check, and dropping range
variables, case clauses or func literal parameters, each fail 3.
- Kernel parity, Go: gin 99/99, etcd 1065/1105 (40 deferred, as main),
prometheus 746/746, kubernetes 17779/17952 (173 deferred, as main),
0 files with diffs.
- A/B against
|
||
|
|
7dfef8451f |
fix(installer): Hermes keeps your MCP entry and gets no platform_toolsets line (#2774)
Root cause, two parts:
1. Install and refresh appended `mcp-codegraph` to
`platform_toolsets.cli` in `~/.hermes/config.yaml`. It was never
needed. Hermes loads every enabled MCP server on a platform whose list
names none (hermes_cli/tools_config.py `_merge_mcp_servers`, a rule
present back in May 2026 when the target was written). The name
itself is harmful:
- Hermes up to 0.22.2 rejects it at startup ("Unknown toolsets").
- Hermes 0.22.4 maps `mcp-<server>` onto the server, so the list
becomes an MCP allowlist and every other server disappears from CLI
sessions.
A user who deleted the line got it back on the next upgrade.
2. When the `mcp_servers.codegraph` block differed from the template, it
was replaced wholesale. That reverted an absolute command path, the
timeouts, `enabled: false` and `env`. The line-based YAML code also
missed some shapes:
- a codegraph key written on one line or quoted, which got a second
codegraph key;
- a 4-space-indented `mcp_servers`, which came out unparseable;
- a one-line `mcp_servers: {…}`, which got a second top-level key, and
Hermes's PyYAML reader then dropped the user's servers.
CRLF files came back LF.
Fix:
- CodeGraph never writes `platform_toolsets`.
- A `- mcp-codegraph` line an earlier install left in
`platform_toolsets.cli` is removed, or becomes `- codegraph` when the
list is an allowlist naming other MCP servers (the spelling every Hermes
version reads as the server). An allowlist that leaves CodeGraph out
gets a note, not an edit.
- An existing `mcp_servers.codegraph` entry is the user's: nothing in it
changes. A block entry missing `command` or `args` gets just those, at
its own indent.
- A new entry goes in at the indent its siblings use, and a codegraph key
is found however it is written. `mcp_servers: {}` becomes a block.
- A one-line mapping that holds other servers makes install refuse with a
clear error and change nothing. One that holds codegraph is left as
written, with a note, and uninstall says it can't remove it.
- The file keeps its line breaks, and is written only when something was
edited.
- Uninstall also removes a `- codegraph` the refresh may have written.
Hermes references (NousResearch/hermes-agent v0.22.4, 64dbae64):
- hermes_cli/tools_config.py `_get_platform_tools` / `_merge_mcp_servers`
("mcp-<server> names the same server as its bare alias: either spelling
selects only that server").
- hermes_cli/toolset_validation.py `saved_toolset_resolver`.
- website/docs/reference/mcp-config-reference.md (command, args, env,
enabled, timeout, connect_timeout).
- hermes_cli/tools_config.py at 1b9c539c (2026-05-17) for the May 2026
rule.
Validation:
- installer-targets.test.ts: 13 new Hermes tests. 3 existing tests that
asserted the `mcp-codegraph` write are updated; the #456 same-indent
list case now proves the old line is removed cleanly. With main's
hermes.ts put back, those 16 fail, all on behavior. The 7 other Hermes
tests pass on both.
- The real CLI on this Windows machine against a private HERMES_HOME:
`install --yes`, a second `install --yes`, `install --refresh` and
`uninstall --yes --keep-cli`, for 14 config.yaml shapes. Each result
was loaded with PyYAML (the reader Hermes uses), checked for duplicate
keys, and run through a re-implementation of the cited selection rule.
Of 42 results:
- main: 2 unparseable, 10 with duplicate keys, 26 carrying
`mcp-codegraph`, 10 where Hermes 0.22.4 would load only codegraph in
CLI sessions, 3 converted CRLF to LF, and the customized entry
reverted.
- The fix: 0, 0, 0, 0, 0, the customized entry kept byte-for-byte, and
1 install refused by design (a one-line `mcp_servers` holding other
servers).
Fixes #2485
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
5a6917b437 |
fix(c): a function whose return type is a macro call is indexed (#2773)
In C, a function declared with its return type written as a macro call,
like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or
libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node.
blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call
that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`.
Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside
redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is
often the return type. Blanked together with its argument, it left
`cJSON_Print(...)` and a block, which parse as a call statement. The
function was lost, the calls in its body went to the file, its locals
and parameters became file-level variables, and a header's prototype
read as a call from the header.
C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter
reads it as a macro_type_specifier, when all of these hold:
- it is in column 0;
- its one argument reads as a type;
- only a function declarator follows: a name, its parameter list,
`__attribute__` or ALL-CAPS attributes, then `{` or `;`;
- the line right above leaves no declaration open. jemalloc writes
`size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`.
C++ is unchanged.
Validation:
- New __tests__/c-macro-return-types.test.ts (3 tests: the issue's
reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT,
REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the
blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel
and with CODEGRAPH_KERNEL=0.
- Pre-parse output compared file by file on 12 repos. The only changes
are macro text kept where main blanked it; no file's parse-error count
got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1,
APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq,
redis (with jemalloc), sqlite and curl, and on the C++ controls fmt,
glm, json and abseil-cpp.
- A/B on main
|
||
|
|
0de94978fc |
fix(installer): the Codex table is found however its config.toml writes it (#2771)
Root cause: the TOML helpers behind Codex's install, refresh and uninstall
found CodeGraph's table with two literal searches, `[mcp_servers.codegraph]`
at the start of the file or right after a newline, and detection used a
plain substring check. Some valid ways of writing that table went unseen:
- an indented header, which formatters that indent sub-tables write;
- spaces inside the brackets or around the dot;
- quoted or escaped keys.
Install then appended a second `[mcp_servers.codegraph]`. A table defined
twice is invalid TOML, so Codex loaded none of its config. Install and
refresh also appended a table after a definition written with dotted keys
or as an inline table, or under an `mcp_servers` that is an inline table or
an array of tables (both invalid too). The literal search also matched
header-shaped text inside a multiline string and spliced into the string.
Uninstall left the user's original table, a `[mcp_servers.codegraph.env]`
sub-table (a server with no command), and dotted or inline definitions in
place. CRLF files came back with mixed line endings.
Fix: a small structure scan of the file finds every statement (table
header, array-of-tables header, key/value). It decodes key paths the way
TOML reads them (bare, "basic" with escapes, 'literal', spaces around
dots), tracks the table each key sits in, and folds the lines of a value
that spans several into the key that opened it. On top of that scan:
- The `[mcp_servers.codegraph]` table is found however its header line is
written. Install and refresh replace the first one in place and remove
any further copies, so a file the old bug broke parses again.
Sub-tables such as `.env` are kept.
- A block ends at its last key, so a comment heading the next table stays
with that table.
- Uninstall removes the table, every copy, its sub-tables, and
definitions written with dotted keys or as an inline table, as whole
statements.
- An entry the user wrote with dotted keys or as an inline table is left as
written, with a note.
- Where a table can't be added (`mcp_servers` set as an inline table, a
value or an array of tables), install refuses with a clear error and
changes nothing.
- An entry inside an inline `mcp_servers` table is reported by uninstall,
never edited. The uninstaller now prints such notes.
- Everything is written in the file's own line breaks, and the existing
block is compared regardless of them.
Validation:
- 25 new tests in installer-targets.test.ts:
- 20 on the TOML helpers: 8 header forms, CRLF, the old bug's duplicate,
look-alikes (other names, header-shaped text inside strings and
arrays), sub-tables, comments, 4 dotted/inline/array definitions, 2
shapes where a table can't be added, an entry inside an inline table;
- 5 on Codex install, re-install, refresh and uninstall end to end.
With main's toml.ts, codex.ts and installer index.ts put back, all 25
fail: 24 on the behavior itself, and one, a header with a trailing
comment, only on the new detection helper, since main already handled
that form. The 13 existing TOML tests pass on both.
- The real CLI on this Windows machine against private homes: `install
--yes`, a second `install --yes`, `install --refresh` and `uninstall
--yes --keep-cli`, for 23 config.toml shapes, each result parsed with
Python's strict tomllib.
- main (
|
||
|
|
6bd217a263 |
fix(cpp): code that calls operator++ by its name no longer stops an index or sync (#2769)
Indexing or syncing a C++ project stopped at "Resolving refs" with
"Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/:
Nothing to repeat" as soon as its code called an overloaded operator by
name, as `this->operator++();` inside `operator++(int)` does. Choosing a
method's overload reads the argument list after the name, and
cppParenListIn and cppOverloadParameters wrote the name into that
pattern with only `~` escaped. `operator++` did not compile. `operator()`
matched the name's own empty parentheses as the arguments, `operator[]`
never matched, and `operator+=` matched the next `operator=`. The same
helper serves every language that overloads by arity, so Scala `++`
overloads and Kotlin names in backticks that hold a `(` failed the same
way.
The fix adds one escape helper, escapeRegExp in src/utils.ts, the same
body as the private copies elsewhere. It is used at every place in src/
where a node name, a reference name, a piece of one, or a word read
from source goes into a RegExp unchecked: 114 interpolations in 13
files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and
the 2 overload readers only `~`. An AST pass over all 490 dynamic
RegExp constructions followed each interpolated value to its sources.
The 72 it still flags were each read by hand. They are
identifier-checked first, built from escaped names, constants, or the
CLI/MCP glob converters. The kernel's one dynamic regex already
escapes.
With operator arguments now read, nlohmann/json's `p->operator[](0)`
reached the argument-type scorer and picked `template <typename T>
operator[](T* key)`: a `size_type` parameter scored as an unknown
class, a `0` as a null pointer. `size_type` and `difference_type` now
count as numbers, as `size_t` does.
Validation:
- New __tests__/operator-method-names.test.ts (5 tests: init and sync of
an operator fixture, operator() and operator+= overload choice, the
json operator[](0) shape, Scala and Kotlin). With main's sources,
4 fail (3 throw "Nothing to repeat"); the json shape fails only with
size_type taken out of the number types. All pass on the kernel and
with CODEGRAPH_KERNEL=0.
- Census with a preload that logs every throwing RegExp: main threw
only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in
all 12 repos below.
- A/B on main
|
||
|
|
1ef8866aae |
test(explore): the cross-call dedup test passes with LF and CRLF checkouts alike (#2772)
explore-cross-call-dedup's "never re-sends a line it already sent" case has failed on every LF checkout (Linux, macOS, CI) since PR #2655. It passed on Windows only because core.autocrlf writes the fixture with CRLF. Root cause. Since PR #2655 the payroll fixture's RunResult struct (cycle.go lines 31-37) references Payslip through a field, so call 1 renders it. Call 2 renders the file's head, and that 7-line held run comes back inside it: dedupeRange keeps a covered run shorter than MIN_COVERED_LINES (8) in the emit set on purpose, rule 3 of the range algebra, which its own unit test pins. The handler case counted any repeated line as a duplicate. On a CRLF checkout explore's per-file character budget also counts each line's carriage return, so cycle.go rendered fewer lines and the case never met the short run. Fix, test only: - The case groups the repeated lines into consecutive runs and fails on a run of MIN_COVERED_LINES lines or more, read from EXPLORE_DEDUP. A shorter run is the algebra's deliberate re-send. What agents receive is unchanged. - The suite gives its copy of the fixture LF line endings, so every checkout indexes and renders the same bytes. Validation on Windows, with the kernel and with CODEGRAPH_KERNEL=0: - main's test fails with an LF fixture ("call 2 re-sent internal/usecase/payroll/cycle.go lines 31,32,33,34,35") and passes with the CRLF one. The new test passes 26/26 with both. - With the normalisation and main's assertion, a CRLF checkout fails the way an LF one does (lines 31-37): both checkouts now test one scenario. - Teeth: an algebra mutated to re-send held runs of 8-23 lines fails the new case ("call 2 re-sent internal/usecase/payroll/cycle.go lines 42-51 (10 lines)"). - tsc on the test file is clean. Contributor PR #2758 by @danusha2345 reached the same diagnosis. This change was written separately and also normalises the fixture. Fixes #2756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0abae11959 |
fix(installer): a symlinked instructions or config file is written through, not replaced (#2768)
Root cause: every installer write goes through one helper that writes
`<path>.tmp.<pid>` and renames it onto the path. A rename replaces a
symbolic link itself, not the file it points to. So a `~/.codex/AGENTS.md`
or `~/.claude.json` linked from a dotfiles repo became a detached regular
copy on install: the shared file never got CodeGraph's settings, and later
edits to it no longer reached the agent. This held for every target (Claude
Code, Codex, opencode, Cursor, Gemini, Antigravity, Kiro, Hermes and the
Copilot family) and for install, the refresh that `codegraph upgrade` runs,
and uninstall. Uninstall also deleted a link whose file it had emptied,
leaving CodeGraph's entry behind in the shared file.
Fix:
- The write helper resolves a link to the file at the end of its chain and
writes and renames there, so the link stays exactly as it was, relative
or absolute.
- A link to a file that doesn't exist yet gets that file created, but only
in a folder that already exists, as writing through it in a shell would.
No folder is ever created where a link points.
- A dangling chain is followed hop by hop from the real folder holding each
link, so a relative link under a linked folder lands where the OS would
put it.
- When the linked file can't be written (a read-only file, a folder that
doesn't exist), the error names the link and its target and nothing
changes.
- Where uninstall used to delete a user's file it had emptied (an
instructions file, Codex's config.toml, Copilot CLI's mcp-config.json,
the legacy project .claude.json), a link is kept and the emptied content
is written through it.
- The refresh sweep catches a failure per agent, reports it, still
refreshes the others, and exits non-zero, so the upgrade warns.
Decided: links to CodeGraph's own legacy files (Cursor's codegraph.mdc,
Kiro's steering doc), which install only ever deletes, are still removed.
Validation:
- 28 new tests. 25 are in installer-targets.test.ts:
- every target and location: install and uninstall keep each of its
files that is a link (18 cases);
- the shared files' content through install and uninstall for Claude
Code, Codex, opencode (two agents sharing one AGENTS.md) and Cursor;
- relative links under a linked folder;
- a link into a missing folder;
- a link to a read-only file;
- uninstall emptying through links;
- a failing link in a refresh;
- a folder link guard.
The other 3:
- 1 in upgrade.test.ts, where the refresh runs the built CLI;
- 2 in a new installer-links-simulated.test.ts, which simulates the
link with spied lstat/realpath/readlink, so it runs on machines that
can't create file symlinks.
With main's sources put back, 27 fail; the folder-link guard passes on
both. All pass with the fix. The real-link tests are skipped, with the
reason in their names, where file symlinks can't be created (Windows
without Developer Mode or admin).
- This Windows machine has Developer Mode on, so the real-link tests ran
for real.
- The real CLI against a private home (`install --yes`, `install
--refresh`, `uninstall --yes --keep-cli`), 10 linked files for five
agents plus relative, dangling and read-only links:
- on main, all 29 link checks found the link replaced by a copy;
- with the fix, none did, every link's text was unchanged, the shared
files got CodeGraph's settings and lost them again on uninstall, and
the missing-folder and read-only cases exited 1 with the new message
and changed nothing.
Fixes #2721
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d6e8c91e0a |
fix(installer): upgrade keeps everything around CodeGraph's section in instructions files (#2765)
Root cause: the write that refreshes CodeGraph's marker-fenced section in an agent's instructions file (CLAUDE.md, AGENTS.md, GEMINI.md) located the section with two independent searches: the first start marker anywhere in the file and the first end marker anywhere in the file. Everything between them was treated as CodeGraph's and replaced. A stray start marker above the real section (what is left when part of an older section is deleted by hand, or the marker quoted in the user's own notes) therefore stretched the "section" over the user's text, and when that marker was the first line the file came back as just the block, which is the report. The `codegraph upgrade` refresh (`codegraph install --refresh`) and plain `codegraph install` both go through this write, for the Claude Code, Codex, Gemini and opencode targets. The same pairing made a lone end marker look like "no section", so every run appended another block, and left a second block stale. Uninstall (and the npm preuninstall hook) and the Cursor rules-file cleanup used the same pairing to delete the same span. Fix: one parser finds the well-formed sections. Each end marker closes the nearest start marker before it, and a marker counts only when it is alone on its line, as every block codegraph has written puts it. A stray start or end marker is left as plain text. The refresh rewrites only the bytes between the first section's markers, using the line breaks that section already has, so a CRLF file stays CRLF and a current CRLF block is unchanged. Further sections are duplicates and are removed, keeping the text around them. With no section yet the block is appended after the existing bytes, which are no longer trimmed. Uninstall and the Cursor cleanup strip only well-formed sections. Validation: - 18 new tests (17 in installer-targets.test.ts: exact bytes for a stray start marker on line 1, a stray start marker with no section after it, a marker quoted in prose, a lone end marker, two sections, CRLF, no final newline, appending, a BOM, uninstall; a refresh plus uninstall per target for Claude Code, Codex, Gemini and opencode; the Cursor rules file. 1 in upgrade.test.ts: an npm upgrade whose refresh runs the built CLI against a private home). 16 fail on main's sources (the other 2 are guards that hold on both), all pass with the fix. - The real CLI on this Windows machine against private temp homes: `codegraph install --refresh`, 14 file shapes x 4 targets x 2 runs. Main had 28 bad results of 112 (the user's text lost in 3 shapes, a block added on every run after a lone end marker, a stale second block kept), the fix 0. With the fix, the bytes outside the section are identical in every single-section shape, and the input stays a byte prefix in every append. - `codegraph install --yes` and `codegraph uninstall --yes --keep-cli` on the reported shape: main kept none of the notes (install left only the 802-byte block, uninstall left 19 bytes); the fix kept them all. Fixes #2506 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
efff745783 |
fix(go): a field typed by a dot-imported package resolves in that package (#2764)
A Go type written without a package qualifier was looked up only in the
file's own package. In a file that dot-imports a project package, like
harbor's config tests (`import . ".../controller/config"`, then a
`controller Controller` field), the type isn't there, so the call
`c.controller.AllConfigs()` fell back to name matching. Name matching
took the same-named type of a package the file doesn't import, the one
that sorts first: matchGoFieldChainCall used the struct's own directory
for a bare field type, resolveGoMethodInPackage found no such type there,
and resolveMethodOnType picked the first of every
`Controller::AllConfigs`.
The same gap affected calls through parameters, locals and package-level
variables of such a type (one set by a conversion), through an alias of
it, and through methods promoted from an embedded field. It also affected
method values, type references, and the embedding edge itself where the
namesake was as near.
The field chain, the typed-receiver and method-value package lookup,
embedded types, alias targets, package-level values and Go type positions
now ask goBareTypeDir. It reads a bare name as Go does: the file's own
package first, then a project package the file dot-imports that declares
it. An exported name that only a dot import of a package outside the
project's modules can supply (`. "github.com/onsi/gomega"`) names none
of the project's types. The exception is a type that one of the file's
functions declares, which the index holds no node for: name matching
still handles it, as before. A bare embedding is retargeted only when a
dot import decides, and files without dot imports resolve exactly as
before.
Tests: go-dot-import-field-type.test.ts has 10 cases on both the kernel
and wasm backends. 18 tests fail on main and 2 guards pass. All
go-*, goframe, gin-*, frameworks*, resolution*, sync* and dead-code test
files pass (65 files, 1,140 tests). tsc and the build are clean.
Validation compares main
|
||
|
|
bf6f1fa67e |
fix(dart): a typedef references the types it aliases (#2763)
In
class Report { final int score; const Report(this.score); }
typedef ReportCallback = void Function(Report r);
`codegraph callers Report` did not list ReportCallback. Both extractors
minted the typedef's node and nothing else. tree-sitter-dart gives
`type_alias` no fields, so extractTypeAlias found no `value` to read and
had no Dart branch, and the kernel's extract_type_alias mirrored that.
Impact on a model type missed every callback or collection type built
on it. The legacy form with a return type, `typedef Report
LegacyMaker(int n);`, was also named after that return type: with no
`name` field the name fell back to the first type name in the
declaration, so a `type_alias Report` (or `Widget`, `Future`, `bool`)
stood beside the real type and took references meant for it.
Both extractors now name a typedef by the last type name before its
`=`, its type parameters or its parameter list, and add a `references`
ref from the typedef, on each type's name, for every type its aliased
type names: a function type's return and parameter types, generic
arguments, record fields, a type parameter's bound, and the legacy
form's return and parameter types. Annotations are not part of it, an
import prefix names no type, and a type parameter (the typedef's own,
or one a generic function type or an old-style generic function-typed
parameter declares) is skipped unless written through a prefix. Kernel
and wasm output are identical; a new parity fixture,
TortureTypedefs.dart, pins every form in LF and CRLF.
Validation, kernel path, arms built from
|
||
|
|
dbb6aa9ba9 |
fix(vbnet): link reads through a namespace or a bare member name, and run the parameterless overload (#2762)
Three kinds of VB.NET member read that #2355 and #2377 left unlinked: - A method named without parentheses (`Me.Load`, `Calc.GetTargetDAR`, a constructor's `Me.New` / `MyBase.New`) went to the first overload declared, so `Me.Load` reached `Load(path)` instead of `Load(Optional force = False)`, and a `Me.New` whose first overload was the calling constructor itself linked nothing. - A read whose first name is a namespace (`Billing.Invoice.MaxLines`, `StaxRip.UI.ButtonEx.ButtonSymbol.Delete` with the root namespace in front, a Module's `RedGifs.RedGifsSiteKey`) linked nothing: the receiver was looked up as a value or a type only. - A field, property or Module variable named bare before the dot (`Button1.Text`, `Settings.Plugins`, staxrip's global `p.TargetWidth`) was not counted as read: a bare name is no member access, so only what is read through it could link, and nothing when that is .NET's (`Text`). Fix (src/resolution/vbnet-receivers.ts; resolution only, extraction output unchanged): - A method a reference runs without parentheses resolves among its type's overloads to one that takes no arguments (none, or only Optional and ParamArray parameters, read from the declaration), never the member the reference is in. AddressOf / NameOf keep the first overload, as before. - A receiver name that is neither a value nor a type is looked up as a namespace the way VB.NET does: nested in a namespace around the read, outward to the root namespaces, then in the file's and project's Imports. Each further link is a type declared in it, a nested namespace, or a member of one of its Modules. The read links the type (`Billing.Invoice`) or the member (`Assets.Logo`), as #2355's `Outer.Mode` reads do. - A bare name before the dot that VB.NET binds to a field, property or Module variable (not a local or parameter, which hide it) is linked as a read alongside what is read through it. Not for a `With` block's `.Text`, a method run through the name (the call is the method's alone, as with parentheses), another project's Module, or a member declared with an outside type of its own name (`Responser As Responser`), whose Shared members and nested types VB.NET reads through the type; a project type of its own name follows #2377's Color Color rule. - membersNamed looked a type's members up in every project's same-named type and memoised the list by qualified name alone, so SCrawler's two plugin `UserData` classes shared one list ordered by whichever was asked first. It now keeps to the type's own project. Alone this leaves main's graph unchanged on SCrawler and staxrip; without it, 17 of the new bare-name reads in one plugin linked the other plugin's members. Dropped from the 2026-10-04 test set: a method named through its class also linking the class (#2355 treats it as a call, and its test pins that); `AppSession.SessionId` through a property typed OtherSession linking nothing (#2377 links OtherSession's field, pinned the same way); `SCrawler.My.Resources...` reads (#2355/#2377 send no path that starts with `My` or `Global`); and same-named classes picked by namespace, Imports and project, which main already does once the fixture declares its RootNamespace. Verification: new vbnet-member-access-scope.test.ts (9 tests) fails 6/9 on main and passes; a mutation that restores main's membersNamed fails the own-project test. The other VB.NET, C#, extraction, resolution, sync and framework suites pass; tsc and build clean. Validation, main vs this branch (indexes from each arm's own build; nodes unchanged; self-loops unchanged; serilog and gin dumps byte-identical): - SCrawler 31,201 -> 36,378 edges. 1 removed: `COLOR_PICKER.ColorsGet` re-pointed from the two-argument overload to `ColorsGet() As DataColor`. Added: 4,973 bare-name reads (3,731 own class, 957 Module variables, 285 inherited), 192 namespace reads, 12 reads through a Module member reached by namespace, and that 1 overload. - staxrip 70,864 -> 83,909 edges, 0 removed. Added: 12,983 bare-name reads (8,991 own class, 3,820 Module variables, 172 inherited), 61 reads with the root namespace in front, and `box.Show` reaching `Function Show() As DialogResult` from its Shared overload. - Checked against source: every bare-name read's name starts the access, has no local of that name, and binds to its class, an ancestor (23 through a qualified `Inherits Instagram.UserData` the graph's own extends edge misses) or a Module of its project; every namespace read's path ends its target's full name and the target's project is the reader's or one it references; no edge crosses between staxrip and its AutoCrop tool; 48 sampled edges hand-checked, all correct. - Cost (5 interleaved rounds, medians, quiet machine): SCrawler wall 5.60 -> 5.75 s, CPU 19.5 -> 20.3 s, DB 43.4 -> 45.1 MB; staxrip wall 7.23 -> 7.11 s (noise), CPU 25.7 -> 26.3 s, DB 59.0 -> 64.4 MB, the growth being the new edges (unresolved refs fall 47,074 -> 43,607 and 38,051 -> 36,165). Not covered: a call through a bare name (`Button1.Focus()`) or a function named before the dot, reads in a `With Button1` block, and `My.` / `Global.` paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b5d1d543d |
chore(scripts): regenerate the Go standard-library method table from Go's source (#2760)
go-stdlib-methods.ts lists the methods of 130 standard-library types with their parameter and result counts. Its generator lived in a validation kit. scripts/gen-go-stdlib-methods.mjs reads a Go source tree with the engine's Go grammar, counts with the engine's own goSignatureArity, and prints (or --write's) the table for the types it lists, so a later Go release's methods are one command away. Against go1.27.3 it reproduces the table byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
847f1df77d |
fix(go): an interface or struct embedding a standard-library type counts its methods (#2759)
goImplementsEdges (#584) follows declared embeddings since #2402, but an embedding of a type outside the index resolves to nothing, so it added no methods: prometheus's TBRun (testing.TB plus Run) went to a struct with only a Run, QueryLogger (slog.Handler plus io.Closer) had no implementations, kapacitor's Diagnostic (a third-party Logger plus Log) took every struct with a Log, and the predeclared `error` was not recorded at all. - go-stdlib-methods.ts lists the methods of 130 standard-library types, each as `name:params/results`, generated from go1.27.3's source and counted the way goSignatureArity counts a stored signature. - Both extractors record an embedded `error`, which stays unresolved. The predeclared-type set is unchanged, so alias and defined-type references still skip it. - goImplementsEdges reads each Go type's failed `extends` rows and reads the package back off the line (goRefQualification, now exported). A standard-library type's methods join the method set, keyed as goMethodKey keys the project's (`std:<import path>` for an unexported one), and the arity gate holds them to the table's counts on both sides: a `sync.Mutex` makes a struct hugo's `Locker`, not etcd's gRPC `LockClient`. Any other outside type is met by a type embedding the same one, or by the only type with every other method the interface names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1f3a274c53 |
fix(go): a method a type declares in a file the index couldn't read is not its embedded type's (#2757)
kubernetes' core/v1 generated.pb.go is 1.7 MB, over the index's 1 MB limit, so every API type's own String, Marshal, Unmarshal, Size and MarshalToSizedBuffer is missing from the graph. Resolving `pod.String()` on a `*v1.Pod`, or `m.Template.Size()` on a `v1.PodTemplateSpec`, the walk over embedded types found the method the embedded TypeMeta or ObjectMeta has and linked that, a method the call never runs. resolveGoMethodInPackage, which every typed Go receiver goes through (parameters, locals, fields, assertions, package-level values, indexed elements), now reads the method declarations of the files of the type's package that the index couldn't read, from their text, and links nothing when the type declares the method there. matchMethodCall's typed branch and matchGoPackageValueCall keep that null instead of falling through to name matching, which found the same promoted method. A method the type really gets from an embedded type, like GetName from ObjectMeta, still links. The check that #2755 added for indexed elements moves into the shared path. On kubernetes 4c05e667, 63 calls lose that link and nothing else changes; etcd, prometheus, express and flask are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a5fe6e1a27 |
fix(go): a call on an indexed element reaches its element type's method (#2755)
A Go call on an indexed element (kubectl's `schemas[ix].ValidateBytes(data)`, deepcopy-gen's `(*in)[i].DeepCopyInto(&(*out)[i])`, go-to-protobuf's `m.Items[len(m.Items)-1].Unmarshal(...)`) reaches the resolver by its bare name, at the column where the receiver starts, and nothing typed the receiver: a method one type declares was guessed by name, often onto a namesake, and a common one like `DeepCopyInto` got nothing. matchGoIndexedCall, an early hook beside matchGoAssertedCall, reads the receiver chain with goChainLinks and types it as Go does: the head from the declaration in scope (goLocalDecl; readGoScopes now records where each `:=` or `var` value starts and which name it is) or a package-level variable; values (composite literals, make, new, append, conversions, fields, elements, addresses, range elements); struct fields, promoted ones too; and the element type of a slice, array, array pointer, map or named slice type, with `(*p)` dereferenced. The method is the element type's own, its interface's or one embedding promotes, and a type parameter's comes from its constraint. An element type from outside the project, a predeclared one, a type literal or an outside alias links nothing, and so does a method the type declares in a file of its package the index couldn't read (kubernetes' core/v1 generated.pb.go), which is read from that file's text. What can't be typed (a call's result) is left to the other strategies. On kubernetes 4c05e667 the indexed-element calls go from 760 resolved and 2,284 unresolved to 2,666 resolved and 378 unresolved; every changed site in kubernetes, prometheus and etcd is such a call, nodes are identical, and the express and flask controls are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
553d5d101d |
fix(sync): an import a name match took follows its module once it resolves another way (#2752)
A named import that a name match linked to a declaration elsewhere while
its module did not export the name kept that link after an edit gave the
module the name. A barrel's `export { EmptyState } from './EmptyState'`
declares nothing, so no definition delta, failed-ref retry or barrel walk
saw it (the "retargets" PR #2744 left). A name match into the module
itself, a class exported only as its default and imported by name, kept
its link after the default was dropped, where a fresh index finds nothing.
reopenNameMatchedImports (module-importers.ts): for each module the sync
modified, its importers' binding edges that resolved some other way than
their import (getImportBindingsResolvedElsewhere, an indexed range per
importer), whose import statement names this module, are resolved again
against the graph as it is (ReferenceResolver.resolveNow, writing
nothing). When the answer differs, the importer's edges through the name
and into the members of what the name match took go back for the sweep.
Regression test sync-name-matched-imports.test.ts: 4 cases comparing the
synced graph with a fresh index of the final tree. Fresh indexes are
byte-identical on 8 repos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
bbf987e13b |
fix(explore): the named symbols take turns in the dynamic-dispatch links list (#2751)
Links of the same rank were listed in edge order, so one named symbol's long fan-out could fill the 6-line list before another named symbol's first link. prometheus's `refresh.Discovery.refresh` hands its func field to 28 discovery packages, and in `refresh.Discovery.refresh hetzner.refresher.refresh hcloudDiscovery.refresh` those links filled the list before `refresher.refresh` → `robotDiscovery.refresh`. Each link now has a turn: how many links of its rank the named symbol it hangs from already has. Links sort by rank as before, then by turn, then by edge order, so links of the same rank alternate between the named symbols. A token that names two interfaces gets both: prometheus's `Appender.Append` matches storage.Appender and chunkenc.Appender, and storage's implementations no longer wait behind six of chunkenc's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f5dfa21fef |
fix(explore): a symbol written with its package resolves, like refresh.Discovery.refresh (#2750)
Explore's token resolution, codegraph_node's lookup and the callers, callees and impact lookups dropped every Go symbol written the way Go code names it: `refresh.Discovery.refresh`, `storage.Queryable.Querier` and `gin.Context.Next` resolved to nothing, and so did `binding.Default`. Three gaps: - A qualified name was matched against the qualifiedName, which holds the type and not the package, or entirely against the path, which holds the package and not the type, never as a package followed by a type. A new matcher stage reads `pkg.Type.method` as leading qualifiers that name the package by path segment and trailing ones that end the node's scope. A Python `pkg.mod.Class.method` and a Rust `module::Type::method` read the same way. - A module's root package has no directory: gin's `context.go` is `package gin`. A Go package qualifier now also matches the package the file declares, read from its `package` clause (src/graph/go-package.ts). That covers versioned `v2/` directories and generated packages like prometheus's `io_prometheus_client` as well. The clause is read last and only where it can decide, a top-level function or a node whose scope already ends with the written type, so a common name's hundreds of definitions cost no file reads. - Explore and codegraph_node took a qualified token's matches from the full-text search's top 50 only, and for gin's `binding.Default` those were three `binding/default_validator*.go` files. Through one shared helper they now fall back to the exact-name index when the search has no match; the CLI's lookup already started there. The new stage runs only after the existing ones fail, and the search keeps its order, so a name that resolved before keeps its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fc293f0081 |
fix(sync): a default import follows the module's default when an edit moves it (#2749)
An edit that changes which declaration a module exports as its default
(`export default A` becoming `export default B` while A stays, or the
default dropped) left every reference through a default import of it on
the old declaration. A sync moves other files' edges into a changed file
onto the node that replaces each target, and an `export default` statement
is no node, so no definition delta, failed-ref retry or importer walk saw
the change. A fresh index links the new default, or nothing.
- ReferenceResolver.changedDefaultExports: for each modified JS/TS module
that unchanged files import by default or as a namespace (an imports edge
into its file node named for the binding), compare what the whole-name
references through those bindings resolved to with the module's default
now (moduleDefaultExport, the lookup resolveViaImport makes). No file is
read; a module forwarding its default samples 8 importers' edges.
- reopenReferencesThroughChangedDefaults (module-importers.ts): when one
moved, the edges bound before this sync resolved anything go back for
the sweep: through every binding that reads the default (importersOf
seeded with `default`: default imports, { default as Y },
require('./x').default, modules passing it on), the routers'
`layout:<binding>` references, and into the old default's own members
(typed receivers, inherited members).
Regression test sync-default-export-change.test.ts: 6 cases comparing the
synced graph with a fresh index of the final tree; all fail on main.
Fresh indexes are byte-identical on 8 repos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
3cfd24a46d |
fix(explore): every dynamic-dispatch link gets its own line, the ones the query names first (#2748)
codegraph_explore's "Dynamic-dispatch links among your symbols" section deduped its lines by `source name > target name`. Every interface → implementation link of one method reads the same way (`TrySet → TrySet`), so an interface method with several implementations printed one line, and which implementation it kept was edge order, not the query. On gin, `tryToSetValue setter.TrySet multipartRequest.TrySet setByForm` printed only `TrySet → TrySet @binding/header.go:35` and hid formSource's link. Lines are now deduped by the edge's endpoints (node ids), and the 6-line cap stays. When more links exist than fit: - a link that reaches a test or example file through an end the query didn't name goes last, as explore's answer leaves those files out; the predicate is explore's own `isLowValue`, moved to module scope so both uses share it; - otherwise links go by how pointedly the query named their two ends. A symbol a token picks out (at most three definitions, the same notion as the boundary scan's agent-specific symbols) or a named non-callable endpoint weighs 3, a candidate of an ambiguous token 1, an unnamed symbol 0. An implementation the query names always comes before one it doesn't. The weights also keep an ambiguous token's candidates from crowding out a precise one: prometheus's `Discovery.refresh` matches a `Discovery` in each discovery package, and in `Discovery.refresh fetchApps Decoder.Decode` their links to each other filled the cap and hid the one link into `fetchApps`; - edge order breaks ties. The section takes the rendered Flow chain itself. It leaves out an edge between two of the chain's nodes, as before, and also a line that reads the same as one of the chain's synthesized hops or as a line already listed. gin declares its `Binding` and `StructValidator` interfaces once per build tag (binding.go, binding_nomsgpack.go), so the twin declaration's link printed exactly like the Flow's hop. A same-name link names each end by its owner, `setter.TrySet → formSource.TrySet`, the way an agent writes it in a query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e450ae3a63 |
fix(go): an unexported interface method is satisfied only from its own package (#2746)
An unexported Go method name belongs to the package that declares it.
goImplementsEdges matched method names only, so an interface asking for
an unexported method was linked to every type in any package with a
method of that name: each of prometheus's `refresher { refresh(...) }`
interfaces (one per discovery package) went to about 37 providers in
other packages, such as discovery/aws.EC2Discovery, none of which Go
lets satisfy it.
Method sets now hold an unexported name together with the directory of
the file that declares the method (goMethodKey), on both sides. Where
the method is declared is what counts, not where the type is: etcd's
gRPC servers outside api/etcdserverpb keep their KVServer edges,
because the UnsafeKVServer or UnimplementedKVServer they embed declares
mustEmbedUnimplementedKVServer in the interface's package.
The interface-dispatch bridge uses the same key, so a call through the
interface reaches the method of the interface's package: not the
type's own namesake from another package, and not one Go's selector
would skip at a shallower embedding depth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
6e24711590 |
fix(go): a call through a func-typed struct field reaches the functions put in it (#2745)
A Go struct can hold a function in a field and call it through the field. Prometheus' service discovery runs every provider that way: refresh.Discovery calls d.refreshf(ctx), NewDiscovery copies refreshf from Options.RefreshF, and each provider passes its own method (RefreshF: d.refresh). Nothing answered d.refreshf, so a flow from Discovery.Run stopped at the field; #2430 rightly dropped the interface-impl edges that had stood in for it. goFuncFieldEdges, a Go-only synthesis pass, links a call x.F(...) through a func field of a struct to each function or method value put in that field: by a composite literal (keyed or positional, found through its instantiates edge), by an assignment x.F = v, or by one level of field-to-field copy (refreshf: opts.RefreshF). A value is what the resolver already linked it to, else x's method m for x.m, an interface's method for an interface value. Receivers are typed through the resolver's own Go scope reader (goLocalDeclarationAt over goLocalDecl, #2706), or by a local's composite literal, new(T), type assertion or resolved call result. Channels are keyed by package, struct and field, so a generic field name never crosses between unrelated structs; one package's test wiring links no other package's test; an edge cites a function's production wiring; generated files are not read. Edges are calls, provenance heuristic, synthesizedBy go-func-field, with field, via (package-qualified when the copied field is another package's) and registeredAt; explore, the node trail and ContextBuilder read them as "func field `Discovery.refreshf` (set from `Options.RefreshF`)". The pass sits after the Go framework passes, with a pool cost hint, and every Go file now counts as a synthesis input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e24827011e |
fix(sync): a name imported through a barrel links when an edit gives the barrel its re-export (#2744)
Sync and the file watcher now link `import { useInvocation } from './context'`,
and every use of the name, when an edit gives the barrel context/index.ts
that re-export after the importing file was indexed. Before, the import,
its calls and its JSX or template tags stayed unlinked until the importing
file changed or the project was re-indexed.
The barrel declares none of the names it passes on, so sync's retry by name
never reached the importers' failed references. The edit adds no file and
changes no definition, so neither the importer re-open through added files
(PR #2512) nor the barrel walks (PR #2560, PR #2665) run.
importBindingTail now parks the whole name of a plain named import under
its module's key (`module:context`), as PR #2450 does for default, namespace
and aliased bindings and PR #2507 for Python's unaliased imports. It does so
only while the import resolves to a project file that carries that key and
that module doesn't give the name yet; the by-name lookup still finds the
reference by its own name.
Three versions were measured on 18 repos, and all fix the same barrel-edit
cases. This narrowest one parks 2,727 references on a fresh index, against
11,061 for parking every plain named import, and keeps the retries per
changed file within +1% to +3% of main on folo, vben and milo-ui. Fresh
indexes are identical in nodes, edges and files in all 18 repos, and on
vben's gut scenario sync's difference from a fresh index falls from 2,337
lines to 194.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
51be28d15b |
fix(extraction): a function in an initializer is referenced once, by what the initializer declares (#2743)
PRs #693 and #1802 walk a declaration's initializer under the symbol it declares, so `const routes = { home }` references `home` from `routes`. The dispatcher's declaration-wide function-as-value scan then took the same values again for the enclosing file (or class). Flush-time dedup keys on the source, so both stayed: a second reference to the same function from the same spot. Each initializer walk now records what it walked, and the scan skips it. PR #2738 added that mechanism for component wrappers, and C#, Scala and Dart already had their own. The newly covered paths: - TS/JS module-level constants and variables, owned object literals included (#2300), and TS/JS class properties; - Python assignments, Go var/const specs, Rust const/static items; - Java fields, and Kotlin properties (walkInitializer takes the property's id, so their walk is unchanged). Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin. References no initializer walk takes are unchanged: a module-level call argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler` (the declaration's own value, which the scan still reads). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ae0fc631e0 |
fix(js,ts): a callee that names nothing records no reference, and a wrapped callee is the plain call (#2742)
A TS/JS call whose callee was no name and no member access (an IIFE, a curried `connect(mapState)(View)`, `handlers[type]()`, `(0, fn)()`, a tagged template on a call) stored the callee's whole source text as the reference's name. Nothing can answer such a name, so each one parked as a failed reference, an IIFE's entire body among them. extractCall now reads the callee past the wrappers that keep it the same callee (parentheses, `!`, `as` / `satisfies`, `await`, type arguments) and records nothing when that is not a name or a dotted chain; the calls inside are visited on their own. Mirrored in the kernel (tsjs/extractors.rs callee_name_of). The reference of a wrapped callee sits at its name, where a plain call's does, and the resolver's bare-call check reads `fn!(`, `(fn)(`, `(fn as F)(` and `(fn satisfies F)(` as it reads `fn(`: SigNoz's `(callback as FrameRequestCallback)(0)` and `(callback)(0)` name the arrow's own parameter, and from the `(` they bound a generated interface's `callback` property. tree-sitter-typescript parses a prefix operator over a generic call as it parses `await f<T>()` (#2586): `!settings.get<boolean>('X')` and `void apiRequest<T>()` are calls whose callee is `!settings.get` / `void apiRequest`. The callee peel that handled `await` now peels a prefix operator too (peelTsPrefixedCallee; the kernel's peel_await_callee), so the call is to `settings.get`, and `void apiRequest<T>().then()` encodes as `apiRequest().then`, not `voidapiRequest().then`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa01692620 |
fix(js,ts): an import from the app's root maps through the app's own tsconfig paths (#2740)
bigbluebutton's client imports nearly everything from its root,
`import Icon from '/imports/ui/components/common/icon/component'`, which its
tsconfig maps with `"paths": { "/*": ["*"] }`. tsc maps a rooted name through
`paths` (only `./` and `../` count as relative there) and never reads it
from `baseUrl`. ownConfigCandidates refused every specifier starting with
`/`, so all 2,375 of these imports stayed unresolved and every name they
import linked by bare-name matching, often to the wrong one of several
same-named components.
A rooted specifier now goes through the nearest config's `paths` patterns
only; `baseUrl` is still never applied to it, and a protocol-relative
`//host/x` stays out. On bigbluebutton the unresolved refs fall from 26,189
to 22,565 and its rooted imports from 2,375 unresolved to 1 (a module that
isn't in the repository); 13 control repos are byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ada1445458 |
fix(ts): a qualified type's last name is a member of what its qualifier names, not any declaration of that name (#2739)
Both extractors record the last name of a qualified type alone, so
`React.ReactNode` reached name matching as `ReactNode` and bound by word:
outline's 197 `React.ReactNode` annotations linked to its own editor
`class ReactNode`, excalidraw's `TChat.ChatMessage` to a `ChatMessage`
component, signoz's `EQueryType.CLICKHOUSE` to another enum's member, and
outline's `UnfurlResourceType.Group` to the `Group` model the file imports.
PR #2736 left that name to name matching on purpose.
A TypeScript `references` ref written after a qualifier now resolves
through the qualifier, ahead of the framework, import and name strategies:
- an import: the module's export (`import * as T`, through barrels and the
repository's own `declare module` blocks), or the member of the enum,
class or namespace a named or default import binds;
- a declaration of the file's own, in the innermost namespace around the
reference that declares it, else at the top level: an enum's or a
class's `::` member, or a member of a namespace block;
- otherwise a namespace the project declares global (`Express.User` in a
`declare global` block).
A package from outside the repository, which no manifest of the importing
file's declares as its own and no `declare module` block declares, and a
global the project doesn't declare, link nothing. An import this resolver
can't follow but no manifest declares as a dependency (zod's own tests'
`import * as z from "zod/v3"`, through the package's `exports`), an
`import X = ns.Y` alias, and a name written after an expression
(`import('./x').T`) keep the old strategies.
The index holds no node for a TypeScript namespace, and its members are
indexed by their bare names, so ambient-modules gains scanNamespaceBlocks:
every `namespace A.B { … }` block with its full name and whether it is
global, read from the same tokens as the module blocks.
Validated on 18 TypeScript repos (13 React apps plus zod, vben, mealie,
prometheus's web UI and fastapi-template), main
|
||
|
|
2c4a16d091 |
fix(react): a wrapper component renders the component it wraps, and owns the calls in its styles (#2738)
A component declared through a wrapper with no render function of its own
(`styled.div`…``, `styled(ActionButton).attrs(…)<Props>`…``, MUI's
`styled(Box)(({ theme }) => …)`, `memo(Button)`) was a bodyless component
node: nothing walked its initializer, so the calls in its styles went
nowhere, and nothing linked it to the component it wraps, so a render flow
stopped at it. On outline, explore printed no Flow for
"OverflowMenuButton NudeButton ActionButton performAction".
Extraction (TS and the native kernel) now walks the initializer with the
component on the stack: the template's interpolations, the .attrs() /
.withConfig() / options arguments, a style function and a memo comparator
are the component's code. The wrapper's own chain (styled, styled.div,
.attrs, memo) names the library and is not recorded; a typed tag's type
operands are not walked; and the wrapped component is no function-as-value
reference, from the component or from the file (the declaration-wide scan
skips the walked initializer). A component is also a value-ref reader, as
a function is.
The jsx-render synthesizer links a wrapper to the component it wraps
(`wrappedComponentName` in frameworks/react.ts, beside `wrappedName`),
bound as a tag of that name in the file would be, but only where the file
binds the name by a declaration or an import of a project module, so a
package's styled(Link), a Meteor /imports/… path and a destructured name
render nothing rather than a namesake. Files that declare such wrappers are
synthesis inputs, so a sync redraws the link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7b9c356dd7 |
fix(react): a type annotation in a .tsx file names a type, never a same-named component (#2737)
React's Pattern 1 ran its component lookup on every PascalCase name in a
.tsx or .jsx file, type annotations included, and its candidate could
outrank name matching, so an annotation linked to a component: outline's
InputSelect.tsx declares `type Option`, and its annotations went to the
`Option` component of Export/ExportDialog.tsx; mastodon's `<Item>` type
parameters went to the `Item` class of media_gallery.jsx, and bulletproof's
`TableProps<Entry>` to another app's `function Entry`.
A type annotation is now name matching's, which scopes a type as TypeScript
does since PR #2736: the file's own, an import's or a global one. Pattern 1
decides only a type alias or interface the file declares beside a value or
a component of the same name, both of them the file's own: the annotation is
the type. That covers bigbluebutton's `type ModalRegistration` beside its
render-prop component, mastodon's `type ShortNumberRenderer` and `interface
AccountList` beside their functions, and a type beside a value that PR #2420
reads through its wrapper or loader. Every other reference keeps PR #2420's
order: a value's loaded or wrapped component, then a function, class or
component the file declares, then name matching for any other declaration
of the file's, and the component lookup only for a name the file doesn't
declare.
Validated on 13 React repos, main
|
||
|
|
458ad51921 |
fix(ts): a type a module doesn't import is never another module's (#2736)
A TypeScript module reaches another module's type only through an import, so a type it names without importing it is its own or a global one: a script's top-level declaration or one in a declare global block. #2726 held a supertype to that. A type annotation, a type argument and a type alias's value went to the name matcher like any other name, which handed them a project type of that name: vscode's Omit<...> annotations reached quickInput.ts's export type Omit instead of TypeScript's own, and type parameters and mapped keys, like excalidraw's <Point extends ...> and frontend's <T>, reached other modules' types and enum members. Both name-match strategies now leave out another module's non-global type for a TS or JS module's type reference to a name the file doesn't import, ahead of the ranking, so a declare global type still wins over a nearer module's namesake. Left as they were: the last name of a qualified type, vscode.TextDocument, written after a dot; a name inside a declare module 'x' block, which is module x's own; and a name an import X = ns.X alias binds, which the file's import list doesn't hold. Globals come from declaration files and declare global blocks. A script .ts's top level is a sample's or a fixture's, global to its own program only (vscode's colorize fixtures declare type T), and a test's or fixture's globals reach its tests only, as its declare module blocks do. That holds for supertypes too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0c28480576 |
fix(cpp): -> on a value of one of the project's classes follows its operator->, like rocksdb's FileSystemPtr and MultiGetContext::Range::Iterator (#2735)
C++ reads `x->m` on a class value as `x.operator->()->m`, and applies `->` again to a class it returns. Only a library holder's `->` and a container iterator's (`std::vector<T>::iterator`) were read. A call through a project iterator or smart-pointer wrapper was guessed by the method's name: - rocksdb's `fs_->NewWritableFile(…)` on a `FileSystemPtr`, whose `operator->` returns a `std::shared_ptr<FileSystem>`, reached `FaultInjectionTestFS`; - `writable_file_->Truncate(…)` on an `FSWritableFilePtr` reached the legacy `WritableFile::Truncate`; - `iter->lkey->user_key()` through `MultiGetContext::Range::Iterator`, whose `KeyContext* operator->()` is a `KeyContext*`, reached `BlobCountingIterator::user_key`. Both a member chain's `->` and a `->` call on a declared receiver end in cppIteratorTarget. For a type that is no container's iterator, it now falls back to cppOperatorTarget: - what the class's `operator->` returns, through the pointer it is, or with `->` applied again to a class it is: a library holder, or another of these; - else what its `operator*` returns; - the return type is read where the class declares it, and the class's own template parameters are bound to the type's written arguments (cppTemplateBound): `UnownedPtr<DB>`'s `T* operator->()` is a `DB*`; - a parameter the type doesn't bind names no particular class. A log-only census on main |
||
|
|
4e35e0cb78 |
test(python): the module-resolution tests remove their temp dir after indexing (#2734)
python-module-resolution.test.ts removed a test's temp dir only when the test never opened a CodeGraph (`if (cg) cg.destroy(); else if ...`), and destroy() closes the database without deleting anything. So every test that indexed left its codegraph-pymod-* dir behind, 47 per full run, passing or not, since PR #1921 added the file. The dev box's %TEMP% held 1,739 of them by 2026-10-21. The dir is now removed after destroy(), as the other test files do. Checked with a private TEMP: main's file leaves 47 codegraph-pymod-* dirs, this one none, and its 50 tests pass both ways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bdf288a76f |
fix(cpp): a pimpl's Rep, which a header names and its source file defines, is found for the code in that file (#2733)
A C++ name is looked up from where it is written, and a class that a source file defines belongs to that file's translation unit, so only that file sees it (#2538). A header that declares `struct Rep;` and `std::unique_ptr<Rep> rep_;` writes the name for a `Table::Rep` that its source file defines, and code in that file reads the member through it. The lookup ran from the header, saw no source file's classes and found nothing. The call was then guessed by the method's name, often onto the calling class's own method: - leveldb's `rep_->index_block->NewIterator(…)` reached `Table::NewIterator`; - rocksdb's `rep->write_batch.GetDataSize()` reached `WriteBatchWithIndex::GetDataSize`; - rocksdb's `level_iters_[i]->status()` reached `ForwardIterator::status`. cppClassWritten's lookup now also sees the classes of the file the reference is in, the translation unit that includes the header (visibleFrom(file, unit)). classNode filters by that same visibility instead of recomputing the header's. A member-chain diagnostic on main |
||
|
|
73f68e188c |
fix(ts): a call to a parameter of any type links nothing elsewhere (#2732)
A JS/TS name the calling function binds as a parameter is that parameter, never a same-named function in another file, as the file's own were made in #2226. The parameter reader took a type or default to be anything up to a comma or a parenthesis, so a parameter typed as a function, defaulted to one, or after one was never seen: bitwarden's isAvailable(f) on a callback typed (f: string) => boolean linked to the napi isAvailable, and vscode's merge editor's compare(a, b) on a comparator parameter to strings.ts's. A type or default may now hold balanced parentheses, braces, brackets and generics, a parameter property's modifiers are read, and an earlier destructured parameter is passed over; each alternative still has one parse. A name ends where no identifier character follows, so a const syncState$ no longer declares syncState. The parameter also shadows the file's own import of the name, which the import resolver answered before the check ran, but only where it is in scope: in the body of the function, arrow or catch clause that binds it, or the block or for statement of a const, let, var, function or class, read from the function's text up to the reference. A lone arrow parameter counts, and an object type a function returns is no body. A Promise executor's resolve is the calls inside it, never a resolve(file) after it, and a const X = require('./x').default is the import itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5994edb34e |
fix(cpp): a member declared in an anonymous union, like protobuf's union { Impl_ _impl_; };, is found as a member of the class around it (#2731)
A class's data members are read from its own lines, skipping those of
the functions and classes nested in it. The index keeps an anonymous
union or struct as a nested `<anonymous>` node, so its lines were skipped
too, though C++ looks its members up as the enclosing class's
([class.union.anon]).
Every generated protobuf message keeps its fields in `union { Impl_
_impl_; };`, so a call through `_impl_` was guessed by the method's name:
- `this_._impl_.type_url_.Destroy()` reached `Arena::Destroy`;
- `_impl_._cached_size_.Get()` reached `FieldArenaRep::Get`;
- `_impl_._has_bits_.Clear()` reached the message's own `Clear`.
fmt's `value` keeps its argument in a union of `string`, `custom` and the
rest in the same way.
cppNestedSpans now leaves out an anonymous union or struct with no name
and no declarator (isCppAnonymousMembers):
- a bare `union {` or `struct {` head;
- `};` after the closing brace.
A union with a declarator, `union { … } u;`, keeps its members to itself,
and so does a partial specialization, which the index also keeps as
`<anonymous>` but whose head names its template.
A member-chain diagnostic on main
|
||
|
|
81fd007850 |
fix(cpp): a call through a namespace links no class's method by its name (#2730)
A C++ call written through a namespace, `x::m()`, reached matchMethodCall's name strategies when no declaration of `x::m` was found by qualified name. They can only return a method of some class, which a namespace never holds: the capitalized-receiver class (`internal` → a class `Internal`), the one method of the name in the project, or the best receiver-word guess. So protocolbuffers/protobuf's `absl::StripPrefix(name, …)` linked to its Rust generator's `MultiCasePrefixStripper::StripPrefix` and `absl::Status(…)` to `upb::Status`'s constructor. fmtlib/fmt's `fmt::ptr(p)` linked to a test scanner's `ptr()` method, and `fmt::day(…)` to a test's `date::day`, ahead of the step that reads the namespaces a macro opens and finds fmt's own `ptr` function and `day` alias in `FMT_BEGIN_NAMESPACE`. Those strategies now return nothing when the qualifier is a namespace (isCppNamespaceQualifier): a lowercase name that no C or C++ type or alias node has, and that the calling file declares as neither a template parameter nor an alias (cppNamesBroughtIn, now split into type names and namespace names, since a namespace the file opens confirms it is one). rocksdb's `hash_table::Insert(…)` through `typedef HashTable<…> hash_table;` keeps its edge. So does the constructor of a class the call names in that namespace (isCppConstructorThrough): protobuf's `internal::SourceWrapper<absl::Cord>(&data)` constructs `struct SourceWrapper<absl::Cord>`, a specialization the index holds without its name, whose template is only declared. A method of another template's specialization is no constructor: fmt's `fmt::scan<int>(…)` is fmt's `scan` function, not `scanner<num>::scan`. The check runs only where one of those strategies would return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
009e4377ff |
fix(cpp): a call through a class the index doesn't hold gets no guess by a shared word (#2729)
A C++ call written through a class, `X::m()`, whose class has no node in the index fell through to the last strategy of matchMethodCall, which scores every method named `m` by the words its class shares with `X` and picks one. protocolbuffers/protobuf declares its descriptor messages in descriptor.pb.h, which is over the indexer's size cap, so `FeatureSet::default_instance()` linked to `UnknownFieldSet::default_instance`, `FeatureSetDefaults::GetDescriptor()` to `JavaFeatures_NestInFileClassFeature::GetDescriptor`, and the options messages' `default_instance()` to `pb::enumvalue::JsonEnumValueOptions`'. A class whose name only shares a word with `X` is another class: one named `X` was looked for first, by qualified name and by class name. That guess is now dropped when `X` is a capitalized name (isUndeclaredTypeName) that no C or C++ class, struct, union, enum, alias or namespace node has, and that the calling file makes nothing of itself (cppNamesBroughtIn): not a template parameter of any `template <…>` head in it, not an alias it declares (a function-local `using FD = FieldDescriptor;`, a typedef, or one a macro it expands declares, like rocksdb's IMPORT_RIBBON_IMPL_TYPES), not a namespace it opens, aliases or uses, and not googletest's TypeParam or TestFixture in a typed test. The one method of a name in the project is still taken as before. The scans read the file once (cppTypeNamesDeclared in cpp-type-aliases.ts, shared with the template-head reader) and the project's headers once for macro bodies, only when a guess would otherwise be returned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
accf600e47 |
fix(ts): a declaration in a declare module block is reached through its module (#2728)
A declaration file's declare module 'x' { ... } declares the members of
x, which no other file names by a bare name: an import of x reaches them.
The functions of such a block have been held to that since #2718, but its
other declarations, the types, classes, constants and enums, still took
bare-name matches from any file. On vscode, Node's
import { readFileSync } from 'fs' linked to the fs that vscode.d.ts's
workspace namespace declares, electron's clipboard and shell to env's, a
ProcessEnv to the proposed API's, and an LSP TextDocument re-exported
through a project barrel to the API's own.
Every block-level declaration now takes a match from another file only
when the reference is written in a block declaring the same module (the
proposed API files merge into vscode.d.ts), is a type position in a file
that imports the module (vscode.TextDocument under a namespace import,
which the extractor records by its last name), or names what the file
imports from a project file that imports the module and re-exports it
(copilot's vscodeTypes.ts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f66d064279 |
fix(cpp): a function is named by its declarator, never by its noexcept, throw or requires clause (#2727)
Both C++ extractors name a function by the first qualified identifier found breadth-first in its declarator, which is how an out-of-line `Foo::bar` is named. The walk skipped only the parameter list and the trailing return type, so a qualified name in the clauses after them became the name of a function whose own name is plain. nlohmann/json's constructor `basic_json(CompatibleType&& val) noexcept(noexcept( JSONSerializer<U>::to_json(…)))` was indexed as a method `JSONSerializer::to_json` that doesn't exist, with no node for the constructor; `throw(std::bad_alloc)` named a method `std::bad_alloc`, and a trailing `requires std::integral<T>` a function `std::integral<T>`. The walk now also skips `noexcept`, `throw_specifier` and `requires_clause`, in the TS extractor and the kernel alike; the parity fixture covers all three. Measured on main |
||
|
|
e97012eb31 |
fix(ts): an interface links the interfaces it extends (#2726)
tree-sitter-typescript holds an interface's supertypes in an extends_type_clause, which neither extractInheritance (wasm) nor the kernel's extract_inheritance read: a class's extends and implements became edges, an interface's extends nothing. So an interface had no place in the type hierarchy, a change to it reached none of the interfaces built on it, and a call through a composite interface whose head the receiver typing could not read fell to name matching (home-assistant/frontend's hass.formatEntityName linked to a namesake in an unrelated file). Both engines now emit one extends reference per type of the clause: a name, a qualified name as written (shapes.Shape), or a generic type by its name field (Repo for Repo<User>). They resolve as a class's do, so a package's type stays unresolved, and a namespace import or a module the repository declares resolves. A supertype a TS/JS module names without importing it is now its own declaration or a global one, at a script's top level or in declare global, never another module's or a namespace's: vscode's interfaces extending Omit<...> linked to quickInput.ts's export type Omit, and extends Function to terminal-suggest's Fig.Function. A call on a TS/JS type whose declaration is known now takes the method from that declaration's own supertypes. The walk by name, which unions the supertypes of every same-named type and looks the method up by name again, follows only classes' supertypes, as before interfaces had any. Otherwise vscode's receiver.dispose() on an ILocalOtlpHttpReceiver extends IDisposable reached copilot's own IDisposable, one of eleven, and stream.on() on a NodeJS.ReadableStream the project's ReadableStreamEvents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |