mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
de0a4eb362bbe4deb412f79aedcce11ee036dfbc
638
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc17978174 |
fix(rust): a module a #[path] attribute loads is the module that declares it (#3064)
A Rust module loaded with `#[path = "…"]` was found by its file's directory:
fast-mlsirm's `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`
in `crates/core/src/scaling.rs` made `tests/unit/scaling_tests.rs`'s
`use super::*` walk to a `tests/unit` module that doesn't exist, so its
`predict_rating_multi(1)` linked to nothing, or to a same-named function of
another crate; its `crate::` had no crate, and the parent's private items
looked out of its reach. From the declaring side, a path through such a
module (`utils::mock_event(…)` under `#[path = "./utils.rs"] mod utils;`)
reached nothing.
The fix reads each file's `#[path]` declarations as the Rust Reference
reads the attribute (from the declaring file's directory, or inside an
inline `mod` from that module's own; a path that leaves the project, by
escapesRoot, or that a `cfg_attr` sets, names no file):
- rustSubmodule: a module the module declares once with a `#[path]` is
the file it names, whatever else loads that file too (a helper several
tests load is each test's); cfg alternatives that name a file each stay
unknown, and a path's 2015 crate-root fallback no longer reaches past
such a module to a namesake at the crate root.
- rustPathClaim: a file the regular rules don't place is the module of the
one `#[path]` declaration that loads it, if exactly one does. Placed
(rustPlacedByName) are crate roots, files with a `fn main` or
`#![no_main]` of their own (trybuild cases, fuzz targets), and files a
plain `mod name;` declares in the module owning their directory or in an
inline module of one above it (dioxus's `mod components { pub mod home; }`
in main.rs). Only for a file left over are the project's Rust files that
write `path =` read (a byte check first), once per resolver. With a
claim, rustSuperModule is the declaring module, rustFileModule owns the
file's directory (rustc reads every `#[path]` file as a mod.rs),
rustCrateRootModule is the declaring file's crate, and rustModuleWithin,
isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the
declaring module, so `use super::*` brings in its private items.
- rustCrateGraph: `[[example]]`, `[[test]]` and `[[bench]]` paths are
target roots, as `[lib]`/`[[bin]]` ones are since PR #3014, and such an
example is its own crate's root (dioxus's
`examples/07-fullstack/handling_errors.rs`).
Validation:
- rust-path-super-glob.test.ts (PR 1892's case, credited) and
rust-path-attribute-modules.test.ts (7 cases): on main all 8 fail; all
pass. Every rust-* suite passes.
- A/B against main, native kernel: fast-mlsirm (its Rust files) +2,796
links (1,839 calls, 427 references, 334 constructions, 196 imports), 71
retargeted (exposure_tests.rs's `Lcg::next_f64` from cdm_tests.rs's
private `Lcg` to exposure.rs's, the declaring module's helpers instead
of bifactor_grm.rs's), 87 reaching the same item through the declaring
module (confidence 0.4 to 0.9), 0 removed; dioxus +27 (each headless
test's `utils::mock_event(…)` into the helper five tests load); tokio +2
(`use super::AtomicU64` in the files atomic_u64_as_mutex.rs loads);
ripgrep, axum, clap, serde, gleam, express and flask unchanged.
- Cost, native kernel, medians of 3 interleaved runs: fast-mlsirm CPU 15.3
-> 15.8 s, wall 4.2 -> 5.2 s, peak RSS 825 -> 882 MB; tokio CPU 26.1 ->
26.7 s, wall 10.4 -> 11.2 s, peak RSS 1,023 -> 1,028 MB; dioxus CPU 34.8
-> 35.6 s, wall 10.3 -> 11.2 s, peak RSS 1,125 -> 1,120 MB; gleam CPU
24.1 -> 23.8 s, wall 6.6 -> 6.7 s, peak RSS 1,027 -> 1,023 MB.
Builds on PR #1892 by @seonghobae.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
ed6b88e53a |
fix(js): an export default line inside a template literal no longer names the default (#3068)
statedDefaultExport reads a JS/TS file's own export default statement
by scanning comment-stripped source for line-initial `export default`
heads. Strings are deliberately not masked there (a masker that loses
step on nested templates blanked real statements), and a head counted
when it named a node of the file, the first such head winning. A code
generator's template text breaks that: in
export function App() {} export function Main() {}
export const template = `
export default App
`;
export default Main;
the template's line names the real App, so `import X from './gen'`
bound to App instead of Main (issue #2968). A template holding
`export default () => …` also made a module whose real default is an
expression read as anonymous, which keeps every guess away.
templateLiteralTextAt (strip-comments.ts) scans the source once for
template-literal text: it follows `${…}` nesting (a template inside an
interpolation, a brace inside a regex there), and skips comments,
quoted strings and regex literals, which may hold a backtick; a `/`
after a value divides, anywhere else it opens a regex. A head inside
template text is now skipped. When some template never closes the scan
vouches for nothing and every head counts, as before. Nothing else
changes: a head outside template text is read exactly as it was.
Validation: default-export-declaration.test.ts gains the issue's file,
a template returned by a function behind a nested template, one passed
at module scope and one in a class field, a template's anonymous
default above an expression default, and controls (a regex and a string
holding a backtick, a division after a parenthesis, and a file whose
scan can't close a template); strip-comments.test.ts tests the scan
directly. Red with main's resolver: 2 of 10 failed; green: all pass.
Checked against tree-sitter on every line-initial head of 4,191
JS/TS files in 16 corpora (outline, excalidraw, vben, bitwarden apps,
mastodon, shadcn docs, zod, bulletproof-react, express, astro, react,
trezor-suite, this repo's src, tests and ui): of 4,149 real export
default statements the scan puts none inside template text, and all
322 heads it does place there are template text. A/B on excalidraw
d3a9c2a1, vben 61c07936, astro e3bec3d2 (packages/astro/src and
packages/integrations), with gin-gonic/gin 0f09c3a9 and psf/requests
3c7baaa0 as controls, both arms on main
|
||
|
|
664492551d |
fix(sync): a barrel whose re-export points elsewhere re-links its importers
`c.ts` imports `foo` through `index.ts`, which re-exports it from `a.ts`.
Change the clause to `./b` and sync, and `bar` kept calling `a.ts:foo`
where a fresh index gives `b.ts:foo`. The barrel declares nothing of
`foo`, so the definition delta has nothing to rebind; no file was added
for the walks below a barrel or the added-files pass; the binding never
failed, so the retries never see it; and the name-matched-import pass
reads only bindings a name match took. `export *` moved the same way, and
a scoped sync of the barrel alone too.
The orchestrator now reports the modified ES modules whose `export … from`
statements changed, read off their import nodes (named by the module, with
the statement as signature) before and after the store. Their importers'
bindings through them go back for the sweep, up through the modules that
pass them on in turn, with the failures parked under them and the edges
into the members of what the bindings pointed at (`w.run()` with
`w: Widget`). It is the importer walk the removed-files pass uses, run
after the name-matched-import pass.
Review changes: the trigger compares the `export … from` statements
themselves. The original compared the modules a file imports and counted a
module that lost any import, so dropping a plain import from excalidraw's
`types.ts` (218 importers, no re-exports) re-opened 2,186 refs for the same
graph; it now re-opens none. Comparing statements also catches a name
dropped from a clause whose module stays (`export { foo, qux } from './a'`
to `export { foo } from './a'`), which the module set missed. Plus tests
for both, and the doc comments the new code sat between moved back.
Validation: 9 tests; on main's source 7 fail (the 2 controls pass), and
on the original trigger the 2 new ones fail. 15 fixture edits, each
compared with a fresh index, in a later process and in the same process:
main diverged in 8 of each, this in none. excalidraw with
`export * from "./utils"` in its math barrel (116 importers) retargeted to
`./constants`: main was 224 edges off, identical here over nodes, edges,
refs and files. Fresh indexes are identical to main's.
Original PR #2985 by @danusha2345.
Fixes #2978
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
80f6b50504 |
fix(sync): a tsconfig, jsconfig or go.mod change re-resolves the imports it governs
The resolver read tsconfig / jsconfig `paths` and `baseUrl` and go.mod's
module path once and kept the maps for its lifetime ("callers re-create the
resolver if config changes", which none did), and the watcher follows source
files only. So after an edit to one of them a long-running MCP server kept
resolving imports by the old mapping, and so did later processes: a sync
re-resolves only the changed files, and a config-only edit changes none.
The index now carries a snapshot of the config files resolution read: every
tsconfig / jsconfig the alias loaders opened, `extends` chains included,
every go.mod, and the root's candidates whether present or not. Every sync
reads them again and, when one differs, drops the resolver's alias and
module maps with the memos resolved through them, re-opens every import the
file governs (the ES-module family for a tsconfig / jsconfig, Go for a
go.mod) for the sweep, as a moved workspace map does, and marks synthesis
for a rebuild. An index an older engine stamped records a snapshot without
re-opening anything.
Review changes: each file's digest covers what resolution reads from it, a
go.mod's module path and a tsconfig / jsconfig's `extends`, `baseUrl` and
`paths`, not its bytes. A dependency bump in go.mod or a `strict` flag in
tsconfig had re-opened every import of the language for the same graph
(caddy: 48,945 refs, sync 3.8 s against 0.9 s; excalidraw: 88,860 refs,
9.2 s against 1.1 s). Plus a test for that, the ReferenceResolver class doc
comment back on its class, and the CHANGELOG entry beside the workspace-map
one.
Validation: 7 tests; on main's source the 5 behaviour tests fail, and the
new one fails on the byte digest. 11 fixture edits, each compared with a
fresh index of the same tree, in a later process and in the same process:
main diverged in 7 and 9, this in none. Real repos, sync against a fresh
index over nodes, edges, refs and files: excalidraw with `@excalidraw/math`
retargeted was 27 edges off on main and is identical here; caddy with its
module path renamed was 6,511 edges off and is identical here. Fresh indexes
are identical to main's. A one-file sync on excalidraw, median of 5
interleaved runs: main 3,834 ms, this 3,269 ms.
Original PR #2984 by @danusha2345.
Fixes #2977
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
7ff2841e37 |
fix(resolution): ties that decide a link break by code unit, not the host's locale
Several resolution tie-breaks sorted paths or ids with a bare localeCompare, which follows the host's locale, so two machines could index the same repository differently: under Czech collation `ch/` sorts after `h/`, under Danish `aa` sorts after `z`, and locale collation puts `alpha/` before `Beta/` where code-unit order does the opposite. The PR fixes the nearest-declaration tie in tsFieldCall, for `this.field.method()` on a type declared in two equally near apps. On top, the same bug at the three other sites that decide resolution: - cpp-type-aliases: the declaration an alias resolves to when two headers declare it differently, for a C++ base class named through it; - frameworks/react-native: the Swift copy of a module's class that a JS-to-native call reaches; - go-func-field-synthesizer: the last tie-break of wiringOrder, which decides the values a capped channel keeps (its site comparison already names 'en' and stays). All compare by code unit now. Comparators that only order output (CLI, MCP and viewer listings, dead-code and hierarchy order, the context builder's copybook entries) are left alone. The CHANGELOG bullet moves from the released 1.6.1 section to Unreleased. Validation: a test each for the TS, C++ and React Native sites; all 3 fail on main and pass here. Indexing `ch/` vs `h/` fixtures for those three under a simulated English and a simulated Czech host (a localeCompare preload that reaches the worker threads), main links different targets in all three, this commit the same in both. A/B, main vs this commit on an English-locale host, 12 repos (excalidraw, zod, vben, bulletproof-react, jq, cobra, gin, etcd, prometheus, okhttp, with flask and spring-modulith as controls): every dump byte-identical. Indexed again under a simulated Danish host (the Go repos) and Czech host (the TS repos), both arms match their English dumps, so these repos reach no tie the locale decides; the fix matters where one does. Original PR #1983 by @bompus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4eb8c2a5f2 |
perf(synthesis): line numbers from a newline index, not a split per match
Two quadratic idioms in callback-synthesizer.ts. Twelve passes computed
each match's line as `safe.slice(0, m.index).split('\n').length`, which
copies and splits the file's prefix for every match, and `sliceLines`
split the whole file again for every node a pass looked at (7 callers,
one per node of the same file in a row).
The twelve sites now call `lineOf(src, idx)`, which keeps the newline
index of the last text (built with synth-utils' `makeLineAt`, which
counts the newlines strictly before `idx`, exactly like the old idiom),
and `sliceLines` keeps the last file's lines.
Ours on top: `releaseLineCaches()` at the end of synthesizeCallbackEdges.
The pool workers' copies go with the workers (pool.destroy() after
synthesis), but a sync runs synthesis on the main thread, so a long-lived
MCP server or `watch` kept the last file's text, its lines and its newline
index until the next synthesis. And a celery test with two files of the
same length scanned in a row, which fails if the line index is reused
across texts (checked by keying the cache on length).
Validation, main
|
||
|
|
0b194448dc |
fix(resolution): a call a method hands on to another object no longer links back to it
matchMethodCall's strategy 3 guesses a method by its name when the
receiver's type is unknown. With one method of that name in the project
it was taken outright, so a wrapper handing its call on, like Play's
`def acceptsURL(url) = proxied.acceptsURL(url)` or vben's
`form.validate()` inside `FormApi.validate()`, linked to the method
making the call, which then looked recursive in callers and impact.
The single-candidate branch now gives no edge when that method is the
caller itself and the receiver is another object (not this, self, Self,
super or cls), as the word-overlap branch already did.
On top of the PR: Go is left out. A Go method's receiver and struct
fields are typed before this point, so a Go call that still reaches the
guess is on a local the resolver could not type, mostly a range variable
over the type's own children, which is real recursion: cobra's
`sub.checkCommandGroups()` over `c.commands`, etcd's `child.Repr(...)`,
prometheus's `subParameter.toLabels()`. Also a TypeScript wrapper test, a
Go range-variable recursion test, and a CHANGELOG bullet.
Validation, main (
|
||
|
|
b6379e6d5e |
fix(scala): an Outer.Inner(...) call reaches the Scala case class or object it builds
Scala qualified names carry no package (Outer::Inner), so a call written Outer.Inner(args), a case-class or companion apply, found no type by its written name. It fell through to the name strategies, and under a return type annotation (def f(): WebSocket = WebSocket.Accepted(flow)) the local-declaration reader took WebSocket for the receiver's type and handed over an unrelated Java constructor, Other::Accepted::Accepted. Before the receiver-word strategy, a Scala call whose member is capitalized now resolves to the Scala type Outer::Inner when that is unambiguous: every match in one file (a class and its companion), or one in the call's own file; a class or trait wins over a same-named object. It is recorded as instantiates for a class, as a call for an object. The typed-receiver path no longer hands a Scala call another language's constructor. Real Java static methods are still reached from Scala. Validation: - resolution.test.ts: 5 cases, 4 red on main; the fifth (a Scala call to a real Java static) passes on both and guards the Java side. - A/B against main |
||
|
|
81559dcc37 |
fix(resolution): a call reaches the caller's own copy of a duplicated module
A module kept in several copies, like two Android build flavors that each
hold one package, gives every name in it one candidate per copy.
Directory proximity is capped at six shared folders, so in a deep tree
the copies scored the same and the first indexed won: a call in one
flavor could land on the other flavor's copy. And since the name is not
in doubt among copies, only which copy, scoring the right pick by
proximity (0.7, or 0.4 in a shallow tree) understated it.
findBestMatch now breaks a tie among copies of one symbol (same
qualified name, kind and file name) toward the copy whose file shares
the longest directory prefix with the reference, when that copy is
uniquely closest. matchByExactName scores that pick 0.8: above a
proximity guess, below a unique name or the file's own definition (0.9,
which keeps precedence).
On top of the PR: the copies are judged among the candidates
findBestMatch weighed, so a namesake in another language no longer
blocks the 0.8; a match of the caller itself keeps the proximity score,
as it does for a same-file match; and Kotlin and Java flavor tests, since
the PR's Python fixture calls a function it never imports.
Validation, main (
|
||
|
|
4c572e07b6 |
fix(play): a Class.method reference reaches a member of the class it names
The Play resolver claims every Class.method reference in a Play project, in
routes files and in ordinary Java and Scala code alike, and took the first
method of that name anywhere in the class's file. In a file holding several
classes (Java nested classes, a Scala class with its companion and
siblings) that was usually another class's method: MediaType.parse(...)
went to Cookie.parse, a route controllers.Application.index to a nested
Helper.index, and Scala RequestAuthority.parse(...) to AuthorityPort.parse.
A method now counts only when its qualified name is the named type's own,
cls::method. A Scala object is an owner too, so a companion's own members
are found through it, and for a call in code so are a Java interface's and
enum's static methods. For a call written in code, the types of the
caller's language are tried first: Play declares RequestAuthority both as a
Scala case class and as a Java record, and each language's callers mean
their own; the other language's types still follow, since Play's Java API
calls its Scala classes. Among same-named types the caller's own file comes
first (Scala names carry no package), an overloaded member gives the
overload the call's arguments fit, and a Java file's import of the name
from outside the project leaves the call to the generic resolver. A route
keeps the plain order and takes a class or object. An inherited member is
left to the generic resolver, which finds it through declared supertypes.
Validation:
- frameworks-integration.test.ts: the PR's case (nested Http.Cookie /
MediaType, Scala sibling objects, a route beside a nested Helper.index),
red on main; ours, Scala and Java callers of a RequestAuthority declared
in both languages (red on main and with the PR's own resolver) and an
inherited route handler; and a guard for overloads, same-named objects
in two packages and a Java interface's static method.
- A/B against main
|
||
|
|
cde4cd74eb |
fix(resolution): a lone same-file definition scores as a strong match
matchByExactName scored a winner among several same-named candidates by directory proximity alone: 0.7 when the files share two folders, else 0.4. findBestMatch already ranks the definition in the reference's own file first, so the target was right, but a shallow file's own function read as an uncertain guess (below 0.6), and because the resolver keeps the most confident strategy, the same-file definition lost to a framework's naming guess in another file (ASP.NET's PascalCase and Service rules at 0.7 and 0.85). isDecisiveSameFileMatch now scores it 0.9, as a lone candidate, when it is the only candidate in the file, is not the caller itself, and, as a member of a type, module or namespace, belongs to one the caller is in. The PR checked that last rule for methods only. We extended it to every member kind whose owner the file declares, other than a function, so a sibling enum's entry or a sibling class's field is not raised. Validation, main vs this commit, 21 repos in 11 languages (flask, click, fastapi, gin, cobra, etcd, ripgrep, jq, Slim, serilog, eShopOnWeb, nowinandroid, okhttp, http4s, playframework, spring-modulith, express, zod, bulletproof-react, excalidraw, vben): - no edge added or removed; - 9 targets moved, all in eShopOnWeb and all right: C# properties and fields named after their type that the code reads (Color Color), which ASP.NET's naming rule had linked to the class in another project; - 6,927 edges keep their target and gain confidence, 535 of them from 0.4; in 874 a framework rule's 0.7-0.85 now loses to the same target by exact name; 30 end in another file, Go type positions the type gate moves to the package's own type; - as written, the PR also raised 77 edges onto another type's member to 0.9, about 48 of them wrong (okhttp's InterceptorOverridesTest enum entries, playframework's sibling providers' `get`); they keep main's confidence. Index CPU, median of interleaved runs: okhttp 32.3 vs 32.4 s, etcd 33.4 vs 33.1 s. Tests: 3 of 7 fail on main; the PR as written fails the sibling-member case. js-class-expressions pinned the old 0.4 for `this.run()` reaching the class's own `run` in a shallow file; it now expects 0.9. Original PR #1938 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3a93f57ae1 |
fix(java): a record is indexed as a class, with its components' fields and accessors
Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.
The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.
An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.
Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.
Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
main; the sealed-permits and name-only-guess cases are each red without
their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
sweeps at full parity with deferrals unchanged (jextract 218/218,
spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main
|
||
|
|
d02a88d052 |
fix(java,csharp): a call on a new object reaches the class the file imports, not a namesake (#2864)
PR #2854 sends Java's and C#'s no-argument new Foo().bar() calls, which
the chain conformance pass retries, to the constructed-receiver resolver.
That links them (mybatis-3 +265, Newtonsoft.Json, serilog and jellyfin
+36, all right), but the hint for which Foo it passed was a file path,
which the resolver reads for these languages as a dotted package name,
and C#'s using names namespaces, not classes. So of two classes named
Foo the first indexed one won, whatever the file imported: import b.Foo;
new Foo().bar() linked a.Foo::bar at 0.9, and so did a call the language
itself cannot resolve, after import a.*; import b.*;.
When several project classes share the name, Java and C# now pick as
the language does: Java the written package (new a.Foo()), the file's
single-type import, else the caller's own package; C# the one type the
file's namespaces and usings can see. Only that class's own method links,
otherwise nothing. A name only one class has resolves as before,
inherited methods included.
Validation: the new test fails 4 of 4 on main
|
||
|
|
90c5dc6fc9 |
fix(extraction): a Go method carries the exportedness of its name
Every Go method was indexed as unexported, whatever the case of its name, in both engines: extractMethod never set isExported, and the kernel's go.rs mirrored the omission. Plain functions carried the flag, so dead-code results listed exported methods like Close as dead. A method that is a top-level declaration (methodsAreTopLevel, only Go) now gets the extractor's isExported, as a function does, and go.rs sets is_exported from the same name-case rule in extract_method. Added on top of the contributor's change: findGoPackageMember, which resolves pkg.Name through a project import, matched any exported node of that name in the package's directory, and methods stayed out only because none was exported. With the flag set, a package-level call like harbor's log.Errorf(...) went to the method Logger::Errorf declared beside the function, and prometheus's embedded storage.Appender resolved to a method named Appender, which dropped its implements edges. A method is no member of its package, so it is now skipped there. Two comments in name-matcher.ts that said the flag is unset on every Go method now say why the name's case is still read. Validation: the contributor's test fails on main and passes here, and the new package-call test fails with the PR as first written and passes here and on main, in both engines. A/B against main on harbor 296f304, prometheus a2fda0e, gin 0f09c3a and cobra adbc881: nodes differ only in is_exported 0 -> 1 on uppercase Go methods (5,704, 3,880, 391 and 141). Edges are identical on prometheus and cobra; 22 retargets remain, all name-only guesses between an interface's method declaration and the struct method implementing it, which now win: gin's engine().GET and its siblings reach RouterGroup's methods, and harbor's jobservice routes reach DefaultHandler's. The PR as first written had retargeted 1,384 harbor calls and removed 60 prometheus implements and dispatch sites. The dead-code report lists 3 Go symbols on harbor instead of 167, and 0 on prometheus instead of 11. Kernel parity: 2,493 of 2,493 Go files byte-identical. The controls express and flask are byte-identical. Index CPU on harbor, median of 5 interleaved: 40.8 s on main, 39.8 s here. EXTRACTION_VERSION stays: main already moved it to 28 after 1.6.2, so 1.6.2 indexes get the re-index hint anyway. Original PR #1734 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
428b3c487d |
fix(js,ts): a nested function takes calls to a name the file imports from a package
When a JavaScript or TypeScript file imports a name from outside the
repository, like import { resolve } from 'node:path', matchByExactName
returns no edge for a bare call of that name (the early return main added
for issue 2101). A call to a function declared inside the calling function
under that name, which shadows the import, lost its edge with it.
The candidates for such a call are now narrowed to the calling file's
declarations nested in a function or method (innerScopeShadows), instead
of giving up; a top-level one is the import's own binding. Lexical
reachability, applied later, keeps only the nested declarations visible
where the call is.
Our changes: tests for a sibling function's nested declaration and one
nested a level below the call, which neither may take the call (both hold
through the reachability rule), and for a call from a function nested in
the declaring one; the CHANGELOG bullet moved next to the JS/TS import
bullets, with credit.
Validation: the tests fail 2 of 6 on main and pass here. A/B against main
on 8 repos, kernel per arm: flask, gin, excalidraw, codegraph, pixijs,
pdf.js and three.js byte-identical; vite gains 1 right link, scanImports'
call to its nested scan() beside import { scan } from
'rolldown/experimental'. Index CPU
time on codegraph, test files full of vitest imports, within noise:
81.7 s to 81.9 s (median of 3, interleaved).
Original PR #1715 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
0d46dba08f |
fix(js,ts): a call on a new C() receiver resolves through C
A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.
Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.
Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR #2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.
Validation: the new tests fail 4 of 6 on main
|
||
|
|
16f2d357bc |
fix(go): a project package named like a standard-library package is not the standard library
A Go reference written through a package qualifier was dropped as a standard-library reference whenever the qualifier matched a name in the built-in list (errors, log, types, parser, encoding, template, ...). The check looked at the name alone, never at what the file imports under that name, so a project with its own package of such a name lost nearly every call into it: harbor's errors.New(...) through its lib/errors, prometheus's parser.Inspect(...) through promql/parser. What the file imports now decides before the name does. A qualifier the file imports, by its last path segment, the name goimports assumes for an unaliased import, or an alias, from a package of one of the project's own modules is a project package, and the reference goes on to normal resolution. A file that imports the standard library's package of that name, and a qualifier the file doesn't import at all, are skipped as before. Validation: the new test file has 7 cases, 4 of which fail on main; all pass here, with the native kernel and with CODEGRAPH_KERNEL=0. A/B against main: harbor 296f304 (its Go tree, 1,611 Go files) +2,889 edges, prometheus a2fda0e +454, 0 removed and 0 retargeted on both, nodes unchanged. Every added edge was checked against its source: the line spells qualifier.Name and the file imports the target's package directory under that qualifier from inside the module (3,343 of 3,343), and a sample of 10 was read by eye. gin and cobra, and the controls express and flask, are byte-identical. Index CPU on harbor, median of 3 interleaved: 40.0 s on main, 39.9 s here. Original PR #2368 by @danusha2345. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
449eefa7da |
fix(js,ts): a call on an element of a class field reaches the element's type
A TypeScript or JavaScript method called on an element of a class field, like this.handlers[type].handle() or this.items[0].save(), kept the bare method name: a member chain rooted at this collapses to it so the resolver can read the owner off the enclosing class. A subscript in the chain leaves the resolver nothing to read, so the bare name exact-matched any project method of that name, often another class's (this.listeners[type].push(h) reached an unrelated UndoStack.push). Both engines now emit this.<field>[k].<m>() as this.<field>[].<m>, and this['svc'].m() as this.svc.m; any other subscript in a this-rooted chain, or under window, emits nothing, as arr[0].run() already did. The resolver reads the element type the field's declaration writes (X[], Array<X>, ReadonlyArray<X>, Record<K, X>), on the calling class or a base, and resolves the method on it or leaves the call unresolved: a type parameter of the declaring class, a union with another type, an array of arrays or a non-plain element hold no one type. The class type-parameter reader now reads from the keyword to the list's own >, over several lines and through braces in constraints. Our changes: kept escapeRegExp from PR #2769 in the conflict; the type-parameter reader also accepts the type keyword, since the T['p'] path passes type-alias owners and the PR as written read State<Item>['current'] as the project class Item (new case in ts-array-annotated-receivers); CHANGELOG covers JavaScript and asks for a re-index. Validation: the element tests fail 10 of 11 on main with both engines; the type-alias case fails on the PR as written and its brace half on main. A/B against main, kernel built per arm: flask and gin byte-identical. TS: pixijs 7 links added, 5 retargeted and 5 typed, all right, 15 removed (9 wrong, 6 right only by name: a cast, a mixin's this, a T[] parameter, a merged mixin member, an index-signature type, an any field); typeorm 3 wrong removed, 1 right lost, 2 retargeted to the declared interface; excalidraw 1 wrong removed; vscode's base and editor 19 added, 24 retargeted and 35 typed, all right, 45 removed (37 wrong, 8 right only by name: a getter, an index-signature field, deep chains, a T[] parameter, a union alias). JS: three.js, playcanvas and pdf.js lose 288 bare-name guesses, about 230 of them wrong (array, Map and DOM calls on project classes). Kernel parity holds on every repo with deferrals unchanged. Index CPU time within noise (pixijs 32.4 s to 33.3 s, three.js 64.7 s to 67.9 s, medians of 3 interleaved). Original PR #2766 by @inth3shadows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5c5bfd4051 |
fix(php): a namespace alias matches whatever case it is written in
PHP reads namespace names without regard to case, so after
use App\Fields as Field; the name field\FirstName is the same class as
Field\FirstName. The qualified-name resolver compared the alias as
written, found no import, and read the name relative to the file's own
namespace instead, so new field\FirstName() stayed unlinked. The alias
is now matched without regard to case.
This lands the first half of the contributor's PR. Its second half
refused an alias that two namespace { } blocks of one file bind to
different namespaces. That also dropped the first block's correct edge,
because the use map is file-wide, and the case doesn't occur in real
code: across 8 PHP repos (33,820 files), 185 files declare more than
one namespace, and none binds one alias twice outside strings. The fix
for those files is to scope each use statement to its namespace block,
which needs block positions the index doesn't keep yet: a braced
namespace gets no namespace node, and neither does a second unbraced
one, so it is left for its own change.
Validation: the new test fails on main and passes here, with the
native kernel and with CODEGRAPH_KERNEL=0. Graphs of php-parser,
composer, psalm, monica, laravel and phpunit are byte-identical to
main, as are the controls express and flask. Index CPU on laravel,
median of 3 interleaved: 53.3 s on main, 52.9 s here. The PHP and
Laravel suites pass in both engines (10 files, 67 tests).
Original PR #2369 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
9c4794d694 |
fix(rust): a call keeps its whole receiver chain, and links through method calls in it (#2846)
A Rust method call made through more than one field, or on what another
method returns, was recorded by its bare method name in both extractors:
self.services.store.put("a") became `put`, as did self.store().put("m").
Only a single field kept its receiver (self.store.put). The edge's
refName then said nothing of which value the call goes through, and a
call on a method call's value was never typed, so self.store().put("m")
stayed unresolved. PR #2831 had already made the field-chain calls link
by reading the receiver from the source text, but their names stayed
bare.
Both extractors now keep a receiver that is a chain of fields and method
calls on `self` or on a name, a call written `name()` with its arguments
and turbofish dropped, up to 16 links: self.services.store.put,
self.store().put, vis.map.get, self.0.len, x.get().put. The shapes that
already kept a receiver (x.m, self.m, self.<field>.m, Foo::new().m) are
unchanged; a parenthesized receiver, `?`, `.await`, an index and a longer
chain rooted at a path keep the bare name. The kernel's receiver_chain
mirrors the wasm extractor's rustReceiverChain byte for byte.
Resolution reads such a call as the bare-named call was read before
(rustChainedCallByName, at the top of resolveOneIn), so the frameworks,
the strategies and the scope gates see what they saw, and the edge keeps
the call as written. The chain goes along: the written-receiver hook
reads the receiver the chain names where the call is written
(rustChainedReceiver), so a method call's value is typed from that
method's signature, and a later link that calls the same name again is
told apart instead of being left to a guess by the name.
Validation:
- New __tests__/rust-receiver-chains.test.ts (10 tests): the issue's
workspace verbatim plus self.store().put("m"), chains whose links call
the same name through self, a tuple field and a parameter, a call on a
method's std return value, the recorded shapes and the 16-link cap,
and a sync that adds the missing method. Red on main's sources, 7 of
10 failing on both the wasm and the kernel path; green with the fix.
The Rust kernel parity suite gains a chain case. The extraction test
that pinned the old collapse now pins the chains.
- A/B against main
|
||
|
|
d833827503 |
perf(resolution): skip receiver typing for JS/TS calls of a method no node is named (#2840)
A JS/TS call of a method that no node in the index is named, like
`list.push()`, `fs.writeFileSync()` or a test library's matcher, still
went through matchMethodCall's receiver-type inference, the TS binding
and import readers, and the class strategies. Every one of them ends in a
lookup by the method's name, so for such a name they can only answer
null. The one exception is the object-literal reader, which follows a
member to the binding the literal gives it: `const api = { alias:
renamed }` makes `api.alias()` a call of `renamed` (Issue #1932).
Root cause: matchMethodCall reads the calling file for the receiver's type
before anything tests the method's name.
Fix: the resolution context answers isAbsentName(name) from the set of
node names warmCaches already loads for the pre-filter. The answer is
exact, like getNodesByName, and false whenever the set isn't loaded. For a
JS-family call whose member is absent, matchMethodCall asks only the
object-literal reader, exactly as the full path would. When the reader
answers, the full path runs unchanged; otherwise the call returns null at
once. CodeGraph.indexFiles now drops the resolver's caches after it stores,
as indexAll and sync already do before they resolve, so a later resolution
never reads the name set from before the new files.
Validation:
- New __tests__/absent-member-calls.test.ts (6 cases). The skip reads
nothing of the calling file (red on main). The name set answers only
once loaded. Every path that links a member to a symbol of another name
is pinned with the member asserted absent: a literal alias, a shorthand
member bound to a renamed import, an `as const` literal, a holder in the
same file and one imported, and a renamed re-export read through a
namespace import. A method added through indexFiles after indexAll still
links (main links it; the skip without the cache drop did not). A
fixture indexes identically with and without the skip.
- Graphs byte-identical to main (scripts/dump-graph.mjs: nodes, edges with
metadata, refs, files), kernel built per arm, on express,
vue-element-admin, excalidraw and this repository (JS, TS, Vue, Svelte,
Astro) and on the controls flask, gson, gin, ripgrep and slim (Python,
Java, Go, Rust, PHP). Default mode (pooled on the two largest) for all
nine, single-threaded for the JS four, and again after rebasing onto
the current main. Main run twice is identical too. An incremental sync
adding a file that declares formerly absent names (1,088 failed refs
retried) is identical between the arms, with and without git, and
matches a fresh index.
- Survey of main (instrumented build, nine repos, every language): no call
whose member no node is named was resolved by matchMethodCall. The skip
fires on exactly the calls the survey found (12,354 here).
- Profile counters, single-threaded: receiver inference runs 38,626 times
instead of 50,980 on this repository, 6,460 instead of 9,306 on
excalidraw, 2,488 instead of 3,586 on express. Outcome counts are equal.
- Cost, medians of 5 interleaved runs: the resolve loop on this repository
4.54 to 4.41 s pooled and 16.85 to 16.29 s single-threaded, faster in 5
of 5 paired rounds each; resolution phase -2.1% and -2.3%; whole index
-1.3% and -2.2%. excalidraw's loop -1.6%, express's -5.0%; the five
controls within noise (3 runs each).
Fixes #2829
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f406fe0064 |
fix(cfml): createObject("component", path) links its component, and .init() on it (#2841)
`new a.b.Batch()` made an instantiates edge, but
`createObject("component", "a.b.Batch")`, the form older CFML uses most,
was extracted as a call to the engine function createObject, which never
resolves. A call chained onto either construction, `createObject(...).init()`
or `new a.b.Batch().init()`, was extracted as a bare `init`, which the
name matcher bound to whichever component's init came first (0.4), or to
none when there are many.
Fix:
- cfscript extraction (tag expressions included): a component
createObject, with two arguments, one, or named ones, makes an
instantiates reference under the dotted path as written, not a call.
Java and other object types, and names built at runtime, are unchanged.
- A call on a construction is extracted as `<component>().<method>`.
- The resolver reads a dotted instantiates path with
resolveCfmlComponentPath, the rules extends uses, and resolves a chained
call to the method of that component or one it inherits (matched
ignoring case; inheritance is wired before calls). A bare component name
is the one in the calling file's folder, else the only one so named.
Without both, the call gets no edge rather than a guess by name.
- resolveCfmlComponentPath also accepts a component at the project root
for a path with one folder segment, its file named like it:
`wheels.JobWorker` in Wheels' own source, where `wheels` maps onto the
root. Without it Wheels' core lost calls like
`new wheels.JobWorker().processNext()` that name matching had found. It
ranks below any folder match; extends gains the same.
Validation:
- New cfml-create-object.test.ts (5 cases) and a root-component case in
cfml-inheritance-resolution.test.ts: 5 of the 6 fail on main (the sixth
pins the variable typing that already worked), all pass.
- A/B vs main
|
||
|
|
d90f6abec7 |
fix(cfml): <cfinclude template> and cfinclude() link the code to the file they include (#2838)
Only cfscript's `include "x.cfm";`, written outside any function, linked
the including code to the template. CfmlExtractor never read
`<cfinclude template="...">`, which the grammar parses as a
cf_selfclose_tag whose name is only in the text after `<cf`, so the tag
form made nothing anywhere. cfscript's include hook read only the string
form, `cfinclude(template="x.cfm")` went through the call extractor as a
call to `cfinclude`, and the function-body walker never extracts import
statements, so an `include` in a function was dropped. Resolution went
through the generic pipeline: the file-name matcher links a relative path
like `../shared/helpers.cfm` only when no other file shares its name, and
past it the symbol strategies could take another template's import node
of the same path, or the including file itself.
Fix:
- `<cfinclude template="...">` is extracted like the script form: an
import node in the scope it sits in (function, component, or the
template's file) and an imports reference from that scope.
`cfinclude(...)` and `include template="...";` make the same import and
no call, and an `include` in a function body counts, from the function.
- A path built at runtime (`#dir#/x.cfm`) is no import, in every form.
- A CFML include of a template path resolves as CFML does: relative to the
including file's directory, or from the project root for a leading `/`
(less sure, 0.85), to the file at that path, ignoring case when one file
matches. A path that names no file there goes to the file-name matcher,
as before, but only ever to a file, and never to the including file.
Validation:
- New cfml-include.test.ts: the tag form at component level (same folder,
../, upper case, unquoted), in a function body inside <cfif>, in a
template and from the web root; the script forms; runtime-built paths,
cfinclude(template=variables.x) and <cfmodule> (no import); a relative
path with a namesake elsewhere; an unmatched path that another template
also includes, and a path whose only namesake is the includer. 5 of 7
fail on main, all 7 pass.
- A/B vs main
|
||
|
|
51c4124dad |
fix(cfml): a dotted extends finds its component when a test suite mirrors its path (#2835)
A CFML component that extends another by its dotted path, like
extends="contenscms.core.basecore", got no extends edge when a test
suite held a file of the same name in a folder of the same name, such as
a TestBox spec at tests/testbox/tests/specs/unit/core/basecore.cfc.
resolveCfmlComponentPath counts the path's folders that match a
candidate's, from the right. contenscms is a server mapping onto
contens/, so only core matched, for the component and the spec alike,
and a tie gives no edge. The rule the resolver applies elsewhere since
PR #2209, that production code never names a symbol in a test suite,
never ran on this path: it returns before the name matcher's visibility
gate.
Fix:
- A component in a test suite is no candidate for a reference from
production code (isTestSuitePath, now exported from the name matcher).
This also keeps application code from extending a test file that
happens to match more folders.
- A test's tie between a component and a suite file that mirrors its
path goes to the component outside the test suites. Two production
components still tie.
Validation:
- cfml-inheritance-resolution.test.ts: 2 new cases (the issue's tree in
script and tag form, a test extending the component, a mirror that
matches more folders, a path only a test suite holds, a test naming its
own suite) fail on main's resolver; all 12 pass with the fix.
- The issue's repro: page and tagpage now extend contens/core/basecore.cfc
(qualified-name 0.9); on main both stayed unresolved.
- A/B vs main
|