mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 22:43:17 +08:00
29ce0149cfba3222198a43b2e28aa1068716ff64
1748
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
29ce0149cf |
fix(lock): only one process takes over a stale write lock (#3056)
Issue #2979: FileLock.acquire() read the lock file, saw a dead PID, unlinked the file and created its own with an exclusive create. Two processes that read the same dead PID could both succeed: the first one replaced the stale lock, then the second one, acting on what it had read a moment earlier, unlinked the first one's live lock and created its own. Both returned from acquire() and wrote to the index as its only writer. The same steal happened in two narrower ways: a lock file read in the instant between its exclusive create and the write of its PID was empty, so it counted as stale, and a PID the process may not signal (EPERM: another user's process, or an elevated one on Windows) counted as dead. The fix keeps the lock file and its format (a bare PID) and changes how it is taken: - A free lock costs one create, as before. The lock file is created with its PID already in it: a private file hard-linked into place, which fails if the lock exists. Where hard links are missing it falls back to the exclusive create, and an empty lock younger than 10 s counts as held, since its creator is still writing its PID. - A stale lock (dead PID, malformed, or empty and old) is replaced only by the process holding <lock>.break, itself a FileLock, after reading the lock again there, the way Mercurial breaks its locks. A break lock a crashed process left is broken the same way, one level up, at most three levels. - EPERM from a signal-0 probe means the holder is alive. Validation: __tests__/filelock-stale-takeover.test.ts runs two real processes with a barrier: the slow one stops right after reading the dead PID and resumes only once the other holds the lock. On main both held it, 5 runs of 5; with the fix the slow one is refused and the lock stays the holder's. The other cases cover the slow one getting in once the lock is free, a live and a dead break lock, the empty-lock window, EPERM (mocked, and for real on Windows with the System process), and no leftover files. Fixes #2979 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bc9291afe6 |
fix(java): a call on a value a type holds in a static field reaches the method of the field's type (#3055)
A Java call written through a type's static field, Consts.HELPER.go() or Holder.SHARED.run(), was resolved by the method's name alone: the receiver Type.FIELD said nothing of its type, so the call went to a same-named method of whatever class the name matcher found, and a field of a library class (Intent.ACTION_VIEW.equals(...), System.out.print()) could reach a project method of that name. An interface's constants were not indexed at all (neither engine read a constant_declaration), so a constant a type declares in an interface, or inherits from one, had no declaration to read. A Java Type.FIELD.m() (Outer.Inner.FIELD.m(), a package written in front) now finds the type as the calling file names it (javaTypeRef), then the field, the type's own or one it inherits (five levels up), and calls the method on the value the field's declaration gives it, read with the field's own file's imports: an enum constant's own override, else the enum's method; the method of the anonymous class the field is initialized with (a CREATOR's createFromParcel); the class a new X(...) initializer builds; else the type the declaration writes. A Kotlin object's INSTANCE or Companion reaches the object's member. A library owner, or a field of a library type, links nothing. A receiver whose head is a local or a field links as before. Both engines index an interface's constant_declaration as a static final constant. Validation: - java-static-field-receivers.test.ts: 9 tests, 7 fail on main |
||
|
|
7021de5c30 |
fix(rust): calls through a type a path or a use names reach that type's own function (#3025)
A Rust call through a type, `sim::items::ItemPool::for_def(…)` into another
crate of the workspace or `ItemPool::for_def(…)` under `use
sim::items::ItemPool;`, was matched by its qualified name alone, so a
namesake type's function in another crate or module could take it (a
workspace's `client` crate's `ItemPool::for_def`). The module walk stopped
at the type, a `use` of a `pub const` or `static` (`use sim::RANK_COUNT;`)
linked nothing, and a type rustfmt wrapped below its path (`h:
&scheduler::\n Handle`) linked by the file's `use` of its last name.
The fix, on the module walk:
- rustPathTarget reads `Type::member` calls: the type is the item the
path's modules name, or the one the `use` that binds its name names
(rustUseItemsAt), and rustTypeMember finds its member: an enum's variant;
else the type's own (inherent) function or `const` in the type's crate,
a trait impl's only when it is the one of that name; a trait's own
declared method. A type alias, `Self`, a type the module declares itself
and a type path in any other position are left as they were.
- Module items include `variable`s (a module-level `pub const`/`static`).
- matchRustUseBoundName reads a qualifier written on the line above a
type (rust-scopes' blanked code, so a commented-out path is none) and
resolves the whole path.
- Memoized per (type, member) and per path.
- Manifest paths stay inside the project: a workspace member that leaves
it (`../sibling`, an absolute path) is neither read nor listed
(escapesRoot, in expandMembers), and rustCrateGraph drops a
`[lib]`/`[[bin]]` path that leaves it, as it already did a dependency's.
Validation:
- rust-cross-crate.test.ts (PR 2631's 9 cases, credited),
rust-type-member-paths.test.ts and rust-manifest-paths.test.ts: on main
10 of 15 fail; all pass. Every rust-* suite passes. On main the workspace
map read `../outside/Cargo.toml`, `../vendor/x/Cargo.toml` and
`/abs/crate/Cargo.toml` and listed `../vendor`; now only the project's.
- A/B against main, native kernel, 7 Rust repos (every site the same
as against PR #3014's own commit; re-checked on
|
||
|
|
cdfa31be54 |
fix(ruby): a call on an instance variable that initialize types links to its class's method (#3053)
Both extractors record a Ruby call on an instance variable as
`@writer.flush`, and no strategy read the variable's type, so every such
call stayed unresolved: 2,780 in activesupport and 3,239 in actionpack on
main, for example.
ruby-constant-paths now resolves `@name.m` written in an instance method of
a class (not a class method, not a module) by the class's `initialize`.
When every assignment to `@name` there starts a statement and is a lone
`Const.new(...)`, `A::B.new` or `::Foo.new ...` of one constant
(arguments with or without parentheses, a block or a statement modifier
may follow), the class is the one Ruby's constant lookup finds for it from
`initialize`, and the method is that class's own instance method or the
first its ancestry declares. Anything else leaves the variable untyped for
the other strategies, as before: a conditional, two classes, `.tap`,
`writer || Foo.new`, `@a, @b = ...`, a chain continued on the next line. A
class the project doesn't declare (`Logger.new($stdout)`) links nothing.
Validation, kernel engine, main
|
||
|
|
6d3804cf57 |
fix(resolution): C, C++ and Objective-C rank as one language, and a lone candidate is judged by family (#3052)
Name matching compared a candidate's raw language tag with the reference's: the single-candidate confidence in matchByExactName and matchFuzzy, and findBestMatch's same-language skip and +50/-80 score. So a call whose name had several definitions in another of C, C++ and Objective-C scored -80 for each and linked to nothing (an Objective-C file's _glfwPlatformSetTls(), a C++ call into Dear ImGui's imgui.h, which reads as C), and the one definition of a name across TSX and TS, or C and C++, was a 0.5 cross-language guess. The single-candidate confidence now compares language families (sameLanguageFamily). Ranking treats C, C++ and Objective-C as one language (sameRankingLanguage), the very language worth a point that only breaks a tie; other families keep the language itself, because ranking them by family moved excalidraw's Store::scheduleCapture calls to the App.tsx method beside them and gave okhttp 83 wrong Java-to-Kotlin Builder picks. Validation: - __tests__/name-match-language-family.test.ts, 8 tests (default and wasm): 6 fail on the base main; the pin that a .ts call keeps its .ts target over a .tsx namesake beside it passes on both; all pass with the fix. 24 related test files (cross-language, React Native, Expo, Fabric, frameworks, mixed TS/JSX, Objective-C, resolution): 746 pass. - A/B, main vs fix: glfw +4 calls from Objective-C into C, imgui +93 from C++ into imgui.h (read as C) and imstb_rectpack.h, all to the definitions meant; excalidraw, okhttp, leveldb and jq gain or lose no edge (43, 9, 97 and 30 unique-name links go from 0.5 to 0.9 with the same targets); flask and gin byte-identical. - Index time, median of 3 interleaved: imgui 7.9 s main, 7.7 s fix; excalidraw 9.2 s main, 9.4 s fix. Reimplements PR #1662 by ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
19b74d10f3 |
fix(deps): read .gitignore rules the way git does, with ignore 7.0.14 (#3050)
codegraph applies .gitignore rules itself, through the ignore package: on the filesystem walk for every .gitignore, and in git repositories on top of git ls-files for its built-in defaults merged with the root .gitignore and git's exclude files. codegraph.json's include, exclude and includeIgnored patterns go through it too. ignore 7.0.5 read several forms differently from git: a backslash escape (\d was a regex digit), POSIX classes and a ] or ! first in a bracket, and a run of three asterisks. A lone ! line matched every path, so it re-included everything the rules and the built-in defaults leave out, node_modules included. 2,031 of 200,000 random patterns also threw at match time, which readGitignorePatterns has to guard against (issue 682). ignore 7.0.14, with the range raised to ^7.0.14, reads all of these as git does, and none of the 200,000 patterns throws. Its code diff from 7.0.5 was read in full: pattern translation only, with no I/O, no dependencies and no install scripts. The lockfile entry matches the npm registry's own metadata on registry.npmjs.org. The package has no provenance, like every release its maintainer has published. Validation: - New __tests__/gitignore-pattern-parity.test.ts checks scanDirectory against git's own answer for these patterns, on the filesystem walk and in a git repository: its 4 tests fail with 7.0.5 and pass with 7.0.14. - scanDirectory on react, astro and django, in git mode and in walk mode on a copy without .git, returns the same 4,631, 4,946 and 3,027 files with either version. - On a fixture of the changed forms, 7.0.14 leaves out exactly what git leaves out in both modes, node_modules aside, which codegraph's defaults always exclude. With a lone ! line, 7.0.5 kept 16 of the 18 files on the walk, node_modules among them, where git keeps 6. Without that line it differed from git on 8 files on the walk and on 2 in git mode. - The gitignore and scope test files (extraction, git-ignored-dirs-scope, the include, exclude and includeIgnored configs, src-build-folders and others): 853 passed. Raised by PR 2885 (@johnhldiver-beep). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2514467801 |
fix(jvm): a dotted supertype names its nested type, and a Kotlin object expression is an anonymous class (#3051)
A supertype written through its enclosing type was lost in three ways.
Java and C# kept the dotted path (class Named extends Outer.Inner,
class N : Outer.IListener), but the qualified-name match compared it
verbatim with p::Outer::Inner, so it stayed unresolved; a Java anonymous
class (new Api.Listener() {...}) kept only the last segment, which no
nested type is found by; and Kotlin (both engines) took the FIRST
segment, so class K : Outer.Inner() extended Outer. A Kotlin object
expression, object : Listener {...}, made no class at all: its overrides
became local functions of the code around it, which a call through the
interface never reached, and bare calls elsewhere could be guessed onto
them.
A dotted Java, Kotlin or C# supertype now resolves to the type whose
qualified name ends with the path, when the file can name the path's
first type (its own file, package or namespace, an import; C#'s usings);
of several, the one closest in the tree, or none on a tie. The Java
anonymous class keeps the written path in its extends reference, and
Kotlin joins every segment of a supertype (type arguments left out), in
the wasm extractor and the kernel alike. A Kotlin object expression is
indexed as Java's anonymous class is: a class <Listener$anon@line> that
extends each supertype as written, which the code around it
instantiates (the class it builds, else its first supertype, also as
written and only ever a type that can be a supertype), with its body's
declarations as members; its header (constructor arguments, a
by-delegate) stays the enclosing code's. An anonymous class's members,
Java's and Kotlin's, are named only inside the declaration holding the
class: a bare block() elsewhere is never an object's override val block.
The walk up a Java or Kotlin class's supertypes for an inherited method
asks each supertype by the chain of types that names it
(AssistantMessage::Builder) before its simple name, so a builder's
build() is its own AbstractBuilder's and not an MCP annotation's;
the name still answers when an extends edge reached a namesake
(Minestom's Player extends LivingEntity).
The code in an object expression still sees the scope it is written in,
as when its overrides were local functions: a property of the class
around the object types a receiver there (okhttp's server.url() in an
object : Callback is MockWebServer's), an override is not what a call
of its own name in it means (original!!.contentLength(),
this@Outer.onScroll(), an extension receiver's onDataChanged()), and an
object's members out of reach leave a by-name guess as it was
(okhttp's it.isReady() is still the SerialTask interface's).
Validation:
- jvm-dotted-supertypes.test.ts: 13 tests, 11 fail on main
|
||
|
|
1941caf7bc |
fix(add-lang): the extraction check finds codegraph's npm shim on Windows, and takes CODEGRAPH_BIN (#3047)
scripts/add-lang/verify-extraction.mjs ran execFileSync('codegraph', ...).
On Windows, npm installs codegraph as a .cmd shim (so do npm link and
local-install.sh), and Node neither finds a .cmd by bare name nor runs
one without a shell. The check always died with "spawnSync codegraph
ENOENT" (exit 2), and /add-lang's bench.sh read that as broken
extraction and skipped its A/B.
The script now asks, in order:
- CODEGRAPH_BIN, when set. A .js, .mjs or .cjs path runs with the
current node; anything else runs directly (a .cmd or .bat through
cmd.exe).
- On Windows, the codegraph Windows itself would pick: the first PATH
folder holding codegraph under one of PATHEXT's runnable extensions.
A .cmd or .bat runs through cmd.exe (shell: true) as one command
string. Each token is quoted for the C runtime and caret-escaped for
cmd, so spaces, &, parentheses, a %NAME% and a trailing backslash in
the repo path arrive intact. An .exe or .com needs no shell.
- Otherwise codegraph by bare name, exactly as before. On macOS and
Linux without CODEGRAPH_BIN, the call and the output are unchanged.
bench.sh takes CODEGRAPH_BIN too. It makes a path absolute (indexing
runs from inside the repo), exports it so verify-extraction.mjs reads
the index with the same build, and runs its own --version and init calls
with it. Without it, bench.sh behaves exactly as before. The A/B's MCP
server stays run-all.sh's own CG_BIN.
Validation (Windows 11, Node 22.22.0):
- New __tests__/add-lang-verify-extraction.test.ts covers CODEGRAPH_BIN
with a .js stand-in (all platforms), a .cmd shim on PATH with an
awkward repo path (Windows only), and the bare-name call (macOS and
Linux only). With main's script both tests that run here fail ("could
not read codegraph status"); with the fix, 2 pass and 1 is skipped.
- The fixture from PR 1246's review (a codegraph.cmd on PATH): main
exits 2 with ENOENT; the fix reads the status.
- 13 scenarios with npm's own generated shims, and a real index in a
folder named "lua repo & co (x86) 100%": main fails every one that
needs the shim or CODEGRAPH_BIN, and the fix passes them with the
exact argv reaching codegraph. With no codegraph anywhere, both exit 2
the same way.
- bench.sh with the A/B stubbed: with npm link's shims on PATH, main's
copy skips the A/B (verify exit 2) and the fix reaches it (PASS).
CODEGRAPH_BIN as an absolute or a relative path reaches it too, and a
missing one stops with a clear message.
- With process.platform reported as linux or darwin, main's script and
the new one make the same execFileSync call and print the same output.
Raised by PR 1246 (@bslatner).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4e7a221089 |
chore(deps): take the security fixes for devalue and postcss, drop an unused types package (#3044)
devalue 5.9.1 (root, through svelte) and 5.8.1 (site/, through astro) fall in seven advisories published 2026-10-01 and fixed in 5.9.3, among them GHSA-j22f-vq7h-c4qm (CVE-2026-92708, high). postcss 8.5.6 (root, through vite) and 8.5.15 (site/) fall in GHSA-r28c-9q8g-f849 (CVE-2026-73646, high, fixed in 8.5.18) and GHSA-fxqj-rqcc-2cmp (fixed in 8.5.23); the root copy also in GHSA-6g55-p6wh-862q and GHSA-qx2v-qp2m-jg93. All of these are build-time dependencies. Lockfile-only updates inside the existing ranges: npm update devalue and npm update postcss, in the root and in site/. devalue goes to 5.9.4 and postcss to 8.5.32, whose own ranges move nanoid to 3.3.20 and source-map-js to 1.2.2, which clears their advisories as well (GHSA-28wg-ghj8-5hjv, GHSA-2v37-7h3g-55p8, GHSA-68fv-2mgg-jv7q). The @types/better-sqlite3 devDependency goes: nothing has imported better-sqlite3 since the switch to node:sqlite. In site/, npm 10 also drops five "peer": true markers that the npm which wrote that lockfile had added; they are metadata only, and npm ci installs the same tree. Validation: - npm audit: root 14 advisories before, 10 after; site/ 13 before, 9 after. devalue, postcss, nanoid and source-map-js are gone from both. - npm run build on main's lockfile and on this one: all 1,264 files of dist/, the viewer included, are byte-identical. The site builds its 21 pages, and its 101-file dist/ is byte-identical too. - The release bundle's install (npm ci --omit=dev --ignore-scripts on the staged lockfile) installs the same 12 packages, file for file. - All 8 changed resolved/integrity entries match the npm registry's own metadata and sit on registry.npmjs.org. No git, URL or tarball sources, and no new install scripts. devalue, postcss and nanoid carry provenance from their own repositories' release workflows. Raised by PR 2885 (@johnhldiver-beep) and PR 1477 (@anupamme). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> [skip actions] |
||
|
|
3a4cd3bf65 |
fix(ruby): calls to !/? methods and calls on what .new made link their method (#3049)
Two kinds of Ruby call linked nothing. A call to a method whose name ends
in `!` or `?` on a receiver (`writer.flush!`, `writer.ready?` after
`writer = CacheWriter.new`, `CacheWriter.create!(1)`) failed the method-call
pattern, whose method part took no `!` or `?`. A method called straight on
a new object (`CacheWriter.new(key).write_cache`, `Billing::Ledger.new.charge!`)
was recorded by both extractors with the receiver's raw text, arguments and
blocks included, which no strategy matched.
Both extractors now record a call on `Const.new(...)`, `A::B.new` or
`::Foo.new` as `new Const().m`, the constructed-receiver shape TS/JS, Java
and C# already use; the wasm extractor and the kernel's ruby.rs emit the
same refs. ruby-constant-paths resolves that shape before the name
pre-filter: the class by Ruby's constant lookup from the namespaces around
the call, the method as the class's own instance method or the first its
ancestry declares, and nothing when either is missing, so rack-protection's
`Request.new(env)` (rack's Rack::Request) links nothing. The Ruby dot pattern
in matchMethodCall takes a trailing `!` or `?`; the receiver-word scoring
among several project methods of the name stays off for such names on a
value receiver, where it was wrong on 55 of 74 new edges (`options.key?`
on a Hash -> InheritableOptions#key?). rubyAncestry no longer reads a
module a call returns (`include RouteSet.new.tap {...}.url_helpers`) as a
mixin of the constant the call starts at.
Validation, kernel engine, main
|
||
|
|
b700a3eec2 |
fix(python): a parameter or local named like an imported module is not that module (#3046)
A Python call through a name the file imports as a module, like other.update(...) after `from pkg import other`, linked to the module's function whatever the calling function bound `other` to: a parameter, a local, a with/except/loop target, a walrus, a case capture, a lambda or comprehension variable. resolveViaImport read the file's import mappings alone (its only scope check, pythonFunctionModuleAlias from PR #2600, covers a function-local `import a.b as X`), and isPythonProjectModule, which lets a builtin-named call like update/get/write past the builtin filter for a project module, asked the same file-wide question. The mappings also list from-imports at any depth ahead of plain imports, so a function-local `from pkg import third as other` became every function's `other`. pythonReceiverBinding reads how the code around a reference binds the name: the reference's own statement (lambda parameters, comprehension targets), then each enclosing function, innermost first, through its parameters and own statements (assignment, annotation, for, as, walrus, del, nested def/class, case captures, imports; global keeps the module's binding), memoized per function. pythonImportsInScope gives resolveViaImport's Python branch and isPythonProjectModule only the receiver's mappings that apply there. Validation: - __tests__/python-receiver-rebinding.test.ts, 80 tests (default and wasm, LF and CRLF): 48 fail on the base main (ten rebinding shapes, the local from-import, the cross-function leak), 32 pass (look-alikes: keyword argument, string, comment, comprehension over the module, lambda beside the call, global, keyword pattern, same-module local import); all pass with the fix. The 26 Python-related test files pass. - A/B, main vs fix: django's own package (930 files, kernel path) changes 2 edges' confidence (0.85 to 0.8, same targets: a closure over a parameter `apps` and a local `apps = model._meta.apps`); flask drops one cli.group() call and lowers 4 to 0.7 with the same targets (a nested `def cli` and a pytest fixture parameter `app` named like imports); requests, click, mealie and fastapi (package plus tests), and the express and gin controls are byte-identical. - django package index time, median of 3 interleaved: 11.3 s main, 10.7 s fix. Reimplements PR #2632 by BGamboa13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
407ec4fba2 |
chore: keep the kernel's vendored grammars out of GitHub's language stats (#3045)
GitHub reports the repository as 79% C and 18% TypeScript. Nearly all of that C is the third-party tree-sitter grammars the native kernel compiles from source (codegraph-kernel/grammars/, 29 files, about 78 MB). A new .gitattributes marks every file under codegraph-kernel/grammars/ linguist-vendored, so Linguist leaves them out of the language statistics, and marks the five generated parser.c files linguist-generated, so pull requests show those collapsed. The hand-written scanner.c files stay expanded in diffs on purpose: that is where C gets patched or added (PR 2173, and the open PR 2866), and every contributor PR is security-reviewed, so such a change must stay visible. The patterns use /** because an attributes pattern that names a directory does not reach the files inside it. Validation: git check-attr over all 1,722 tracked files reports linguist-vendored set on exactly the 29 files under codegraph-kernel/grammars/ and linguist-generated set on exactly the five parser.c files; scanner.c, the headers and the LICENSE files are vendored only, and everything else, src/index.ts included, has neither. In a scratch repository the bare directory pattern leaves parser.c unspecified. Git does not act on linguist attributes and codegraph never reads .gitattributes, so builds and tests are unaffected. Raised by PR 2513 (@build8421) and PR 1668 (@kidonng). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
37052b1825 |
fix(go): a package-level var or const named in uppercase is exported (#3043)
Both extractors created a Go var/const node from its var_spec or const_spec without exportedness: the TS Go branch passed no isExported (the declaration-level value is read off the var_declaration, which has no name), and the kernel's extract_variable set none. Every such node was unexported, so findGoPackageMember, which looks up pkg.Name among the package's exported declarations, skipped it: a call through `var Hook = func...` from another package linked to nothing, though the same call inside the package linked to the variable. Both engines now mark the node exported by its own name's case, read from the spec through goExtractor.isExported / is_exported, as functions, methods and types already were. Validation: - __tests__/go-package-value-exported.test.ts, 16 tests (extraction on the kernel and wasm, LF and CRLF; cross-package calls on both paths): 12 fail on the base main, 6 with only the TS half (the kernel checks), all pass with the fix. - A/B, main vs fix, kernel path: gin 108 var/const flags, 0 edges; prometheus 625 flags, 0 edges; etcd 806 flags and 12 new calls, all right (e2e.GetVersionFromBinary x11 and model.NonDeterministicModel, both `var X = func(...)`); flask and express byte-identical. The fix's kernel and wasm graphs are byte-identical on all three Go repos, and the wasm A/B matches. - kernel-parity --lang go: gin 99/99, prometheus 755/755, etcd 1065/1105 with 40 deferred on main too; 0 diffs. - etcd index time, median of 3 interleaved: 13.4 s main, 12.6 s fix. Reimplements PR #1521 by ericquan8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c12a7b6a74 |
fix(explore): Chinese, Japanese and Korean names reach explore, its Flow and context (#3031)
The index stores a name in any script as one full-text token, and `codegraph query 로그인` found it, but every tokenizer between a question and the index knew only ASCII: extractSearchTerms split on [^a-zA-Z0-9] and kept 3+ characters, context's extractSymbolsFromQuery matched ASCII patterns only, and explore's exploreQueryTokens and the Flow's flowTokens required a leading [A-Za-z_$] and 3+ characters. So `explore 寻路服务` answered "No relevant code found", `context 로그인` printed only its header and `explore 入口 末端` had no Flow, while their ASCII twins worked. Shared helpers in search/query-utils now read words in any script whole: letters, digits and combining marks, cut where Han, kana or Hangul meets another script (NavMeshAgent类型 is NavMeshAgent and 类型, never agent类型), at least 2 characters in Han, kana or Hangul and 3 elsewhere, so Russian на and из stay out. extractSearchTerms and the explore and Flow tokens use them. Context adds names with a non-ASCII letter as whole runs, and a mixed name like get用户信息 as written, with no n-grams, so a partial Chinese word still never becomes a search of its own (PR #2005). The ASCII-only patterns see a word with accented or other non-ASCII letters blanked out, so état is no longer read as tat. A Han, kana or Hangul token counts as a deliberately named identifier, as such a name has no case or underscore to show it, so a query of such names is neither damped as prose nor flagged low-confidence. Validation: new cjk-query-tokens tests (12) cover CJK, mixed, Cyrillic and accented words, reference copies of the old tokenizers over an ASCII corpus, and explore and context end to end on Korean, Chinese and Japanese fixtures; 9 fail on main's source. Explore and context A/B with 214 ASCII queries (gin 76, Alamofire 79, express 59; 49 lead with a Flow): all byte-identical. Of 12 non-ASCII queries, 6 changed on gin and 4 on express, as intended: état, Señal and naïve are read whole, and handleHTTPRequest处理 now names handleHTTPRequest. Tokenizing a 10,000-character query takes at most 1.8 ms, against 0.1-0.3 ms before. Reimplements PR #516 by @andy-sg and PR #1262 by @yimingll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9f379af79b |
fix(node): every language and folder of an overloaded name shows in codegraph_node (#3030)
codegraph_node's multi-definition answer rendered a name's definitions in index order (getNodesByName: file_path, start_line) until its caps ran out: 16 bodies within 12,000 characters, then 20 list lines, then a bare "+N more". Forty definitions under an early-sorting folder hid the only one in another language or a late folder: on the triage fixture (40 TypeScript `num` in aaa_client/, one Python `num` in zzz_server/), zzz_server/format.py appeared nowhere. Without includeCode, a long list was cut by the output cap at "... (output truncated)". The bodies still fill in index order, so every overload that rendered before still renders. On top of them, each language with no body gets its first hand-written definition rendered, while the whole answer stays under the output cap. The list of the rest fills its 20 slots in a new definitionCapOrder (the first definition of each language, then of each top-level folder within a language, then the rest, each tier in index order) and is shown in index order; the overflow line names what it leaves out by language and folder, like "+365 more: 365 typescript in aaa_client/". The list-only answer is unchanged unless it is longer than the output cap; then it fills in the same order and names the rest the same way. Generated stubs stay behind hand-written definitions. Promoting each folder's first definition into the bodies, as first tried, cost Alamofire's `validate` three Validation.swift overloads to a Tests/ helper, and `adapt`, `urlSession`, `encode` and `cancel` one or two Source/ bodies each, so folders get early list slots only. Validation: new node-overload-spread tests (6): 5 fail on main's source (the fits-every-cap guard passes on both) and all pass with the fix. A/B of every multi-definition name, with and without includeCode, main vs fix: gin 171 names, 170 identical (`_` only names its overflow); Alamofire 706 names, 702 identical. No body that rendered before is lost: `cancel` adds the jQuery `cancel` from docs/ as a 13th body, and `request`, `urlSession` and `encode` swap one Source/ list line for the first Tests/ one and name their overflow. Alamofire's `validate`, `task` and `didCompleteTask` and gin's `reset`, `Bind` and `Render` are byte-identical. Reimplements PR #1657 by @ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
60b0d7a92d |
fix(affected): a --filter glob matches whole names, from any folder boundary (#3029)
`codegraph affected --filter` turned its glob into an unanchored regex by hand (`**` to `.+`, `*` to `[^/]*`, no `^` or `$`), so it matched anywhere inside a path: `tests/**/*.py` missed `tests/x.py` (`.+` needs a folder), a changed `tests/x.py.bak` counted as a test under `tests/*.py`, the changed test itself was missed under `tests/**/*.py`, `e2e/*.spec.ts` took an `xe2e/` folder, and `/tests/*.py` matched only `other/tests/...`. The filter is now a picomatch glob (an existing dependency, loaded only by this command) with `dot: true`, matched against the whole project-relative path. A pattern with no slash matches the file name at any depth. One with a slash matches from any folder boundary, an implicit leading `**/`, which keeps the help text's own `e2e/*.spec.ts` matching `apps/web/e2e/...` as the filter always did; a leading `/` or `./` anchors it at the project root, and a trailing `/` takes everything under that folder. A pattern picomatch rejects is reported as an invalid --filter instead of failing later. Validation: new CLI-level cli-affected-filter-globs tests (6): 4 fail on main's source and pass with the fix; the other 2 guard behavior kept on purpose (file-name patterns at any depth, folders outside the test conventions, dotfiles, a trailing-slash folder). The existing affected CLI suites pass. The triage fixture shows the same before and after. Reimplements PR #1273 by @Jseca666. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3d19da2f59 |
fix(spring): a route links the method it annotates, including a package-private one (#3009)
The Spring extractor found a mapping's handler with a pattern that wanted
`public`, `private` or `protected` (or Kotlin's `fun`) before the name, in
the next 600 characters. A package-private handler, like Spring Boot 3's
`@GetMapping List<Article> list()`, matched nothing, so its route linked the
next method declared with a modifier, or nothing at all, and a method-level
`@RequestMapping(value = "/feed", method = GET)` on such a method made no
route. A generic Kotlin `fun <T> find()` handler fell through the same way.
Separately, a route bound by name alone took the wrong overload when a tall
annotation stack (`@Operation`, `@ApiResponses`) above the mapping left the
next overload's declaration nearer the annotation.
The extractor now reads the declaration the annotation is written on: past
it and every annotation stacked with it (arguments read as balanced groups,
strings and text blocks included), the modifiers (none at all for a
package-private method), type parameters and the return type (void, a
primitive, a qualified or nested generic type, an array) to `name(`; for
Kotlin, past its modifiers to `fun`, type parameters and `name(`. A field,
a constructor or a class under the annotation gives no handler. The mapping
annotation's own arguments are read the same balanced way. At resolution,
the route's handler reference binds to the method of that name whose span
(which starts at its first annotation) holds the annotation's line, the
innermost such method; any other case resolves by name as before.
Validation, kernel engine, main
|
||
|
|
8e854d11e5 |
fix(js,ts): a function exported as exports["name"] is a symbol named after its export (#3032)
`exports.getItems = async (req, res) => {…}` names its function after the
export property, in both engines (since the CommonJS controller fix), but
the same export written with the name in quotes, `exports["getItems"] =
…` or `module.exports['deleteItem'] = function () {…}`, left the function
anonymous: no node, so nothing could find it or link to it, and its calls
counted as the file's. commonJsExportName (tree-sitter.ts) and its kernel
twin common_js_export_name (tsjs/extractors.rs) accepted only a
member_expression on the left of the assignment.
Both now also accept a subscript_expression on `exports` or
`module.exports` whose index is a string literal spelling a plain
identifier. The test for that string, which `this['svc']` receivers
already used in both engines, is now one helper per engine
(quotedIdentifier / quoted_identifier) shared by the two readers. A key
that no identifier can spell (`exports["not-a-name"]`), a computed key
and a template string still name nothing.
Validation: the new commonjs-exports cases fail on main and pass in both
engines, and a parity case compares the kernel's and wasm's nodes and
refs for the JS and TS forms.
Reimplements the bug part of PR #2896 by @johnhldiver-beep.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a38a1ec0c0 |
fix(paths): a folder named like ..generated is inside the project, not outside it (#3028)
Several checks asked whether a path relative to the project root leaves
it with rel.startsWith('..'), which is also true of a folder whose name
starts with two dots, like `..generated/client.ts`. So a tsconfig paths
alias or baseUrl into such a folder linked nothing, the file watcher
dropped every edit made there (and the Linux per-folder watch tree never
skipped a gitignored one), a gitignored folder of that name could not be
brought back with codegraph.json include, a .gitignore rule for one was
never applied on the filesystem walk of a project that isn't a git
repository, and deprioritize could not name one.
One escapesRoot(rel) helper in utils.ts now answers it: `..` itself, a
first segment of `..` (`../`, or `..\` from path.relative on Windows),
or an absolute path, which path.relative gives for another Windows drive
(applyAliases did not reject that case before). It replaces the bare
prefix test at both path-aliases.ts sites, the watcher's handleChange
and shouldIgnoreDir, the extraction include walk and nested-gitignore
check, and the deprioritize matcher.
Left as they were, since a folder named that way is not a real input
there: Python source roots from config values and C/C++ include
directories in import-resolver.ts, Laravel require targets, and the
workspace-package targets, entries and path dependencies in
workspace-packages.ts.
Validation: new dotdot-folder-names tests (9) fail 8 of 9 on the base
and pass with the fix; the helper's own unit test runs against the new
helper in both. A/B against main on bulletproof-react, excalidraw,
outline, vben, gin and flask: byte-identical (none has a folder named
that way).
Reimplements PR #1406 by @amplifierplus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f1da755a91 |
fix(resolution): calls on the browser's own objects link no project method (#3026)
`window.open('/help')`, `localStorage.getItem('token')`,
`location.reload()` and `history.back()` linked to a project class's
method of the same name, like an app's `WindowService::open` or
`StorageService::getItem`, so callers and impact on such a wrapper
listed every direct browser call in the app. Three readings did it:
- an untyped receiver went to matchMethodCall's guesses, by the
receiver's word (`window` ~ WindowService) or a lone method name;
- a chain from `window`, which the extractor records by its bare method
name so a project object hung on `window` can be found (#1707), fell to
the exact-name match when the chain names no project object:
`window.localStorage.getItem()`;
- a JS `self.close()` was read as `this.close()` by bareCallReceiver and
callReceiverKind, though a `self` the file doesn't bind is the global
object (a worker's scope, or `window.self`).
Now, in the JS family:
- a call on one of JS_BROWSER_HOST_GLOBALS (window, self, globalThis,
document, localStorage, sessionStorage, location, history, navigator,
screen, performance) gets no edge when the file writes that name only
as a receiver (`document.x`, `history.back()`, `self?.close()`) or after
`typeof`, decided before the receiver-type reader, whose backward scan
has no scope. Any other mention (a parameter, destructured ones
included, a declaration, an import, an argument) keeps the name the
file's own: outline passes its Document model as `({ document }:
Props)`, and those `document.save()` calls keep their links;
- matchCollapsedObjectCall declines a `window`/`globalThis` chain whose
first link is one of them, after its project-object lookup; a chain
through a project object (`window.App.init()`) is unchanged;
- an unbound `self` reaches matchCollapsedObjectCall as
`globalThis.self`, and callReceiverKind no longer counts it as the
instance. A `self` the file binds (`var self = this`) reads as before.
Validation: new browser-host-globals tests (4) pass with the fix. On
main the first fails with 8 wrong edges and the third (outline's
destructured `document`, a `typeof window` guard) with 2; the two that
pin what must keep working (a bound, imported or parameter name; `this`,
`var self = this`, `window.App`) pass on both. A first version that
asked only whether the file binds the name dropped outline's
destructured `document` calls; the third test fails against it.
A/B against main: only removals. excalidraw 12 (`window.open` ->
Portal::open, `document.createElement` -> DraggableTool::createElement,
a worker's `self.postMessage` -> WorkerPool::postMessage), outline 21
(test and app `sessionStorage`/`localStorage` calls -> MemoryStorage,
`window.document.body.*`, `window.history.back()` -> a Props property),
bitwarden apps/browser + apps/web 7 (`window.postMessage`,
`window.location.reload()`, `window.navigator.permissions.query()`), and
express unchanged. 39 of the 40 linked a project method by name only;
the 40th linked outline's own `declare global` typing of a browser API
(Document.modelContext.registerTool).
Reimplements PR #2915 by @KissFeng.
Fixes #2907
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
38dc3741aa |
fix(lookup): a qualified symbol query names its scope whole (#3024)
`codegraph callers Format.Price` also selected `LegacyFormat::Price` and
merged its callers into the answer. matchesSymbol, which serves the CLI's
callers/callees/impact, codegraph_node and codegraph_explore's symbol
seeding, accepted a qualified query anywhere inside the qualifiedName
(`qualifiedName.includes('Format::Price')`), so any class whose name
ends with the qualifier matched. A second way in: a path segment could
stand for any qualifier, so a `Format.cs` holding `LegacyFormat` let it
match too.
The match now has to end the qualifiedName on a scope boundary, under one
canonical separator, which the existing boundary stage already did. The
raw substring test also covered two spellings that stage did not: a PHP
namespace's backslashes (`App\Models\Billing::Format::price`), now read
as separators on both sides, which also makes `Billing\Format::price`
work, and an Erlang arity (`my_server::start/0`), now dropped before the
comparison. A path qualifier no longer names the type of a type member
(method, property, field, enum member) unless it is that type's own
name, except in Rust, Python and Go, whose modules live only in paths. A
free function keeps its file or folder qualifier (`utils.formatDate`).
Query-time only: no re-index.
Validation: new qualified-lookup-boundary tests (7, including the CLI
and codegraph_node on the issue's repro) fail 6 of 7 on main and pass
with the fix. On real indexes, `callers` targets: eShopOnWeb
BasketService.AddItemToBasket 2 to 1 (the IBasketService declaration
no longer merged in), laravel ServiceProvider.register 37 to 1,
Store.get 14 to 2, and Support\ServiceProvider::register from not
found to 1. A differential oracle over seven indexes (C#, PHP, TS,
Vue, Go, Python) asked every boundary-aligned suffix of every member's
own qualified name, 201,582 queries: none stopped matching. Its cross
queries only drop matches, mostly classes whose name ends with the
qualifier.
Reimplements PR #2914 by @yang0228.
Fixes #2908
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
787eafbffa |
fix(jvm): a call through an import reaches the Java or Kotlin class it names (#3023)
A Java or Kotlin import was matched against a file path written with the calling file's own extension (com/example/Foo.java from Java, .kt from Kotlin), so an import across the two languages never bound: Kotlin's JUtil.make() on an imported Java class went to a same-named class in another package, JUtil.Nested.deep() and Outer.Inner() linked nothing, and Java's KUtil.INSTANCE.kmake() went to another package's object. The import is now read by qualified name in either language (the package is some prefix of the name, so import p.Outer.Inner is p::Outer::Inner), and the rest of the reference walks the imported type's nested types: a Kotlin object's INSTANCE and a class's Companion, as Java writes them, step onto the type, whose companion members the index keeps on the class. Java's UtilsKt.helper() reaches the top-level function of p/Utils.kt, and import p.UtilsKt links that file. A call picks the member's method over a same-named field or nested class (jextract's Index_h.clang_getRange() is the static method, not the holder class of that name), the overload its argument count fits (Minestom's NetworkBufferTemplate.template(a, g, b, h, ctor)), and, from Kotlin, a nested Java class's own constructor. A Kotlin chain rooted at an imported type is walked before the receiver-chain typing. Only an imported type takes this path; an imported function or property is left to the name strategies, which know a call's receiver. A Kotlin extension declared in another package (fun Modes.hidden(), indexed as Modes::hidden) is now a candidate only from a file that imports it or its package, as Kotlin has it. Validation: - jvm-cross-language-imports.test.ts: 8 tests, all red on main, green here (cases from #1948's test and the T5 triage fixtures). - A/B against main |
||
|
|
57dc7ae481 |
fix(search): path: and name: filters find their matches at any limit (#3018)
QueryBuilder.searchNodes applied `path:` and `name:` only to its final list, after every candidate source had cut its rows to the limit: full-text search to max(limit*5, 100) rows, the substring fallback to `limit`, the typo fallback to 50 names, a filter-only search to the first limit*5 names in name order, and the exact-name supplement to 20 rows. Whenever enough symbols outside the filters ranked first, nothing the filters asked for was left, so `codegraph query "authenticate path:product/"` found nothing at limits 1, 2 and 10, and found the match at 200. That broke the query parser's contract: filters narrow the set and the search ranks within it. Every source now applies the filters before its own limit and offset. Its statement is streamed and JavaScript decides each row, so matching keeps toLowerCase's Unicode folding and literal substrings (SQLite's lower() and LIKE fold ASCII only, and LIKE reads % and _ as wildcards). A page and its offset count only the rows that pass, and the read stops once the page is full. To spare JavaScript the rows that cannot pass, each statement also carries a SQL pre-filter that keeps every row the filters could pass: instr(lower(...)) for a printable ASCII value, which agrees exactly with JavaScript on printable ASCII text, while text with any other character passes through to JavaScript. The typo fallback looks names up until it has its results instead of giving up after 50 names, and skips names that `name:` rules out. A filter that no symbol's name, or no symbol's file, passes returns nothing at once, checked against the memoized name list and a memoized list of file paths. The old final filter stays as a safety net, now before the cut. A search without these filters runs the same SQL as before, except that a filter-only search now honours its offset (the same rows at offset 0, which every caller in the codebase passes). Validation: - Kit repro through `codegraph query --json`: on main all 18 filtered queries (6 queries at limits 1, 2 and 10) returned nothing; with the fix each returns its match. - 9 new tests in search-field-filters.test.ts; 7 fail on main's source. Mutations caught: each of the five candidate sources left unfiltered, the offset ignored, the non-ASCII pass-through removed or kept only for non-ASCII values, and the JavaScript check removed. - Latency A/B, main vs fix in one process on one index, 5 interleaved rounds of the median of 5 calls: vscode src/vs/base + src/vs/editor (56,406 nodes) and a 10x replica of it (564,060 nodes). The 45 unfiltered queries (explore's kinds at limit 16, plain limit 10) return identical ids and scores: 89.7 vs 88.4 ms and 435.7 vs 441.1 ms in total. On the replica the filtered queries now find their matches in 0.2-26 ms; the slowest are a substring fallback that finds nothing (156 ms, what an unfiltered miss already costs) and `kind:method path:...` (116 ms). A filter nothing passes answers in 0.2 ms instead of reading every row. Reimplements PR #2254 by @rudycelekli. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8bd55fa75c |
fix(frameworks): a component's same-folder preference compares whole folders (#3020)
The React, Svelte, Astro and Vue component resolvers each picked among a name's definitions by preferring one whose path started with the referencing file's folder, compared as text with no separator: `src/ui/Card.tsx`'s `Widget` took `src/ui-kit/Widget.tsx`, since `src/ui-kit/...` starts with `src/ui`. A file at the project root has the folder `''`, which every path starts with, so its pick was whichever definition came first, skipping the rule that a name with several definitions and nothing to choose by links nothing (#764). One helper, inReferencingFolder (frameworks/component-folder.ts), now serves all four copies: a candidate counts when its path starts with the folder plus `/`, so a folder below the referencing file's still counts, and a root file has no folder to prefer. Validation: new component-same-folder tests (7) fail 6 of 7 on main, one per resolver plus the root and subfolder cases; the helper's unit test runs on the new module in both. A/B against main: outline, mastodon (app/javascript), excalidraw, vben, shadcn-svelte (docs/src) and gin are byte-identical, since real code imports its components and the framework pick runs only for a name the file doesn't import. Reimplements PR #683 by @Mubashirrrr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
da1f26380f |
fix(index): a repeat full index keeps the name words of files it didn't change (#3017)
A library indexAll() over an existing index cleared the name-segment vocabulary (the words in symbol names that the prompt hook and codegraph_explore match plain-language questions against) before extraction. The store skips files whose content hash is unchanged, so their names were never written back: a second run with no edits left the table empty, and a run after an edit kept only the edited files' names. Sync and healSegmentVocabIfEmpty() repair only a completely empty table, so a partial one stayed that way. A cancelled run emptied the table as well, because the clear ran before the abort check. The CLI was not affected: `codegraph index` recreates the database first. The table is now cleared up front only when the database is fresh. Over an existing graph it is kept, and once a run completes without being cancelled, one DELETE drops the rows of names that no symbol carries any more (file and import nodes don't count, as on the write path). That keeps the orphan cleanup the clear used to do. The prune also resets the session's set of names already written, so a pruned name that comes back later in the same session gets its rows again instead of being skipped. Validation: - Kit repro with the PR's fixture: on main a second indexAll() took the table from 11 rows to 0, an edit plus a third run left 4, and a cancelled run emptied it. With the fix it stays at 11 rows, the renamed class's 3 rows give way to the new name's 3, and a cancel keeps them all. - 5 new tests in segment-vocab.test.ts; 4 fail on main's source, and the fresh-database guard passes on both. Three mutations are caught: no prune, no reset of the written-names set, no clear on a fresh database. - CLI: `codegraph init` then `codegraph index` write the same 11 rows on main and with the fix. - Real repo, vscode src/vs/base + src/vs/editor (1,388 files, 56,406 nodes, 66,250 vocabulary rows), a library indexAll() over the existing index, 3 interleaved runs per arm: main empties the table (66,250 to 0 rows); the fix keeps all 66,250. The prune takes 40 ms of each run, and the runs take the same time (median 8.37 s on main, 8.13 s with the fix). - Cost at scale: the prune statement takes 288 ms on a 10x replica of that index (564,060 nodes), and about 1.2 s on a synthetic graph with 505k vocabulary rows (170k distinct names); a full re-index of that size re-parses every file. Reimplements PR #2252 by @rudycelekli. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4955eac958 |
fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules (#3014)
A bare Rust name took the file's `use`s all at once: a `use` inside one
function (`fn first() { use crate::util::take; take() }`) bound `take` for
every other function too, and for the file's own `fn take`; a renamed one
(`use crate::util::take as consume; consume()`) linked nothing, since no
item carries the alias; a parameter or closure of the function's name
(`fn run(take: fn()) { take() }`, `let take = || {};`) linked to the
imported function; and `use crate::models::E::Good; Good(1)` took any
`E::Good`. Paths through modules (`resolveRustPathReference`) walked the
directories instead of the modules: `super::helper()` and `crate::config()`
with no module between linked nothing, `super` inside an inline
`mod tests { }` went to the parent directory's module, and an item a module
passes on (`pub use`, a glob) or another workspace crate's was not found.
The fix keeps one `use` reader, rustUsesOf:
- each `use` keeps the block it is written in (a function's body or a
block; an inline `mod` and a macro's braces, `cfg_if!`'s nested ones
included, are the module's), and std `use`s are kept by position;
- a reference reads the binding of its innermost scope (rustVisibleUse);
isRustNameInScope uses it, so a block's `use` shadows the file's own item
and a variant `use` names its enum's variant (rustUsePathItems' enum tail);
- matchRustUseBoundName, ahead of the pre-filter, resolves a rename to the
item its path names, and a call to a name a local binds (rust-scopes'
bindings, nearer than any `use`) to nothing, unless the local is a `let`
holding a function by its path (serde's `let test = test::<i8>;`);
- resolveRustPathReference is rustWalkModules + rustModuleItems from the
module the path is written in (rustPathTarget), with today's crate-root
fallback; resolveRustModuleFile, collectRustUseBindings and
rustItemPlacement are gone, and isBoundToOutOfRepoImport reads
rustUsesOf's std `use`s;
- `use`s are read with strings blanked (blankRustCode), so a string's
"use crate::x;" binds nothing;
- the crate graph reads a package's name by its section (axum-core writes
`categories = [..]` first) and a `[lib]`/`[[bin]]` path of its own
(ripgrep's `crates/core/main.rs`), which `crate::` then starts from; a
file no manifest covers starts it from the nearest lib.rs or main.rs
above it, as the directory walk did.
Validation:
- rust-use-call.test.ts (PR 2308's 15 cases), rust-use-scopes.test.ts,
rust-crate-roots.test.ts and 5 new rust-module-path-call cases: on main's
source 18 of 32 fail; all pass. Every rust-* suite passes.
- A/B on main
|
||
|
|
e7c9ffd068 |
fix(security): an MCP session no longer opens a sensitive folder as its default project (#3012)
The MCP server's default project comes from the client (initialize's rootUri or first workspaceFolders entry, or its roots/list answer), the launch cwd, or --path. Only an agent's projectPath argument went through the sensitive-folder guard of PR #2929: the default root was opened with no check, and opening it also started a file watcher and a catch-up sync, and from the launcher a shared daemon keyed on that folder. A session whose default was ~/.config, ~/.ssh or a system folder holding an index served the files inside to the agent. (On main a warm query pool happened to refuse such a call with isError; before it warmed, the main thread served it.) resolveServerRoot, which every default-project resolution goes through (the engine's open and its per-call retry, the initialize handshake and the launcher's daemon root), now applies the refusal a projectPath gets: on the path searched from when it exists, and on the root it lands on, by up-walk or by the sub-project down-scan. A refused root resolves to null with the reason, so the session has no default project. Nothing there is opened, watched or synced; no daemon, lock or socket is set up in the folder; initialize sends the no-default-project instructions; and a call without projectPath gets the usual success-shaped "pass projectPath" guidance, saying why. A call that names the folder keeps its PathRefusalError, and sub-projects below a refused folder aren't listed. --path is refused the same way, as `codegraph ui` refuses its root, but the server stays up with no default project instead of exiting, so the agent keeps codegraph for its other projects. A daemon started by hand for such a folder exits. The sensitive-folder half of validateProjectPath is split out as sensitiveDirectoryRefusal; validateProjectPath behaves as before. Validation on Windows 11: - 6 new tests in security.test.ts. On main's sources every entry point is red: the engine opened ~/.config from the folder, a folder inside it, a missing folder under it and a junction to it, serving a fake token and taking writer.pid; rootUri, workspaceFolders, cwd and roots/list sessions served the token; the down-scan adopted or listed sub-projects; a launch from the cwd started a daemon in the folder. The ordinary-project control passes on both. All 6 pass with the fix. - security.test.ts: 70 passed, 4 skipped (POSIX and macOS gates). 51 MCP, daemon, proxy and directory suites: 646 passed, 0 failed. - The real serve --mcp against a private HOME, from a sensitive cwd and with a sensitive --path: no-default instructions, the guidance instead of the token, no daemon.pid or writer.pid. An ordinary project is unchanged. - Cost: resolveServerRoot 141 -> 377 us per call on an ordinary project (median of 7 rounds of 2000 calls). It runs at the handshake, the engine open and the launcher, and per tool call only while a session has no default project. Follow-up to PR #2929. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
86c85cff02 |
fix(kotlin): a call on an imported type reaches that package's class when two share a name
When several classes share a name, resolveMethodOnType picks the one the call file imports by matching the import's path against the candidate's file path (com/example/b/FooConverter.kt). That holds for Java, where a public class names its file, but a Kotlin class often lives in a file named otherwise (FooConverter in Converters.kt), so no candidate matched and the call went to whichever class came first. The imported name is now matched against each candidate's qualified name first: import com.example.b.FooConverter expects the method com.example.b::FooConverter::convert, which the package directive gives every JVM declaration whatever its file is called. The file-path match stays as the fallback. The Java calls this once fixed, a Java file calling a Kotlin class it imports (okhttp's Java tests and mockwebserver3.MockWebServer, SystemUI's BlurUtils), are now resolved on main by the receiver's declared type (#1942), so on today's main the change shows in Kotlin. From #539 we take only this change and its test. Its other two parts, indexing Kotlin class properties and reading Kotlin imports without a trailing semicolon, have since landed on main another way. Validation: - resolution.test.ts: the Kotlin same-name case (#314's shape in Converters.kt files) is red on main, green here. - A/B against main |
||
|
|
40f5851787 |
fix(kotlin): a call on an outside type reaches the project's extension on it
A Kotlin import from outside the project owns its name (#2196), so Modifier.fillMaxSize() after import androidx.compose.ui.Modifier never lands on a same-named project method. That rule returns before any other strategy, so a project extension on the imported type, Compose's fun Modifier.pad() (indexed as Modifier::pad), could not be reached by Modifier.pad() either, nor Color.fromHex() on a fun Color.Companion.fromHex(). Inside the rule, a Kotlin Type.member call now resolves to a project extension Type::member that the calling file can see (same file or package, or imported by name or with .*, as for other top-level Kotlin declarations) and whose own file imports the same type. Anything else stays unlinked, as before. Validation: - kotlin-extension-on-imported-type.test.ts: 2 cases, red on main, green here. - A/B against main |
||
|
|
13f983db5a |
fix(kotlin): a private property, and the members of a private class, stay in their file
A Kotlin property was indexed without its visibility modifier, so the Kotlin file-local rule (#1745), which declines a private fun in another file, read every property as public: a call like token(...) could link to another file's @Volatile private var token, and JavaScript, Go or Python callers to a Kotlin test fixture's private val. A public member of a private class or object was reachable from other files too, though nothing outside its file can name it. Both extractors (wasm and the native kernel) now read a property's modifiers the way they read a function's, and a candidate inside a private Kotlin class or object is declined from another file. That second rule is Kotlin's alone: a Scala private[pkg] class, or one with a private constructor, is its package's, and a C# private nested class serves every file of its partial class. Applied to every language where private is file-local, as the PR had it, it removed 742 edges on http4s (right ones among them, like stream.receiveHeaders() on a private[h2] class H2Stream), 91 on scalachess and 10 on serilog (9 of them right typed edges). Validation: - cross-file-visibility.test.ts: 2 cases, red on main, green here; the kernel parity fixture gains a private val and an internal var. - Kernel parity (kotlin), 0 diffs: okhttp 588/619 files, mihon 891/940, nowinandroid 340/350, SystemUI's Kotlin 188/198 byte-identical, the same files deferred to wasm as with main's kernel; torture 1/1. - A/B against main |
||
|
|
01e0ac3aa7 |
fix(kotlin): a call through a property or a receiver chain resolves on its declared type
Kotlin indexes a class property without a signature, and a primary-constructor property (class A(private val repo: Repo)) is no node at all, so a call through either (probe.close() on a lateinit var probe: HttpProbe) found no receiver type and fell to the name-only strategies: the interface's method, or any same-named method elsewhere. A call through a chain (engine.pump.drain()) was emitted as its bare method name, and a local bound to a qualified call (val lease = HardwareLock.tryBegin()) was untyped. tree-sitter-kotlin also predates fun interface: such a declaration parsed as a broken function and, with a doc comment after it, its error recovery swallowed the next declaration too. The declared type is now read from the declaration itself: a property's own lines, the class header for a constructor property, a callee's declared return type for a local bound to a call, an enum entry's enum, each segment of a receiver chain in turn (the chain is kept in the reference name, by the wasm extractor and the native kernel alike), a superclass's property, and an outer function's local inside an anonymous object. A receiver whose type the project does not declare (Regex, Properties, a JDK class) gets no edge instead of a name-only guess, unless the project extends that type. The extractor's preParse blanks the fun of a fun interface, offset-preserving, so the file parses cleanly in both paths. Validation: - kotlin-property-receiver.test.ts: 17 tests, 12 red on main, all green here; kernel-kotlin-parity.test.ts pins the fun interface fixture. - Kernel parity (kotlin), 0 diffs: okhttp 588/619 files byte-identical (main's kernel 578, the rest deferred to wasm on parse errors), mihon 891/940 (887), nowinandroid 340/350 (339): the fun interface files no longer defer. - A/B against main with #1942 (positions only: the extractor now keeps a chain in the reference name): okhttp +299/-257, 2,554 retargeted, 2,158 of them test calls moving from the deprecated okhttp3.mockwebserver.MockWebServer (a 0.65 name guess) to the mockwebserver3.MockWebServer each file imports; mihon +82/-118, 337 retargeted (Preference get/changes/set/register onto the declared Preference); nowinandroid -9, 23 retargeted (properties onto their concrete test doubles); SystemUI's Kotlin +1/-104, 7 retargeted; jextract, click and gin identical. The removed are name guesses on library types (okio sinks, collections' filter and clear, OkHttp bodies, MutableStateFlow.update, Kotlin's apply). 7 right edges are lost where a chain passes through a library wrapper (Lazy.value, StateFlow.value, Pair.first, a ViewBinding's view). - Cost (mihon, median of 3 interleaved indexes): CPU 25.8 s on main with #1942 vs 26.7 s, wall 8.3 vs 8.4 s. Original PR #1933 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
77ee3484be |
fix(aliases): a paths wildcard no longer matches an import that leaves nothing for its * (#2969)
A tsconfig/jsconfig `paths` key with text on both sides of its `*`, like "@x/*/index", matched an import that starts with the prefix and ends with the suffix only by counting one character twice: `@x/index` starts with `@x/` and ends with `/index` through its single `/`. applyAliases tested startsWith and endsWith independently and sliced an empty capture, so the import and every call through it linked to `widgets/index`, a file the import never named. The pattern is now skipped when the specifier is shorter than the prefix and the suffix together, which is TypeScript's own isPatternMatch rule. A less specific pattern can still match, and an empty capture at exactly prefix + suffix length stays allowed, as tsc allows it. Validation: new path-alias-overlap tests (4) fail 3 of 4 on main and pass with the fix; the exact-length case passes on both. A/B against main on bulletproof-react, excalidraw, outline and vben (all with paths aliases) and gin and flask: byte-identical, as no paths key in them has text after its `*` (0 of 44 configs with paths in our local clones). outline index median of 3 interleaved: 19,165 ms main, 18,546 ms fix. Full suite: 9,226 passed, 0 failed, 85 skipped. Reimplements PR #882 by @Dashsoap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7973df0d09 |
fix(java): a call resolves on its receiver's declared type, and a library type gets no guess
Java typed a call receiver from a local declaration or a plain field of
the enclosing class only. A field of an outer class read inside an inner
or anonymous class (mHandler.post(...) inside new Runnable() {...}),
Outer.this.f, a chain of fields (this.mOwner.mRepo.save()) and a local
whose type has type arguments fell through to the name-only strategies,
and so did every receiver whose declared type is a library class:
parcel.readInt() on an android.os.Parcel went to a project
VersionedParcel.readInt, list.size() to any project size().
The declared type is now read for each of those shapes, a chain one hop
at a time with each hop's own imports, keeping a written package or
outer class (Map.Entry, play.api.mvc.BodyParser). A receiver whose
declared type the project does not declare, or that an import or a
written package pins outside the project, gets no edge. When no import
decides between same-named types, a Java call prefers the Java
declaration over a Scala or Kotlin one.
Also, from review: a field the class around the call declares is read
as written before the member reader, which keeps only the last name
(PauseTest.Suite, a field written with its package); a local or loop
variable keeps the package its declaration writes; a field typed by a
type parameter goes to its bound, and a class the class head names as
a type argument (Comparable<ApplicationModule>) is not taken for one; a
nested type a superclass declares, or one an on-demand import of its
class names (import play.mvc.Http.*), counts as named; and a typed miss
on a type no project class or interface is named (a URI, an enum's
name()) gets no guess, as on main.
Validation:
- java-receiver-type.test.ts: 19 tests, the PR's 5 red on main; the
other 14 pass on main, and our 8 each fail without their change.
- A/B against main
|
||
|
|
72f398af6e |
fix(index): a video named .ts counts as skipped, so a full index isn't PARTIAL (#2943)
The full index's batch reader recognises an MPEG transport stream named `.ts` from its first bytes (Issue #1910) and skips it, but that branch counted the clip only as processed. Every other file the scan finds lands in filesIndexed, filesSkipped or filesErrored, and CodeGraph.indexAll checks those tallies against filesDiscovered, so the clip left the run one file short. `codegraph init` and `codegraph index` then warned "Index is missing 1 of N discovered files ... The index is PARTIAL", stored index_state = partial, which `codegraph status` reports, and left the indexed commit unrecorded, so status fell back to its full scan. The branch now counts the clip in filesSkipped, as the branch below it does for a file gone from disk. Validation: - mpeg-ts-not-typescript.test.ts: a new case (a clip beside one source file) fails on main's source with the tallies at 1 of 2, and passes with the fix: no index_partial warning, index_state complete. The file's other 11 cases pass. - CLI: on main, `init -y` of app.ts plus testdata/clip.ts prints the PARTIAL warning, status --json reads state "partial", and in a git repo indexed_at_commit stays empty. With the fix it prints "Indexed 1 files", the state is "complete" and indexed_at_commit is HEAD. Follow-up to Issue #1910, reported by @Hetox; found while fixing Issue #2904 (PR #2928). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5594843698 |
fix(go): a call on a value bound from a call or a conversion reaches its type's method (#2961)
A Go method call on a local bound from a call or a conversion, like
`r := newRing(); r.Reset()`, `v1 := router.Group(...); v1.GET(...)` or
`c, r := CreateTestContext(w); r.Use(...)`, was matched by the method's
name alone: inferLocalReceiverType reads a declaration's written type and a
few line patterns, none of which reads a call's result. The by-name guess
took a namesake (etcd's `lg := s.Logger()` linked `lg.Info(...)` to a
project logger's Info, prometheus's `NewWriteHandler(...)` results to the
read handler's ServeHTTP), or found nothing.
Main already types a value from its declaration for indexed and chained
calls (goDeclaredValueType, #2782). Plain receivers now use it too: a local
whose in-scope declaration has a call or a conversion as its value takes
that value's type (the k-th result of several, a method's result, a
conversion's type), read in the file that writes it (GoReceiverDecl.file),
for matchMethodCall, matchMemberFunctionRef and matchGoFieldChainCall. A
value of a type from outside the project links nothing, including one a
project function returns as an outside interface. Assertions stay with
goAssertedLocalType, and a type switch's guard is not typed from its value.
The typer itself, for every path that uses it:
- a trailing `.(T)` is a T whatever its prefix (`pool.Get().(*Conv)`);
- a call through a func-typed field gives the field's result type;
- a bare call a dot import brings in is typed (`package watch_test`
calling `NewStreamWatcher(...)`);
- a method of a struct or interface literal, of an outside generic type
(grpc's `ServerStreamingClient[Resp].Recv()`) or one the index doesn't
show on a project type gives an unknown type, not "outside", so name
matching runs as before.
Validation:
- go-receiver-type.test.ts (the PR's 23 cases, 2 expectations updated where
the call is now typed, plus 4 ours), go-std-method-receivers.test.ts and
go-chained-receivers.test.ts: on main 19 of 51 fail; all pass here. All
45 go-*.test.ts files pass (491 tests).
- A/B vs main
|
||
|
|
3f8e8b0f65 |
fix(rust): a module-path call links the function its module declares
A Rust path into a module, `crate::util::take(1)`, `super::util::take(1)`,
`util::take(1)` or a `use crate::util::take;`, went to the first node of
that name in the module's file, methods included, so an `impl Buf { fn
take }` above `pub fn take` caught every such call. A function written
inside another function, or in a `mod tests { ... }` above the module's
own item, could take it too, a path into the module's own file linked
nothing, and a path through a module a `use` takes from the standard
library (`fs::read` under `use std::fs;`) went to the project's own `fs`
module.
The path's item is now the module's own: never a method, an impl's or a
trait's associated item, or an item of a function body; one written at
the file's top level before one in a module the file declares inline,
which the path reaches through the module's re-export (`#[cfg(unix)] mod
imp { ... }` + `pub use imp::*;`). A path whose head a `use` binds from
the standard library is left to it (main's isBoundToOutOfRepoImport rule
for bare names), and a path into the module's own file resolves.
Validation:
- rust-module-path-call.test.ts: 8/8; on main 7 fail (method Buf::take,
Deep::get, Only::grab, no import edge, the inline-module namesake, the
re-export from its own file, std `fs::read` -> src/fs/mod.rs).
- A/B on main
|
||
|
|
69cddc0156 |
docs(changelog): credit the earlier pull requests for three unreleased fixes (#2956)
While closing first-time contributors' pull requests whose fixes had already landed, three were found to predate the fix that shipped, and their close comments promised changelog credit: - @meganemura's PR 1503 wrote installer config through symlinked files 2.5 months before the fix for issue 2721. - @rudycelekli's PR 2251 preserved Hermes settings after the MCP server block three weeks before the fix for issue 2485. - @equipados's PR 1641 linked unqualified paren-less Delphi calls six weeks before the fix for issue 2514. Changelog only; no code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
25776014e9 |
fix(resolution): a call on the caller's own class reaches that class's member, or what it inherits
Root cause: a bare call in Java or Scala, and a `self.`/`this.` call in Python, TypeScript and JavaScript, reaches matchByExactName as a bare name. With several same-named methods in reach, findBestMatch ranked them by file and line distance, so a sibling class's method declared nearer the call won over the caller's own class's or the one it inherits; above the 500 candidate ceiling the call was dropped. Fix (contributor): matchEnclosingScopeMember runs before the ranking. It reads the receiver back from the call line (bare, self/this, super) and walks the caller's scopes innermost-out: the own class, then what it inherits, then, for a bare call, the classes it is nested in. A member found there is the answer (0.9), and its overloads are ranked among themselves. A super call in a JS class expression is left to js-super-calls (PR 2497). Ours: - Inherited members first come from the declaration's own resolved supertypes (getSupertypeNodes), in the order its header lists them, a class further up before an interface that only declares the member; the PR's walk by name stays as the fallback when those edges reach nothing (an `extends` the index resolved to a namesake, as Minestom's Player to a nested EntityStatuses.LivingEntity). By name alone, Django's spatialite, oracle and mysql GIS editors reached the postgresql backend's DatabaseSchemaEditor.execute and _delete_index_sql (0.9), a class with two bases took either one's member (BaseDeleteView(DeletionMixin, FormMixin) got FormMixin.get_success_url), and SystemUI's RotationContextButton got RotationButton's declaration instead of ButtonDispatcher.getCurrentView. - Only `super`, `this`/`self` and Python's `cls` name the receiver. The PR also read `parent.`/`base.` as super and `cls.`/`static.` as self, but Java's extractor drops a receiver variable of those names too: protobuf's `parent.getFullName()` on an EnumDescriptor went to GenericDescriptor. - C++ is left to matchCppImplicitThisCall (PR 2479), which already looks members up in the class, its bases and its enclosing classes. The PR's C++ branch added the caller's own file's namespace functions, which beat the overload the arguments fit: fmt's 3-argument `copy<Char>(b, e, out)` calls went to format.h's 2-argument copy (0.9). - Tests for each, and the CHANGELOG bullet in the house format. Validation: enclosing-class-member-call.test.ts has 12 cases: 6 fail against main's resolver, and all 5 of ours fail against the PR as written. Kernel A/B against main |
||
|
|
760e31f2b4 |
fix(install): install.ps1 resolves the latest release without GitHub's rate-limited API (#2939)
install.ps1 resolved "latest" with one Invoke-RestMethod to the GitHub API, with no fallback, under ErrorActionPreference Stop. The API allows 60 unauthenticated requests an hour per IP and then answers 403, so `irm .../install.ps1 | iex` died with a raw "(403) Forbidden" on shared networks and in CI. A pinned CODEGRAPH_VERSION=1.6.2 was used as-is, so the download URL lacked the v that release tags carry and 404ed. install.sh (PR #336) and `codegraph upgrade` (resolveLatestVersion) already handle both; install.ps1 was never ported. Fix: step 2 is now a function between Resolve-CodeGraphVersion markers. It sends a HEAD to github.com/<repo>/releases/latest with System.Net.HttpWebRequest and AllowAutoRedirect off, reads the tag from the Location header with /releases/tag/([^/?#]+), falls back to the API inside try/catch, adds the v to a bare version, and keeps the friendly throw when both fail. Install-CodeGraphFiles is untouched (still equal to WINDOWS_SWAP_FUNCTION). Validation (Windows PowerShell 5.1.26100; pwsh 7 is not installed here): - Each arm's unmodified step 2, with Invoke-RestMethod shadowed so the API answers a local 403: main stops with "(403) Forbidden"; the fix resolves v1.6.2 from the real redirect with no API call. CODEGRAPH_VERSION=1.6.2: main builds .../download/1.6.2/... (HEAD 404), the fix .../download/v1.6.2/... (HEAD 302). With the API available, main makes one API call and the fix none. - New Windows-gated test runs the shipped resolver block in PowerShell against a local stand-in for github.com and the API: redirect read with one HEAD and no follow, API fallback for a 500 or a missing Location, the friendly error (no raw 403) when both fail, the v added to a pinned version with no request. 4 fail on main's install.ps1, 4 pass with the fix; upgrade.test.ts (the swap function pin) passes. - Both scripts parse with no errors in PowerShell 5.1. The install steps were not run: they always add the install to the user PATH. Reported in PR #560 by @hiyochi; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6162d872be |
fix(watcher): a deleted link target no longer pins a CPU core on Windows (#2938)
On Windows, libuv reports a watched folder that is being deleted by the folder's own absolute path (\\?\D:\...) and re-arms the read, which fails the same way at once: about 70,000 events in half a second until the stream is closed. The watcher gives each directory link in a project its own recursive stream, so deleting a link's target outside the project started that storm. handleDirEvent joined the absolute name onto the watched folder, got a path that doesn't exist, scheduled a full sync and nothing else. The stream was never closed, the server held a core until restart, and the storm kept re-arming the debounce, so no sync ever ran: the deleted files stayed in the index and later edits never auto-synced. The project root's stream had the same blind spot. Fix: handleDirEvent, which both stream callbacks use, now treats an absolute name as the watched folder itself reporting it is gone. The first such event on a stream arms one check 200 ms later; the rest of the burst finds it armed. The check closes a link's stream when the link no longer leads to the folder it watched (deleted, or pointed elsewhere) and re-reads the tree, whose full sync drops the link's files; a link that leads somewhere live is watched anew. A project root that is gone degrades the watcher with a "project folder was deleted or moved away" reason, which closes every stream. A normal event pays one path.isAbsolute check. Validation (Windows 11, Node 22.22, libuv 1.51): - Raw probe: fs.watch on a junction whose target is deleted gets 70,009 events named \\?\<the junction> in 500 ms (499 ms CPU), and none after close(). - Real MCP server over a project with a junction libs -> outside, outside deleted: main used 3484, 3406, 3391, 3281 ms of CPU per 3 s window and kept the linked symbol; an edit afterwards never synced. With the fix: 296, 0, 0, 79 ms; the linked symbol left the index; the edit synced. - New watcher tests: a fake fs.watch link storm (10,000 self-named events cost no fs call, the stream closes once, a full sync runs, the project stays watched), a fake root storm (degrades once, closes the stream), and a Windows-gated real junction whose storm must go quiet. All 3 fail on main's watcher.ts; watcher.test.ts passes 61/61. Reported in PR #2487 by @L0garithmic; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
31ec908224 |
fix(sync): a long-running session picks up a framework added while it runs (#2941)
The MCP server's watcher holds one CodeGraph for the whole session, and its framework detection was cached from the first time it ran: the orchestrator reset it only in indexAll, and CodeGraph.sync never re-initialized the resolver. A framework that arrived mid-session, like Express in a new package.json, was missing from every file synced after it, so a new route file got no route nodes until `codegraph index`. The resolver's declared-dependencies cache was also keyed by the indexed file COUNT (PR #2519): swapping one app for another of the same size, or editing a manifest, kept the old dependency set, even through reinitializeResolver(). Detection now records what it read: indexed source files, which a sync's change lists cover, and every other path it read or probed, with a size and mtime signature, including the per-app manifests the extractor gating reads. Before a sync extracts files, it detects again only when files were added or removed since, a source file detection read changed, or one of those paths changed on disk. A scoped sync takes the file list from the index plus the files it adds, so it never walks the project (the first sync in a process used to run a full scan for its first detection). What detection read is kept between detections and dropped by the same change lists and signatures, so detecting again reads only what changed. When the detected set differs from the resolver's, the sync re-initializes the resolver. Its initialize() now clears its caches before detecting, and clearCaches() forgets the declared-dependencies cache with the other per-context memos. Files indexed before a framework change are not re-extracted. A framework like Express applies to every file of its language, and re-reading them all inside a watcher's sync would cost what a full index does. Instead a full index records the frameworks it used, a sync records how detection moved since, and `codegraph status` (text, and --json with reindexRecommended and frameworksChangedSinceIndex) recommends `codegraph index` while they differ. Status only reads that record. Validation: - sync-framework-detection.test.ts (new, 8 cases): Express arriving with a new route file, by full and by scoped sync; a .svelte file added with no manifest change; an edited source file that detection read; a manifest-only edit picked up by the next sync, with the status record, cleared on revert and by a full re-index; a fresh process's scoped syncs detecting once, from the index, with no scan; the equal-size app swap; and the dependency cache forgetting on request. 7 of 8 fail on main's source (the 8th needs the new export). Ten mutations, one per mechanism, each fail a test. - The triage probes: main detects [] after Express arrives and routes.js gets no route; the fix detects express and extracts GET /hello, by full and by scoped sync. For the swap, main's reinitializeResolver() gives [react] and the fix [express]. - Cost on excalidraw (737 files, scoped syncs in one long-lived instance): the detection step takes 8 ms on a sync where nothing it read changed and 8-13 ms when it detects again, against syncs of about 260 ms (add) to 2 s (modify). The first sync in a process detects once (about 300 ms) and no longer scans the project. Reported in PRs #2516 and #2519 by @nk33-dev. This reimplements both fixes independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7cceba52b0 |
fix(resolution): library calls on untyped Python, Scala and Java values stay unlinked
Root cause: when nothing types a call's receiver, matchMethodCall's last
strategy links `recv.m()` to the one project method named `m` (0.7), or to
the best receiver-word match among several. A standard-library call on a
plain value - `options.setdefault(...)` on a dict, `m.getOrElse(...)` on an
Option, `data.put(...)` on a map in a `var` - then linked to an unrelated
project method of that name. Go, Rust, Kotlin, C#, VB.NET and Dart already
keep such a guess only when the receiver names the method's owner
(stdMethodNames); Python, Scala and Java had no list.
Fix (contributor): Python, Scala and Java standard-library method lists in
stdMethodNames. Ours: the PR also exempted a receiver named `cls` from the
rule; both extractors already strip `cls.` from Python calls, so it never
applied there, while in C#, Dart, Rust and VB.NET it let
`var cls = Make(); cls.ToString()` link to an unrelated Money.ToString.
Removed, with a C# test case. CHANGELOG bullet rewritten and moved next to
the Python built-in method bullets.
Validation: library-method-calls.test.ts: 3 of its 5 cases fail on main
(Python build, Scala go, Java untyped), all 5 pass; the C# case fails when
`cls` is put back. Kernel A/B against main
|
||
|
|
45a29b4685 |
fix(worktree): a linked worktree is recognized however Windows spells its path (#2937)
On Windows one directory has several spellings. VS Code and Cursor pass a
lowercase drive letter, NTFS ignores case, a subst drive is a second name
for a folder, and long names have 8.3 aliases. git answers with the
spelling on disk, but src/sync/worktree.ts compared its paths with the
caller's as strings, after a JS realpath that keeps the caller's spelling.
The main checkout's git common dir comes back relative (".git"), so it
took the caller's spelling while a linked worktree's came back absolute
in git's. A worktree inside the indexed checkout, spelled unlike git,
failed all four comparisons:
- detectWorktreeIndexMismatch found no mismatch, so codegraph status
(text and --json) dropped the borrowed-index warning;
- nestedRepositoryBelow took the worktree for a different repository, so
the MCP tools refused it as unindexed and told the agent not to call
codegraph for it again.
Fix: the four comparisons now ask whether two paths name one directory,
with the identity canonicalProjectRoot already uses (the native realpath,
lowercased on Windows), falling back to the JS realpath where the native
call fails. Returned paths keep the caller's spelling for display. A
nested repository below an index root outside git is still reported, as
before.
Validation (Windows 11, Node 22):
- New Windows-gated tests with a lowercase drive letter, at the helper
level and through ToolHandler (search and status, with and without a
default project): the 3 behaviour tests fail on main's worktree.ts and
pass with the fix. Both files: 32 passed.
- Fixture: a checkout with a worktree under .claude/worktrees and an
embedded clone, spelled four ways (on-disk, lowercase drive, other
capitals, subst drive). Helper verdicts wrong: 6 on main, 0 with the
fix. codegraph status warned in 2 of 8 runs on main, 8 of 8 with the
fix. The real MCP server refused the lowercase, capitals and subst
spellings on main; with the fix it serves all four with the
borrowed-index notice, codegraph_status warns, and the embedded clone
is still refused. An 8.3 short name (the contributor's case) went from
2 wrong verdicts to 0.
Reported in PR #2517 by @nk33-dev; this reimplements the fix
independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
475e56b209 |
fix(cli): read commands and the prompt hook open the index read-only (#2936)
Running query, files, node, callers, callees, impact, context, explore or affected while `codegraph index` rebuilt a project could make the rebuild fail with "database is locked", and so could the hidden prompt-hook, which Claude Code runs on its own for structural prompts. Each opened the index read-write, so its open ran the writer's repairs: during the rebuild's bulk load they recreated the secondary indexes the indexer had dropped, and the indexer's next write, whose transaction had already read, failed with SQLITE_BUSY_SNAPSHOT (517). PR #2930 fixed the same thing for status. These commands now open the index read-only, through one helper, and leave migrations, index recovery and WAL maintenance to the writer: - query, files, node, callers, callees, impact, context, explore, affected and prompt-hook are read-only. explore and node run the MCP tool handler in process without a lifecycle, so they never sync or open another project. - While another live process holds rebuild.pid they say so in one line on stderr (a --json answer stays parseable), as status does. - The prompt hook stays out of a running rebuild altogether: the index is half-built until it ends, the MCP tools answer "rebuild in progress" meanwhile, and not opening it keeps the hook from holding the file the rebuild replaces. Its gate records the new outcome noop-rebuild. - The hook's one write, filling an empty segment vocabulary (issue #1142), goes through a writer connection of its own, opened only once it holds the index lock, which every index and sync holds while it writes. - status (read-only since PR #2930), sync, index, init and uninit are unchanged; the last four write. A read-only open migrates nothing. QueryBuilder.hasSchema lets each read that needs a later table or column answer as that migration's defaults would: files.generated (v9), unresolved_refs.status and name_tail (v8), and name_segment_vocab (v7). The status pending count from PR #2930 uses it too. Validation (Windows 11, Node 22.22): - The issue's interleaving, each command in another process inside the writer's edge transaction: on main all 14 command forms make the insert fail with errcode 517 and bring the 5 dropped edge indexes back; with this change all 14 inserts succeed and the indexes stay dropped. - Real rebuilds of excalidraw 95307fa1 (736 files) with query and the prompt hook looping beside them, default settings and with the resolution window forced: main failed 6 of 14 ("store worker: database is locked" with an empty index, or "database is locked" partway through resolution), and 12 of its 109 query runs failed at open with SQLITE_BUSY. This change failed 0 of 14, with 0 failures in 299 query and 284 hook runs and the same graph every time (14,236 nodes, 61,210 edges). - Old indexes: 24 command forms on indexes at schemas 4 to 16 (11 schemas rebuilt from a current index, plus indexes built by 0.9.9, 1.4.0, 1.5.0, 1.6.0 and 1.6.2): output identical to main's, which migrates first, 384 of 384. The schema stays as it was, except where the hook fills the vocabulary of a pre-v7 index; that writer's open migrates it, as every read-write open did. - Timing on excalidraw at rest, 22 interleaved runs per command: medians within -5.8% to +3.3%, -0.9% in total. - read-commands-during-rebuild.test.ts: 38/38 pass. With main's sources 35 fail; with only the CLI opens reverted 33, only the schema fallbacks 19, only the read-only vocabulary fill 2. Follow-up to PR #2930. Fixes #2913 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cbd9817f7c |
fix(extraction): a damaged C #if group no longer turns into functions named if
tree-sitter-c parses every branch of a `#if … #elif … #else … #endif`
group as a run of block items, so a group is harmless only when each
branch is complete on its own. Three shapes in real C are not: a branch
that begins with `else` (an arm of an if-chain kept behind `#ifdef`), a
branch that ends with a bare `if (…)` header whose body follows the
`#endif` (ST's HAL latency tables), and a branch whose braces don't
balance (a function head that differs per build). Each leaves a
K&R-shaped `if (…) { … }` that the grammar reads as a function named
`if`, filed inside the enclosing function, and the functions after it
were nested under it or lost. Separately, a block macro in capitals in
statement position (`ATOMIC_BLOCK(NVIC_PRIO_MAX) {`) was skipped by the
statement-macro blank, which matched lowercase iterator macros only.
blankCUnbalancedConditionalBranches runs at the end of preParseCSource,
after the directive restore, since it edits directive lines. A group
where a branch shows one of those shapes, or is a fragment of an
expression or a list (`|| (h4 == NULL)` inside an `if (…)`), loses its
directive lines, offsets kept. When a branch can't stand next to the
others, one branch is kept verbatim: the `#else` of a positive test
(`#ifdef X`, `#if defined(X)`), the first of `#ifndef`, the live one of
`#if 0`. The statement-macro blank also takes ALL_CAPS names (PascalCase
still excluded, for C++ constructors). Both engines see it: the kernel
gets the pre-parsed bytes from the TS route point.
Ours on top:
- A group on a flag whose branches each balance (braces and parens)
keeps every branch, only its directive lines blanked: read in a row
they are still C (`} else if (a) {…} else if (b) {…}`, list entries,
`|| x || y`). Dropping one lost real code of builds the project uses:
betaflight's failsafe.c arms under `#if ENABLE_RESCUE_PLAN` (default-on
in common_post.h), and adc_stm32h7xx.c's adcTagMap, whose inner MCU
chain kept its `#else #error` branch. Unbalanced and literal groups
keep one branch, as above. The PR's first unit test now expects both
arms of its balanced else-led group.
- The group is read on a copy with comments and literals masked. A brace
inside a block comment over several lines counted as code, so Lua's
`** {======` banners made loadlib.c's platform group look damaged, and
its dlopen, Windows and dyld implementations were dropped for the stub;
on betaflight it dropped 19 CMSIS-DSP MVE functions.
- A line splice is read on the raw line: a `\` at the end of a comment
line inside a #define body still continues the #define (masking hid
it, which made jemalloc's tsd.h include guard look damaged).
- Only a directive's own `\` continuations are blanked with it. The PR
blanked every directive line right under a collapsed group's `#if` or
`#else`, so a kept branch lost a leading `#include` or `#define`.
- `#if 1` keeps its first branch; it kept the dead `#else`.
- Unit tests for each, mutation-checked; CHANGELOG bullet rewritten
without numbers.
- Tried and dropped: keeping the feature branch of a flag the file
#defines itself (the same-file evidence PR #2615 uses). Applied per
group, it broke dtoa's coordinated groups in jq (`#ifdef
Avoid_Underflow` kept its branch while a later `#ifndef
Avoid_Underflow` still kept its `}`), cutting jvp_strtod in half.
Validation, main
|
||
|
|
c30696703d |
fix(cfnptr): a C function-pointer table links only to C and C++ functions
resolveFn in the C function-pointer pass looked a registered handler's
name up across every language, filtered only to function and method
kinds. In a repository that also holds Rust or Python, a C table could
link to a same-named Rust or Python function, and when that function
sorted first it also took the place of the real C handler.
The fix keeps candidates in C and C++ files only, using the pass's own
C_CPP_EXT, the file set it scans for tables and dispatch. Headers the
language detector labels Objective-C still count. Objective-C `.m` and
`.mm` files stay out, like they are out of the scan: a table in a `.m`
file is never read, and allowing them would let an Objective-C method
named `init` or `read` stand in for a C handler of that name.
On GLFW, which mixes C and Objective-C, main links 354 table entries,
none into a .m file; its one C table naming a .m function
(_glfwConnectCocoa) is an anonymous-struct table the pass doesn't
bridge on main either.
Validation, main
|
||
|
|
1cb4aab913 |
fix(status): a project below its repository root sees its uncommitted changes (#2932)
When the indexed project is a folder of its git repository (a monorepo package, an app/ folder beside a server/ one), `codegraph status`, `status --json` and the freshness check behind codegraph_status reported the index up to date while `codegraph sync` found the very same changes. `git status --porcelain` prints paths from the repository root whatever folder it runs in, and reports the whole repository. Change detection joined `app/src/index.js` under the project, found nothing at `app/app/src/index.js`, and dropped it. The dirty paths a sync records for the restore check (from PR #1878) were stored the same way, so a file put back with git after a sync was missed too. And a project in a folder its repository gitignores got a permanently clean answer from git, which says nothing about an ignored folder. Change detection now asks where the project sits in its repository (`git rev-parse --show-prefix`, kept per folder, and skipped when the folder has its own .git). Below the root it runs git status with a `-- .` pathspec, takes the prefix off every path and leaves out anything outside the folder. The project's ignore rules match those project-relative paths, the recorded dirty paths are project-relative, and a project its repository ignores declines the git path as the scan already did, so callers take the full scan. A project at its repository root runs exactly the git command it ran before. Validation: - git-status-subdir-project.test.ts (new, 7 cases): edited, added and deleted files in a subdir project, matching sync; edits outside the project left out, with the pathspec asserted; the restore case, across a reopen; the project's own anchored .gitignore rule; an embedded repo inside the project; the gitignored-project fallback; and a repo-root project running exactly today's status command with no prefix call. 6 fail on main's source; the repo-root control passes on both. Four mutations (no pathspec, ignore rules on repo-relative paths, no fallback, no .git shortcut) each fail the test. - CLI repros: after an edit and an added file in app/, status --json reads 0/0/0 on main and 1 added + 1 modified with the fix; the same in a gitignored project; a subdir restore reads 0 on main and 1 modified with the fix (a repo-root project reads 1 on both). - The MCP freshness worker on a subdir project: 0/0/0 on main, 1 added + 1 modified with the fix. Reported in PR #1636 by @innoxxicide. This reimplements the fix independently, on top of the restore check that landed after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0174f804c3 |
perf(resolution): read a bare JS/TS call's local binding once per match — graph byte-identical
matchByExactName asked isLocallyBoundJsName for every cross-file candidate of a bare JavaScript or TypeScript call, though the answer depends only on the reference's name and file, which are the same for every candidate. The helper memoizes it per file and name, but each call still built the key and read the memo. The answer is now kept in a lazy local for the one match, at the same place in the filter, so candidates, order, scoring and confidence are untouched. Validation: fresh indexes of codegraph's own source, excalidraw, vben, express and zod dump byte-identical to main's (nodes, edges with their metadata, unresolved refs, files), and express stays identical through sync after a JS file is added, edited and deleted. A CPU profile of indexing codegraph's own source (all threads, two runs per side) puts isLocallyBoundJsName's self time at 150-173 ms on main and 45-46 ms here; the scans it guards (bindsAtSites) are unchanged. Whole-index CPU on vben, 10 interleaved runs, moves less than its noise. The PR's 8 tests pin the matcher's behaviour and pass here and on main. Original PR #2871 by @luojiyin1987. Fixes #2870 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2afee00cb0 |
fix(viewer): a file with % in its name opens as itself (#2933)
codegraph ui couldn't show a file whose name contains a %: 100%.ts was refused as an unusable path, and a%20b.ts was served as changed on disk with no lines. resolveProjectFile, the chokepoint for every file the viewer reads, percent-decoded its path. But that path is either one the index stored or a query value the URL parser had already decoded once, so this was a second decode. It threw on 100%.ts and turned a%20b.ts into a b.ts, while resolveRequestedFile looked the file up in the index by the name as given: the bytes of a b.ts were hashed against a%20b.ts's record. The chokepoint no longer decodes. It refuses raw control bytes, and on Windows a backslash, then checks containment as before. Raw request paths (the viewer's own assets) are still decoded where they are read. The File view's /api/file/<path>, whose path the HTTP layer also decodes once, had the same double decode and is fixed by the same change. The POSIX half of PR #2009 (backslashes kept in names on POSIX) is left out, as it needs a wider change and a POSIX run. Validation on Windows 11, with a fixture holding src/a%20b.ts, src/a b.ts and src/100%.ts: - codegraph ui over HTTP, /api/source with the query a browser sends: main serves a%20b.ts as drift=true with no lines and answers 403 for 100%.ts; the fix serves all three files as themselves, drift=false. - Tests: 2 new tests (the chokepoint, and /api/source plus /api/file/<path> over HTTP) fail on main's source and pass with the fix. Two existing cases that fed an encoded ..%2f and %00 straight to the chokepoint now go through the URL parser's single decode, as requests do, and are still refused. Reported in PR #2009 by @shrankr; this reimplements the viewer half of the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
983c4df374 |
fix(index): a deleted file git still lists is skipped, not reported as unreadable (#2928)
A full index reads every path the scan lists, and `git ls-files` keeps listing a tracked file deleted from the working tree until the deletion is staged. So `codegraph init` and `codegraph index` stat'ed the missing file, got ENOENT and reported "1 files could not be read" with an errors.log entry. The errored file also kept the run from recording the commit it indexed, so `codegraph status` fell back to a full scan afterwards. The batch reader in indexAll and the single-file indexFile now take ENOENT and ENOTDIR to mean nothing is there: the file is skipped and counted as skipped, so the run reads as complete rather than PARTIAL. A symlink that points at nothing is skipped the same way, as the directory walk for projects without git already did. Every other read failure (EACCES, EIO, a folder where a file was) is still reported. On POSIX a path under a file fails realpath with ENOTDIR, which validatePathWithinRoot rejected as if it escaped the root; it is now judged like any missing leaf, by its nearest existing ancestor, so it reaches the reader and is skipped too. Discovery is unchanged, with no extra stat per tracked file. Validation: - index-gone-tracked-files.test.ts (new): all 5 cases fail on main's source and pass with the fix: committed and staged deletions, a folder replaced by a file, a dangling symlink, and EACCES still reported by both the full index and indexFiles. Mutating the single-file path, the skipped tally or the EACCES boundary fails the test each time. - security.test.ts: a path under an existing file validates, and one under a symlink to a file outside the root is still refused. - The issue's CLI repro: main prints "1 files could not be read" and writes errors.log; the fix prints "Indexed 1 files", writes no errors.log, and status reports nothing pending. Reported in PR #2910 by @xianzhi-xzw, who also filed issue #2904. This reimplements the fix independently, at read time rather than with an lstat of every tracked file during discovery. Fixes #2904 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |