11 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 2344ce0722 test: stop test runs leaving temp dirs behind (#2622)
Every full test run left about 40 directories in the temp dir, even when it
passed. On the Windows dev machine about 16,000 had piled up by mid-October.
A full run with TEMP pointed at an empty folder showed where each one came
from:

- function-ref (20) and ts-field-classification (3): each case's `finally`
  cleared `tmpDir` right after `cg.destroy()`, so the `afterEach` that removes
  the dir found nothing to remove.
- npm-sdk (3) and liveness-watchdog (3) never removed the dirs they made.
- extraction's #1502 suite (3) made a dir in `beforeEach` and had no
  `afterEach`; pr19-improvements' schema-version case (1) made one inline.
- setup-home-sandbox (6 on Windows): Vitest runs no hooks for a file whose
  every test is skipped, so each platform-gated file left its home sandbox.
  Each sandbox now records itself in a per-run registry, and a global
  teardown removes whatever is still listed and names any dir still in use.

With the fix, the same full run leaves nothing behind but Node's own reused
compile cache. test-home-sandbox gains two tests: a file's home is listed in
the run's registry, and the teardown removes every listed home, then the
registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:00:12 +00:00
f838c5a02a fix(resolution): resolve method values on untyped Python module globals (#2074) (#2291)
* fix(resolution): resolve method values on untyped Python module globals (#1820)

A method value whose receiver is a module global without a static type
(`thread_pool_exec(settings.conn.fetch, ...)`, where `conn = None` is
rebound by `global conn; conn = Backend()`) resolved to a `variable` node
and produced no edge. The global's type is now the set of classes its
module assigns to it, at module scope or in a function that declares it
`global`. One class resolves to its own method; several bind to the
nearest declaration every candidate inherits, as a base-typed receiver
does.

No edge (a wrong callback edge is worse than none) when:
- any binding of the global in its module is not a whole constructor
  call: a factory, a conditional, a tuple target, `for`/`with`/import,
  a star import, or an annotation the constructor contradicts;
- the caller or an enclosing function binds the name itself: parameter,
  assignment (any target position), loop, comprehension, `as`, `case`
  pattern, lambda parameter;
- the receiver is a deeper chain (`settings.conn.pool.fetch`);
- the importing file rebinds the name, or imports it from two sources.
Statements are read with their continuation lines, and only statement
lines (bracket depth 0) can assign. Not seen: rebinding the global from
another module (`settings.conn = X`) or through `globals()`.

`pkg.mod.Cls` now resolves through `import pkg.mod`, unless two namespace
imports share the last segment.

Python import mappings also read parenthesized from-imports
(`from x import (\n    A,\n    B,\n)`), which previously yielded the
single name `(` and hid every class imported that way from receiver and
inheritance resolution. Comments and docstrings are stripped first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0aab32a18b)

* fix(resolution): account for writes to a Python module global from other files (#2074)

The module-global receiver type was read from the global's own module only,
so `settings.conn = Decoy()` in another module, or `globals()["conn"] = X`,
left a stale type and a wrong edge.

- A production write `<module>.<name> = Cls(...)` from another file joins
  the type set (resolved in the writing file); any other production write
  (another value, a tuple target, `setattr`/`patch.object`) makes the type
  unknown. The module is matched through each file's imports, including
  aliases and relative imports.
- Test files install doubles (`settings.conn = MagicMock()`,
  `monkeypatch.setattr(settings, "conn", ...)`): they do not change the
  production type, but a ref inside such a test resolves nothing.
- `globals()[...] = ...` with the global's name or a computed key, and
  `globals().update(...)`, in the global's module make the type unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 2055711226)

* fix(resolution): match a module global's writers by file, not dotted suffix

Review follow-up to the cross-module write scan.

- A write now lands on the file its import names: relative imports resolve
  exactly, absolute ones by path suffix; when a spelling could name another
  file sharing the tail (`x/settings.py` and `y/settings.py` for
  `import settings`), a write through it makes the type unknown instead of
  feeding or clearing the wrong module.
- `import a.b` binds `a`: the module is spelled `a.b`; the mapping's
  last-segment name is not treated as an alias of it.
- Test doubles are recognised by the narrow test-suite set plus any
  `conftest.py`; examples, fixtures and benchmarks are production writers.
- Writes inside `if __name__ == "__main__":` are script code, not module
  state, in the global's module and elsewhere.
- Namespace-dict writes: `globals()`, `vars()` and `sys.modules[__name__]`
  are read per statement (continuations joined, strings ignored); any use
  other than a literal-key read or `.get`, or a literal-key write of the
  name, makes the type unknown. `<module>.__dict__[...]` / `.update(` from
  another file does too.
- `settings.conn: Store = Store()` and `settings.conn = (Store())` are
  plain constructor writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 7aab29269a)

* fix(resolution): explicit dotted aliases, one-line __main__ guards, scoped vars()

Review follow-up.
- `import pkg.settings as settings` binds `settings` even though it equals
  the last segment; the import mapping cannot tell it from a plain
  `import pkg.settings`, so the source line decides.
- `if __name__ == "__main__": stmt` on one line is script code, and a
  `globals()` write inside a `__main__` block no longer counts.
- `vars()` is the module dict only at module scope; inside a function it
  is the locals, so `return vars()` there no longer makes the type unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0a5fb69797)

* fix(resolution): explicit-alias check matches the exact module, outside strings

Review follow-up: `import other.pkg.settings as settings` or a string
`"import pkg.settings as settings"` no longer marks a plain
`import pkg.settings` as explicitly aliased, which attached a write to the
wrong module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit a5569a07ea)

* test(resolution): give the Python module-global adversarial table its own timeout

It indexes one project per case, which takes about nine seconds on macOS,
past vitest's five-second default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): Python module globals and multi-line imports (#2074)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:47:36 +00:00
Colby Mchenry 9be3f1a8a2 fix(resolution): keep unknown Python import members unresolved (#2040)
Bisect identifies 5eaa6fed (#2034, #1820) as the first bad commit: its Python named-import fallback resolves task.delay() to the task function itself, hiding the queue effect in API steps. Require member resolution before returning a named Python import's target for attribute access.

Keep direct calls, callbacks, constructors, and known members working. Add absolute/relative aliased-import regression coverage without changing the existing steps assertions.

Validation: git bisect with preserved steps test, tsc and copy-assets at every step, CODEGRAPH_KERNEL=0 throughout bisect. 338 tests pass across ui-steps-api-servers, function-ref, rebuilt kernel-tsjs-parity, resolution, and ui-effects. Issue #1820 repro and tsc --noEmit pass.
2026-09-27 14:02:38 +00:00
5eaa6fedd5 fix(extraction): preserve receivers for method values (#1820) (#2034)
* fix(extraction): record callers for methods passed as values (#1820)

pool.submit(obj.method), thread_pool_exec(self.store.fetch), and Go
Submit(c.store.Fetch) produced no callers/impact edge because only
obj.method(...) call expressions were captured. Member values are now
emitted as *.method function-refs and resolve unique-or-drop
(same-file first). Two methods of the same name still produce no edge.

Go go c.store.Fetch(ids) remains a calls edge (it is a call expression).

EXTRACTION_VERSION 27: MCP daemon catch-up rebuilds a stale index on
start so the watcher does not keep serving the hole after upgrade.

* fix(resolution): do not let test mocks veto #1820 method-value callers

A unique production method plus a unit-test double of the same name
was treated as ambiguous, so retrieve_vision_vectors still looked
callback-invisible on a real repo. Prefer the non-test file when
exactly one production target remains.

* fix(resolution): prefer a real override over a NotImplementedError base

#1820 unique-or-drop treated DocStoreConnection.delete_payload_fields
(the ABC stub) and QdrantConnection.delete_payload_fields as ambiguous,
so gc_stale_member_stamps stayed invisible. Skip short not-implemented
stubs when exactly one real implementation remains.

* fix(extraction): preserve receivers for method values (#1820)

Python attributes and Go selectors used as callback values were dropped or reduced to bare method names.
Preserve receiver paths in native and WASM extraction, resolve through type/import scope, and reject ambiguous or noncallable targets.
Cover callback, assignment, collection, and query behavior, with extraction version 27 signaling the required reindex.

Co-authored-by: Josh66 <info@aska-systems.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:42:27 +00:00
9181dd1ef3 fix(extraction): land upstream declaration initializer walks (#1511) (#1802)
Squash danusha2345's PR #1511 at d282f9e8 onto main 8c9c4761,
preserving its nine non-merge commits and main's existing Unreleased notes.
Calls in Kotlin, Java, TS/JS, Scala, Rust and Python declaration initializers
now retain the owner established by the upstream regression expectations.
Include the upstream CFML, dynamic-dispatch summary and viewer follow-ups.

Linux fail-to-pass validation (Node 22.19.0, rebuilt dist and native kernel):
- Before: TS load belonged to file:app.ts; Python/Kotlin/Scala/Rust calls
  vanished; Java lost the field-lambda, anonymous override and eager calls.
- After: all six languages PASS; 12 native/WASM LF/CRLF parity checks PASS.
- Focused initializer regressions: 10 passed with CODEGRAPH_KERNEL=0 and
  10 passed with the kernel enabled; Kotlin's grammar fallback is recorded.
- Related regression suites: 879 passed, 1 skipped across 15 test files.
- Evidence: /workspace/cg-1510-repro/before and /workspace/cg-1510-repro/after
  (combined test output: after/vitest.log).

Fixes #1510
Supersedes #1511

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
2026-09-08 17:33:45 -05:00
Colby MchenryandClaude Fable 5 38580e0b04 fix(python): bare class references produce references edges to classes (#1478) (#1493)
Python's class-as-value idioms (return SomeClass, x = SomeClass, registry
dicts, classes passed as arguments) produced no references edges, so
callers/impact on a Django/DRF serializer missed the views that consume it.
Three gates dropped them:

- return_statement was never dispatched by PYTHON_SPEC (kernel mirrored)
- the extraction gate (definedHere) collected function/method names only
- resolution accepted function/method targets only (matchFunctionRef +
  the function_ref import fast path)

Capture return_statement for Python (single expression; tuple returns not
descended), admit same-file CLASS names to the gate, and accept class
targets for Python bare identifiers — scoped to Python so the TS/JS KIND
FILTER contract is untouched. The docopt false-positive mechanism behind
the function-only rule (lowercase locals vs same-named methods) doesn't
transfer: methods stay excluded for bare ids, and the same-file/import
gate + unique-or-drop rules still apply.

Probed on django-rest-framework (~250 files): 559 new references→class
edges, 10/10 sampled genuine (serializer_class = AuthTokenSerializer, the
ModelSerializer field-mapping registry, aliases, ctor args, isinstance).
EXTRACTION_VERSION 24 → 25.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 01:36:07 -05:00
Colby MchenryandClaude Opus 4.8 dce61a5f4a fix(extraction): qualified Type::member refs skip the name gate — no-import references resolve (#812)
`KtHandlers::handle` registered from another file produced no edge: the
extraction gate required the scope to be a same-file type or an IMPORTED
name, but Java/Kotlin same-package references and Kotlin companion members
need no import at all, so the gate could never see them. (The "companion
members extract unqualified" limit recorded during Arc A was a probe
artifact: a SINGLE-LINE `class X { companion object { … } }` is an
upstream tree-sitter-kotlin misparse (ERROR node); real multi-line
companions extract transparently as qualified methods of the class.)

Qualified `Type::member` candidates now skip the name gate the same way
`this.<member>` ones do: the explicit-ref syntax is self-selecting, and
resolution stays scope-suffix-anchored + unique-or-drop, so a
`Decoy::handle` can never match a `KtHandlers::handle` ref (tested).

A/B vs main: rxjava +4 (same-package `Maybe::just` / `Single::just`
method refs), fmt +3 (gtest `&Test::DeleteSelf_` /
`&TestSuite::RunSetUpTestSuite` cross-file member pointers), okio 0-delta,
redis byte-identical — every new edge verified genuine, zero calls edges
touched, node counts identical.

Full suite 1392 passed. EXTRACTION_VERSION 22 → 23 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:44:14 -05:00
Colby MchenryandClaude Opus 4.8 1f15f93feb feat(extraction): PHP string/array callables + Ruby lifecycle-hook symbols (#811)
The last two deferred callback-registration shapes from #756, each scoped
to positions where the reference is trustworthy:

PHP — a string is a callable ONLY in a known callable position:
  - string args of core HOFs (usort, array_map, array_filter,
    call_user_func*, preg_replace_callback, spl_autoload_register,
    set_error_handler, … — PHP_CALLABLE_HOFS): ungated (PHP globals are
    referenced cross-file without imports) + resolution unique-or-drop,
    function-kind only ('Cls::m' strings resolve qualified)
  - array callables anywhere in call args: [$this, 'method'] routes through
    the class-scoped this. resolver (parents included); [Foo::class,
    'method'] resolves qualified
  - strings to arbitrary functions: deliberately nothing

Ruby — hook-DSL symbols name a method of the enclosing class:
  (skip_)?(before|after|around)_* / validate / set_callback /
  helper_method / rescue_from(with:) symbols → class-scoped this.<sym>,
  riding the supertype pass so `before_action :authenticate` in a
  controller resolves to ApplicationController's method. `validates`
  (plural) excluded — its symbols name ATTRIBUTES. Class-body-level hooks
  attribute to the CLASS node (the scoped resolvers now accept class-like
  from-nodes).

Also hardened while validating: the this.X supertype pass is now
NODE-anchored — file-anchored class node → implements/extends edge targets
→ contains-anchored member lookup — replacing the name-keyed
getSupertypes walk, which unioned every same-named class's parents (rails
has a dozen `Engine`s) and produced a cross-class wrong edge.

A/B vs main: WordPress +556 (14/14 sampled genuine — [$this,'m'] wiring,
array_map('absint',…), sodium polyfill call_user_func_array dispatch);
rails/rails +385 after the node-anchored fix (16/16 sampled genuine, incl.
inherited hooks across real extends edges); controls byte-stable
(excalidraw 0-delta, redis identical, typeorm keeps its +4 inherited
getters). The only calls-edge deltas anywhere are pre-existing
minified-bundle resolution jitter (wp-tinymce.js single-letter symbols).

Full suite 1391 passed. EXTRACTION_VERSION 21 → 22 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:30:29 -05:00
Colby MchenryandClaude Opus 4.8 38095aa95b feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
Three callback-registration shapes deferred from #756/#808, one arc:

1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>`
   registration whose member isn't on the enclosing class defers to a second
   pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs,
   runs after implements/extends edges persist, same lifecycle as the #750
   conformance pass) and resolves up the supertype chain, depth-capped BFS,
   validated targets only. `bus.on("submit", this.handleSubmit)` in a
   subclass links to FormBase::handleSubmit; same-named methods on unrelated
   classes never match. this.-prefixed candidates skip the extraction name
   gate (an inherited member can't be in definedHere).

2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` /
   `OtherClass::handle` emit QUALIFIED names resolved by the scoped
   suffix-matcher — cross-file capable, gated on the scope name being a
   same-file type or an imported name (dotted JVM imports now contribute
   their last segment). `this::m` and `super::m` route through the
   class-scoped resolver (super rides the supertype pass). References
   through a VARIABLE (`subscriber::onNext`) deliberately produce nothing —
   receiver type is unknowable; RxJava's baseline bare capture was resolving
   these to same-named same-file methods (a test method "registering" an
   anonymous class's onNext) — the rework drops 18 such wrong edges and
   keeps the 7 genuine Type::method refs RxJava's main tree actually has.

3. SWIFT enclosing-type scoping (implicit self): bare callback names match
   methods only of the from-symbol's own type (extension/nested scopes
   reconciled by suffix), and top-level code never matches methods.
   Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier`
   resolving to same-named methods on unrelated protocols), all verified;
   the same-class param collision (`task`) remains and is documented.

New ResolutionContext.getNodeById lets matchers derive the from-symbol's
class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw
stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges
changed on any of 7 A/B repos; nodes identical everywhere. Kotlin
companion-object members extract unqualified (pre-existing) so
`Type::companionFn` stays silent rather than guessing — documented.

Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:09:01 -05:00
Colby MchenryandClaude Opus 4.8 38eb4e688c fix(extraction): classify TS/JS class fields by value — properties, not methods (#808) (#809)
Every TS `public_field_definition` / JS `field_definition` extracted as a
method-kind node, so a plain field (`public fonts: Fonts;`) was reported
as callable: class shape was misrepresented, kind-based filtering was
defeated, and bare-name call resolution landed on data fields — typeorm's
boolean `ColumnMetadata::isArray` field was soaking up Array.isArray(...)
call edges (685 such wrong edges on typeorm alone).

Classification now follows the VALUE (classifyMethodNode hook, mirroring
resolveBody's callable detection): arrow-function / function-expression
fields and HOF-wrapped ones (`onScroll = throttle(() => {…})`) stay
methods with their bodies walked; everything else becomes a property that
keeps its type-annotation references edge, visibility, static-ness, and
decorators. Field initializers are now walked too (`history =
createHistory()` attributes the call to the property — previously
invisible), and JS class fields — whose name lives in the grammar's
`property` field, so they never extracted a symbol at all — now appear in
the graph (resolveName on the JS extractor).

With fields correctly kinded, `this.X` callback registration is re-enabled
for TS/JS (removed in #807 because field pseudo-methods made it mostly
wrong): `this.<member>` candidates resolve CLASS-SCOPED
(resolveThisMemberFnRef) — the target must be a function/method sharing
the from-symbol's qualified-name class prefix, same file, no fallback —
so `addEventListener("online", this.onOfflineStatusToggle)` and API-object
wiring (`{ mutateElement: this.mutateElement }`) produce registration
edges to the enclosing class's own method, while `this.fonts` (a
property) and inherited/unknown members yield no edge.

A/B (baseline = #807 main): excalidraw / typeorm / express — node counts
identical on all three; kinds shift method→property only (typeorm: exactly
7,406 swapped; excalidraw also corrects 5 anonymous-class mock fields that
were function-kind); every one of the 736 dropped call edges targeted a
node that is now a property (calls into data fields — verified 100%);
gains are retargets to real callables, initializer-call attributions, and
+74/+7 class-scoped this.X registration edges (sampled: addEventListener/
removeEventListener wiring, imperative-API method maps). Full suite green
(1386).

EXTRACTION_VERSION 19 → 20 (re-index to benefit).

Closes #808

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:48:11 -05:00
Colby MchenryandClaude Opus 4.8 8a114ba53c feat(extraction): capture function-as-value — callback registration sites in callers/impact (#756) (#807)
A function name used as a VALUE — passed as an argument
(signal(SIGINT, handler), qsort(..., compare)), assigned to a function
pointer or field (ops->recv_cb = my_cb, OnClick := Handler), or placed in
a struct initializer / handler table ({ .recv_cb = my_cb },
{ "get", getCommand }) — produced no edge in ANY of the 19 tree-sitter
languages, so registered callbacks looked dead and their registration
sites were invisible to callers/impact.

This adds table-driven function-as-value capture across all 19 languages
(plus the wrapper forms: &fn, &Cls::method, Java Class::m, Kotlin ::f,
Swift #selector, ObjC @selector, Ruby method(:sym), Scala eta, Pascal
@Handler), gated at extraction (same-file definitions + imported
bindings; C-family file-scope initializers are constant-expression
contexts and skip the gate, which is how redis-style cross-file command
tables resolve), and resolved by a dedicated strategy: function/method
targets only, same-file first, unique-or-drop cross-file, no fuzzy
fallback ever. Edges persist as kind 'references' with metadata.fnRef,
so getCallers/getImpactRadius surface them with zero graph-layer
changes; MCP callers/callees label them "via callback registration".

Precision rules bought by real-repo false positives (full A/B record in
docs/design/function-ref-capture.md): C++ is &-explicit outside
file-scope tables (fmt's begin/out/size collisions; out-of-line member
defs are function-kind); TS/JS/Python bare ids resolve to functions only
(TS class fields extract as method-kind — pre-existing quirk); Swift
refuses same-file method overload-families; param-forward shapes
(this.x = x, value: value) and destructuring are skipped; minified
bundles (*.min.js) produce no candidates.

Validated on 17 public OSS repos (redis, excalidraw, gin, bytes, okhttp,
okio, Alamofire, flask, sinatra, Newtonsoft.Json, scopt, provider,
busted, Fusion, AFNetworking, PascalCoin, fmt): node counts identical,
zero calls edges lost or gained, references strictly additive
(+3,200 registration edges total), precision spot-checked by reading
sampled source lines (redis 30/30, flask 8/8). Deliberately NOT covered:
indirect-dispatch resolution (o->cb(x) → impl) — that needs data-flow
through struct fields, and a wrong edge is worse than none.

EXTRACTION_VERSION 18 → 19 (re-index to benefit).

Closes #756

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:20:27 -05:00