Commit Graph
65 Commits
Author SHA1 Message Date
Colby MchenryandClaude Opus 5.5 4f3eaf33ae fix(resolution): component and scoped-alias imports link the module file (#2528)
An import statement's own module reference linked nothing in two cases,
even when its bindings resolved through the file: a Vue, Svelte or Astro
component's import (`isJsPathImportRef` covered only the script languages),
and a bare scoped package that a `tsconfig` path or a workspace package maps
to a project file (`isJsPathSpecifier` excluded `@scope/pkg`). So a barrel or
a side-effect module showed no dependency from those files, and sync's
importer discovery, which follows the module edge into a changed file,
never found them when a barrel they import through was deleted or added.

Both now resolve through `resolveImportPath`, as a script's path import
does; an outside scoped package still names no file. Component imports
written with an extension, which a basename match linked before, now link
through the resolver too. When a sync adds a file, it also retries the
failed imports that now resolve to it but wait under no key its path gives:
a scoped package parked under its last segment (`common` for
`@excalidraw/common`, mapped to `packages/common/src/index.ts`, whose keys
are `src` and `index`), under the module ceiling, and an import of the
importer's own folder or the one above (`.`, `..`), parked under no tail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:23:41 +00:00
Colby MchenryandClaude Opus 5.5 041db10146 fix(ts): a re-export imports its module, and sync follows barrels of barrels (#2526)
An `export … from './x'` statement emitted one `imports` reference per
re-exported name and nothing for the module itself; `export * from` emitted
nothing at all. So a barrel made only of `export *` lines had no dependency
on what it re-exports (no dependent for `affected`), and sync could not find
an `index.ts` that re-exports another barrel: when the inner one was deleted
or added, the outer one's importers kept their stale or name-matched edges.

A re-export now imports its module the way `import … from` does, in both
extractors (TS and the Rust kernel): an import node named by the specifier
and a module reference that links the barrel to the file it names. Sync's
importer discovery follows such modules to their own importers, carrying
the names each passes on (`export *`, a named or renamed re-export, an
`export * as ns`), as deep as the resolver chases a re-export; an ES
namespace import reads whatever its module passes on. A re-export's import
node does not count as a symbol for the "parse produced no symbols" warning,
so a barrel with an unparseable line still reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:56:50 +00:00
Colby MchenryandClaude Opus 5.5 7ba8eee4a7 fix(sync): imports through a re-exporting module follow it when it is deleted or added (#2512)
Deleting a module that re-exports what it imports (a package's
`__init__.py`, an `index.ts` barrel) left every file importing through it
linked to the symbols it re-exported: those edges point into files that
stay, so the removal cascaded none of them. When such a module was added or
came back, its importers kept the edges a name lookup had made in its
absence, or stayed failed; and in a large Python project the failed import
of the module itself stayed failed, because the failed-import retry skips a
stem with more than 500 imports waiting (Django's `test`, `models`).

Sync now re-opens, as pending references for its sweep, what the files
importing a removed or added module bind through those imports: before the
removed files are deleted, and after the retries for the added ones (with
their failed references and the module's own failed import). Importers are
found by the import edge into the file, by Python import nodes naming the
module or its package, and through what the module passes on (an import
inside a function has no node); in Python, a package or star import
re-exporting a changed module leads on to its own importers, as deep as the
resolver chases a name. All changed files are handled in one pass, with
importer edges and failed references filtered by the bound names in SQL.

Python and the ES-module family only; no schema or extraction change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 07:10:08 +00:00
Colby MchenryandClaude Opus 5.5 a3fdb920a8 fix(sync): a Python import of a name a package re-exports links when the package appears later (#2507)
`from fastapi import FastAPI` reaches `fastapi/applications.py` through
`fastapi/__init__.py`, which re-exports it and declares no node named
`FastAPI`. While that `__init__.py` was missing, or before it gained the
re-export, the calls through the name failed and were parked under the
name, which the package file that then appears or changes never carries.
A fresh index linked them; a sync never did.

An unaliased from-import of a module in the project's own folders is now
parked under the module's key, as an aliased one is (#2501): a relative
import, or an absolute one whose dotted path, or the package holding it,
is a project folder at any depth. An outside package's names (`from
django.db.models import Q`) keep their tails, since on netbox they would
crowd 'module:models', 'module:urls' and others past the retry ceiling.
#2450's by-name lookup still finds every parked ref by its own name.

Two limits made the restored package's importers wait anyway:

- A module key gets a retry ceiling of its own, 5,000 rather than the 500
  a name tail has. A large population under a module key is every
  importer of a package that was missing, not noise: fastapi's 1,069
  `from fastapi import ...` refs, Django's 2,257 under 'module:urls'.
- The CG-33 rebind's per-name ceiling counts only the edges that existed
  before the retry. Relinking every `from django.urls import path` pushed
  `path` over it, so the `path` edges the sync's new definitions moved
  were never rebound; the retry's own edges are still rebound under a
  name that stays below it, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:19:11 +00:00
Colby MchenryandClaude Opus 5.5 5171a6aa9c fix(sync): a Python import alias links its module when the module appears later (#2501)
`from .models import User as UserModel` and `from . import helpers as h`
bind a name their module declares under another: `User`, `helpers`. Every
reference through the alias (the import, a call, a base class, the class
or function passed as a value) was parked under the alias, which no file
a sync adds or changes carries. So a module added after its importers were
indexed, restored after a delete, or given the name in a later edit never
relinked them, while a fresh index did. #2450 fixed the same gap for ES
modules.

Such a reference is now parked under its module's key. A Python import
names its module by a dotted path, counted from the importing file's own
package when it starts with dots: `.models` in app/main.py is
'module:models', `.` is 'module:app', `..pkg` the package above's
'module:pkg'. #2450's lookup still finds it by its own name, which is how
a fresh index links an alias its module never declares. A package's
`__init__.py` also carries the key of the package above it, as a module
file beside it does through its folder, so `from app.pkg import sub as s`
relinks when the subpackage appears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:22:41 +00:00
Colby MchenryandClaude Opus 5.5 33c1436130 fix(js,ts): a default import is what export default declares, not the first exported function (#2432)
A default import resolved to the module's first exported function or
component whenever the `export default` statement wrote a declaration
(`export default function Vans()`, `export default class Store`): only a
separate `export default NAME` statement was read. A React Router 6.4+
data-router page exports its `loader` or `action` above the page
component, so `import Vans, { loader as vansLoader } from './Vans'` bound
the route `/vans` (and every call through `Vans`) to `loader`.

The resolver now reads the file's own `export default` statement from
comment-stripped code. A declaration it writes is found by position, a
binding it names by name, and an anonymous function, class or arrow
function is no node at all (the guess took an exported function beside it,
or one nested inside it). The statement also beats an exported styled/memo
component above it. Every line-initial statement is read and the first one
that names a node of the file wins, so template text that only reads like
a statement names nothing. Strings are not masked: a masker loses step on a
template whose `${…}` holds another template or a regex with a backtick,
and on outline it blanked the real statement below one. Only an expression
default (`memo(Card)`, `new Service()`), a binding the file doesn't
declare, or no statement keeps the old guess.

`require('./x').default` (#2433) reads the same statement first, so it
reaches the declared default too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:11:59 +00:00
Colby MchenryandClaude Opus 5.5 e47cb258c1 fix(sync): a reference through an import binding links its module when the module appears later (#2450)
A default, namespace or aliased import binds a name its module never
declares: `import tagsController from './tag/tag.controller'`,
`import { Component as GroupsPageWrapper } from './groups_page'`. Every
reference through that name (the import, a call, `new`, a base class, a
JSX or template tag, an Express mount) was parked under the name, which
no file a sync adds or changes carries. So a module added after its
importers were indexed, restored after a delete, or given its export in a
later edit never relinked them, while a fresh index did.

Such a reference is now parked under its project module's key
(`module:tag`), which sync already looks up for every file it adds or
changes (#2422), and it is still found by its own name: a binding its
module never resolves is linked by that name alone, as vben's
`{ VbenFormSchema as FormSchema }` from a workspace package is. Schema
v15 adds the partial index that by-name lookup needs; its leading
`status` keeps the planner off idx_unresolved_status, which read 830K
rows on vscode's index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:50:09 +00:00
Colby MchenryandClaude Opus 5.5 2c807f5c13 fix(js,ts): a catch-all or node_modules path alias no longer makes package imports look local (#2437)
The out-of-repo import guard (`isOutOfRepoImport`) never fired in two
setups:

- A catch-all `paths` key. The guard started with `isExternalImport`,
  which treats an import as local when it starts with a root alias's
  prefix, and a `"*"` key's prefix is empty. On cord-field
  (`"*": ["./typings/*"]`), 169 imports of `Typography` from
  `@mui/material` bound to the project's own `Typography`.
- An alias that lands on disk but outside the index. `fileExists` falls
  back to `fs.existsSync`, which also answers for a directory or a file
  under `node_modules`. home-assistant's
  `"lit/decorators": ["./node_modules/lit/decorators.js"]` bound 4,916
  `@property()` decorators to an unrelated `property` field, and
  topcoder's `config` package landed on its `config/` folder.

The guard now skips the alias-prefix test (it already asks
`resolveImportPath` whether an alias maps the import to a file) and
counts that resolution only when it lands on an indexed file. Other
callers of `isExternalImport` are unchanged: resolving an import still
needs the prefix test so `"*": ["src/*"]` aliases resolve at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:04:13 +00:00
Colby MchenryandClaude Opus 5.5 c051ea0a0e fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable (#2448)
goRefQualification read a dotted Go call's package qualifier from its line,
as the line's only `X.Name` spelling. A second spelling of the name left it
with none (`Digest:` beside `digest.Digest(dig)`, `"Log using Infof"`), and
another selector before it was taken instead (`klog` for `err.Error` in
`klog.Error(err.Error())`). It now takes the reference name's first segment,
the only one that can be a package.

A call through a parameter or local that takes an import's name (etcd's
`jwt, err := newTokenProviderJWT(…)`, testify suites' `suite` receivers) is
a call on that variable. A per-file scope reader finds the parameters,
receivers, results, `:=`, `var` and `const` names in scope at the call, so
such a call is no longer one into the package.

The variable's declared type, when its declaration writes one, decides what
the call reaches:
- An outside package's type, or a value an outside package's function hands
  out (`clock := clocktesting.NewFakePassiveClock(…)`), has none of the
  project's methods.
- A project package's type is resolved as the receiver's type. This also
  covers parameters declared through a package (`logger logger.Interface`).
- A field read through the local (`metadata.Type.String()`) keeps its own
  type.

A sync drops a file's scope reading with the other per-file memos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:03:52 +00:00
Colby MchenryandClaude Opus 5.5 9a1d4bd91d fix(js,ts): require('./x').default reaches an ES module's default export (#2433)
`const X = require('./x').default` and `const { default: X } = require('./x')`
map to a named import of `default`. That is right for a CommonJS module that
sets the property itself (`exports.default = fn`, or the dual
`module.exports = X; module.exports.default = X`), but a module written as an
ES module and compiled to CommonJS sets it to its default export, which is no
named export. References through the binding stayed unresolved (bitwarden's
`new OsBiometricsServiceMac(...)`) or fell to name matching: the local
variable holding the module, or a namesake method in another package.

findExportedSymbolWalk now falls back to the module's ESM default export when
the named lookup finds no `default`. Only for JS-family refs, only in the
module the require names (`export * from` forwards no default), and only when
that module has an ESM default export at all: a single-file component, or an
`export default` statement. A CommonJS module's `exports.x = function`
declarations are flagged exported, so without that gate the
first-exported-function guess would invent a default for it.

bitwarden +2 edges (the two failed `instantiates` refs). react-native +872
-617: all 617 re-resolved at the same site (541 from the local require
binding to the real function or class, 47 from a namesake method in another
package to the right module, 28 metadata only, 1 from a type signature to the
implementation), 870 of 872 added edges on the declared default or what it
holds. express, eslint, mocha, koa, body-parser, fastify, topcoder,
proshop_mern, next-saas-starter and create-t3-turbo byte-identical. Dead-code
claims unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:49:21 +00:00
Colby MchenryandClaude Opus 5.5 f261fda5c2 fix(js,ts): import { default as X } is the module's default import (#2412)
bulletproof-react's router imports its app shell as
`import { default as AppRoot, ErrorBoundary as AppRootErrorBoundary }
from './routes/app/root'`. The import mapping recorded AppRoot as a
named import of an export called `default`, which no module declares,
so resolveViaImport found nothing for it and every reference fell
through to name matching. The route /app bound to AppRoot by
exact-match, and in a project where another file declares an AppRoot
(or a default export is imported under a name another file uses) the
call, route or JSX attribute bound to that file's symbol instead.

The mapping now records `{ default as X }` as the default import, the
same as `import X from`, so every reader of isDefault agrees with it:
the import resolver finds the module's default export, the binding's
own `imports` reference links the module file as a default import's
does, and the JSX-child, Expo native-module, HTTP-client and styled
Link lookups read it as the default import. Re-exports
(`export { default as X } from`) were already chased as the default.
`require('./x').default` is unchanged: `.default` there is a property
of module.exports, which a CommonJS module can set by name.

alan2207/bulletproof-react: 4 edges re-resolved at the same sites (the
route /app reaches AppRoot by import; three binding imports link their
module file). bitwarden/clients: 2 failed Storybook imports now link
their module. bradtraversy/proshop_mern, leerob/next-saas-starter and
t3-oss/create-t3-turbo: byte-identical. Dead-code reports unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:58:25 +00:00
Colby MchenryandClaude Opus 5.5 d8a7f868b3 fix(go): an unaliased import is also known by the name goimports assumes for its package (#2410)
An unaliased Go import was named by its path's last element, so the
qualifier in yaml.Node (go.yaml.in/yaml/v3), klog.V (k8s.io/klog/v2) or
sqlite3.Error (github.com/mattn/go-sqlite3) matched none of the file's
imports. The reference then resolved by its bare name to whatever project
symbol shared it.

An unaliased import now also takes the name goimports'
ImportPathToAssumedName gives its path (the last element that isn't a
major version, without a go- prefix, cut at the first non-identifier
character), unless another import of the file is bound to that name or a
second import assumes it too. The last element stays registered:
k8s.io/api/core/v1 really is package v1.

The import scan now reads only the import section, without comments: the
last word of a comment had become the next import's alias, and imports
spelled in doc comments or string literals were read as real ones.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:15:52 +00:00
Colby MchenryandClaude Opus 5.5 3f5a9e2e1d fix(sync): an import links the file it names when that file appears later (#2392)
A sync retried parked failed refs by name_tail (#1240), but an import
names a file, a folder or a namespace, not a symbol, so the lookup never
found one: the tail of `package:app/b.dart` was `dart`, of `inc/db.php`
`php`, of `./req` `/req`. Most languages never parked theirs at all: an
import that named no file reached the importing file's own `import` node
through matchByQualifiedName (its qualified name is the module path) and
resolved there, so `./x` in TS/JS, `import pkg.mod`, `#include "a/b.h"`,
`<stdio.h>`, a C# `using` of an outside namespace, Go module paths and R,
Solidity, Svelte and Vue imports were consumed by an edge to themselves.
A Lua `require` bound the local it is assigned to. The import linked
only once the importer changed, or on a full index.

- resolveOne's target gate drops an `imports` ref's own import statement,
  so the ref parks as failed (#2388 did this for Dart alone).
- A Lua / Luau `require` resolves to a module file or nothing.
- An import written as a path is parked under its last segment's stem
  (`package:app/b.dart` -> `b`, `./z.js` -> `z`): src/db/reference-tail.ts.
- Sync retries the failed imports an ADDED file can satisfy: by its name,
  stem and folder (importPathKeys), as a failed import's tail or whole
  name, and by the changed files' namespace / module names as a whole
  name (C# `using Foo.Bar`, Java `import com.z.*`). Same per-key ceiling.
- Schema v12: two partial indexes over failed imports, which the planner
  needs to avoid reading every failed call that shares a tail; the
  migration rewrites the tails of path imports parked by an older version.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:03:57 +00:00
Colby MchenryandClaude Opus 5.5 83f54a1853 fix(dart): a library links the files its part directives name (#2391)
A Dart `part 'x.g.dart';` now mints an `import` node (name = the URI,
signature = the directive) and an `imports` ref from the library's file
node, in both extractors. The ref resolves through `dartDirectiveFile` by
the rules an import or export follows, so the library's file links the
part's file. A library and its parts are one library, so a change to a
part, generated or hand-written, now reaches the library and what imports
it in `codegraph affected` and the files listed as using the part. Before,
neither `part` nor `part of` left a trace, and the two files were joined
only by whatever symbol edges happened to cross between them: riverpod's
framework.dart, which holds nothing but `part` lines, by none.

`part of` still records nothing: the library's edge already joins the two
files, and a part -> library edge would list every pair on both import
rails of both files and make each a file 2-cycle.

The kernel's directive arm now goes on into the directive's children, as
the TS ladder does. Its `import_or_export` arm used to return first, so an
annotated directive's arguments (`@Tag(f) import 'a.dart';`) were fn-ref
candidates on the wasm path only.

Validation on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2
with the kernel loaded, base 837a1866:
- bloc: +53 import nodes, +53 contains, +53 imports (library -> part).
- riverpod: +294 import nodes, +294 contains, +258 imports, +36 refs left
  unresolved (generated parts that are not committed).
- Nothing removed; kernel and wasm dumps byte-identical; parity sweeps
  0 diffs. Every added edge sits on a `part` line whose URI names the
  target, and the target's `part of` names the library back (311/311).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:16:55 +00:00
Colby MchenryandClaude Opus 5.5 631df0138e fix(dart): an import or export links the file its URI names, not a namesake (#2388)
A Dart `import` / `export` now resolves its URI through the library model
in src/resolution/dart-libraries.ts: `package:<name>/<path>` is
`<root>/lib/<path>` of the project package whose pubspec.yaml says
`name: <name>`, any other URI is a path from the importing file, and a
`dart:` library, a package from outside the project or a file the index
does not hold links nothing. The ref never reaches the name-matcher, which
took the URI's last segment for a file name (`package:flutter/widgets.dart`
reached riverpod_analyzer_utils' widgets.dart from 130 files, bloc_tools'
`package:args/command_runner.dart` its own command_runner.dart) or bound
it to the importing file's own `import` node.

When packages share a name and neither the importer's own package nor one
enclosing it settles which, the importing package's `path:` dependency of
that name (pubspec_overrides.yaml, then pubspec.yaml) does: bloc's two
example apps each keep an authentication_repository. Library visibility
reads the same answer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:09:46 +00:00
Colby MchenryandClaude Opus 5.5 0571286907 fix(js,ts): link calls through an import whose name contains $ (#2378)
The ES import regex in extractJSImports captured default and namespace
bindings with \w+, which stops at a `$`. So `import items$ from './store'`,
`import $ from './dom'` and `import * as ns$ from './ns'` failed the whole
statement and produced no import mapping, and `items$.getState().inc()`
never reached the store action. The `as` split of named imports, the
`a: b` split of a CommonJS destructuring and the re-export checks had the
same \w-only shape. Some bound a cut-off name instead (`{ a as b$ }` bound
`b`, `{ default as items$ }` bound `items`), and `export { a$ } from` was
dropped from barrel chasing.

Capture every binding with the ASCII identifier class [A-Za-z_$][\w$]*,
keeping the `type`-modifier lookahead: `import type from './x'` still
binds `type`, and `import type $T` / `import type from$` now bind. A
default binding is never followed by `{`, so the `${` of a code
generator's template literal (`import ${x} from '${src}'`, as in qwik's
optimizer) is not taken for a binding.

importShadowedAt and hasParameterBinding bounded the name with \b, which
never fires beside a `$`, so a local `const items$ = ...` or an `items$`
parameter did not shadow the imported store. Named `$` imports already
got those wrong edges on main, and the mapping fix would have extended
them to default and namespace imports. Bound the name with (?<![\w$]) and
(?![\w$]) instead.

Resolution-only: the kernel has no import-mapping mirror. A store
exported by `export { items$ }` (the \b in isExportedLater's clause
branch, left out of #2366) is a separate extraction fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:41:06 +00:00
deac7710f2 fix(go): resolve unexported receivers and nested modules within the right package (#2323, #2322) (#2361)
Calls through a method receiver or parameter of an unexported type
(`func (s *server) Create() { s.service.AddItem() }`) had no edge: the
Go receiver inference only accepted PascalCase types (#2323). And only
the project-root go.mod was read, so a module in a subdirectory, or one
beside other modules as in etcd, treated imports of itself as
third-party (#2322).

Fixing either alone sends calls to the wrong package, because Go type
names are unique only per directory while types and methods were looked
up by name across the project. The receiver pattern alone sent 1,042 of
harbor's 1,998 new calls to another package's same-named type; reading
nested modules alone made harbor's caching wrappers call themselves and
put about 180 of etcd's type references on same-named methods. Both
share one fix:

- A lowercase receiver or parameter type is read only where a parameter
  list puts it (receivers, parameters, multi-line lists, var blocks).
- Receiver inference also reports the type as written. For Go,
  resolveMethodOnType takes the declaring package's directory (the
  caller's for a bare name, the import's for a qualified one) and counts
  only that package's methods, then the types its declaration embeds
  (struct and interface embedding, read from the source) instead of the
  name-based supertype union. The field-chain matcher scopes its struct
  and field types the same way.
- The resolver reads the nearest go.mod of every directory holding
  indexed Go files (no disk walk; a module under testdata/ or a _/.
  directory serves only its own files) and maps an import path to the
  module with the longest module path, the importing file's own module
  breaking a tie. getGoPackageDir replaces getGoModule.
- A bare name written through a package (`job.OPCommand`) resolves
  through the import into that package's directory, and a name-matched
  candidate outside the qualifier's package is rejected; a method reached
  through a variable that shadows an import is exempt.

Verification: new go-unexported-receiver (3) and go-nested-module (11)
tests: 12 fail on main (the 2 controls pass), all 14 pass. The issues'
repros go from 0 to 1 caller; the root-go.mod and `Server` variants are
unchanged.

Validation, main 26e8488a -> this branch, whole repos, Go->Go edges keyed
by source/target qualified name, file, line and column:
- etcd 6bb7e5e: 52,530 -> 66,284 (+13,827 / -73), calls 18,074 -> 25,265
- harbor 721c6d4: 62,423 -> 77,615 (+15,819 / -627), calls 16,308 -> 25,236
- prometheus 770ca8f: 90,970 -> 92,399 (+2,249 / -820), calls 40,050 -> 41,493
Of the 1,520 removed edges, 1,458 are the same call site or reference
now resolved to the right package's symbol; the other 62 were field
reads taken for method values, names whose real target is not indexed
(a `type T = string` alias), and a call to a method the receiver's type
does not have. Indexing time and CPU are unchanged.

Co-authored-by: Diao Shengjia <104132148+GoDiao@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:16:14 +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
5271c89f08 fix(php): a class written with a namespace in its name resolves to that class (#2256) (#2286)
* fix(php): a class written through a namespace alias resolves to its class (#2256)

`use App\Fields as Field;` aliases a namespace, so `new Field\FirstName()`
names `App\Fields\FirstName`. Expand the first segment through the file's use
map and look the class up by its qualified name, before the pre-filter that
drops names without a `.` or `::`.

(cherry picked from commit 3917de431cbe8d8bbba32d9f4c739458c36e3fed)

* fix(php): resolve every qualified class name the way PHP reads it (#2256)

Extends the namespace-alias fix beyond `new`: extends, implements, static
calls (`Alias\X::make()`), names relative to the current namespace and fully
qualified `\Ns\X` now all resolve by their exact qualified name. A name
with no single project class is outside the project, so it no longer falls
through to a same-named guess; a static method the class only inherits is
left to the strategies that walk supertypes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 12c742a917ebd455840c148db1e37c895b9587ad)

* docs(changelog): one entry for the PHP qualified class names fix

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

---------

Co-authored-by: drakeo338 <paranoyouz@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:20:59 +00:00
Colby MchenryandClaude Opus 5.5 c4babfce6a fix(js): a CommonJS require and a module path import the file they name (#2263)
* fix(js): a CommonJS require and a module path import the file they name

A `require('./application')` was only a call, so a CommonJS file depended
on nothing at file level: Express's lib/express.js showed no imports and
impact missed the files it loads. Local require specifiers are now read
after extraction (both the wasm extractor and the kernel, including its
deferred-decode transport) as `imports` refs from the file, and a JS/TS
module path resolves to its file the way the runtime finds it —
extensions, index files, aliases — which also lands an extensionless
`import './polyfills'` on its file instead of its own import node.

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

* fix(js): read require imports inside each extractor, so kernel parity holds

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:37:37 +00:00
Colby MchenryandClaude Opus 5.5 34ede4de88 fix(python): a module member follows the package's re-exports too (#2248)
resolvePythonModuleMember (`mod.member` after `import mod` / `from pkg import
mod`) looks the member up with pythonModuleSymbol, so a package's
__init__.py re-exports lead to the definition.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:34:57 +00:00
Colby MchenryandClaude Opus 5.5 6d8802cef8 fix(python): an imported name follows the package's re-exports to its definition (#2247)
pythonModuleSymbol: a name imported from a module is its top-level
definition, or one the module re-exports by name (`from .tokens import
Token`) or through a star import (`from .users import *`, read from source),
three packages deep, memoized.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:29:55 +00:00
Colby MchenryandClaude Opus 5.5 da1312e83b fix(kotlin): read Kotlin imports; an outside import owns its name (#2196)
The JVM import reader required a `;`, so a Kotlin file had no import
mappings at all. It now reads `import a.b.C` and `import a.b.C as D`.

- The outside-import rule from #2195 applies to Kotlin: `import
  java.net.Proxy`, `import org.mockito.Mockito.mock`, `import okio.buffer`
  own their names. A declaration the file itself makes comes first, in its
  own namespace (a callable for a call, a type otherwise) - Exposed's
  `toLocalDateTime(value)` beside `import kotlinx.datetime.toLocalDateTime`
  is the column type's member; gson's `new URI(...)` stays `java.net.URI`
  beside `TypeAdapters`' field `URI`.
- The mappings pick among same-named project classes: okhttp's
  `MockResponse.Builder()` (642) and `Request.Builder()` reach their classes,
  Exposed's `TestDB`/`SchemaUtils` the module the file imports, moshi's
  `Moshi.Builder()` (102).
- A renaming import binds its alias by FQN: kotlinx.coroutines' `import
  ...internal.unsafeFlow as flow` makes its 28 `flow { }` builders
  `unsafeFlow`, not the public `flow`.

okhttp -486 +1303, Exposed -1685 +1351 (duplicate-module reroutes; losses
are kotlin.test/kotlinx/okio/clikt/Gradle functions), moshi -21 +121, okio
-128 +51, koin -22 +11, coroutines -68 +47, nowinandroid -44 (Gradle DSL,
Compose). Java repos (gson, commons-lang, retrofit, jsoup, halo)
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:45:40 +00:00
Colby MchenryandClaude Opus 5.5 7c1c9a3691 fix(ts): export * as ns exports the namespace, not every member (#2192)
`export * as core from '../core/index.js'` was recorded as a plain wildcard,
so a barrel walk for `z.number()` went into core, on through core's own
`export * as regexes`, and landed on the `number` regex constant - ahead of
the `export * from './schemas.js'` that holds the schema function. zod's
`z.string()` (262 calls), `z.number()` (141), `z.email()`, `z.boolean()`...
all went to regex constants.

- A namespace re-export is now its own kind: it supplies only `ns`, and a
  member path through it (`z.coerce.number()`, `z.core.safeParse()`)
  continues in that module. A 3-segment chain is resolved when its root is an
  `import * as` binding and only through the import resolver - untyped chains
  stay unresolved as before.
- A named re-export (`export { safeParse } from '../core'`) is found for a
  namespace import's member; the walk compared against `'*'` and only reached
  those through the misread wildcard.

zod -471 +571 (every regex-constant edge now reaches the schema function;
`z.lt` -> core `_lt`, `z.coerce.string` -> coerce.ts), trpc +4 and kit +1
(namespace-import members through named re-exports). typeorm, hono,
excalidraw, element-plus, starlight unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:01:48 +00:00
Colby MchenryandClaude Opus 5.5 e77ca83621 fix(js): resolve calls through CommonJS require bindings (#2189)
Four gaps left CommonJS calls on the local alias variable - or on the wrong
function:

- `require('../')` up to the project root produced an empty relative path, so
  its `/index.js` candidate was never found;
- a default import never followed `module.exports = X` /
  `exports = module.exports = X` / `module.exports = require('./lib/x')`;
- only the first declarator of `var a = require('x'), b = require('y')` was a
  binding, so `b()` - a package call - stayed on the local variable;
- `require('./utils').member` was read as the module's DEFAULT export: every
  such alias in express resolved to utils' first exported function
  (`normalizeType`). It is now the named member, and `exports.name =
  function impl(...)`, `module.exports.name = impl` and `module.exports = { a,
  b: c }` bind names the export index could not see.

express -1087 +727 (707 `express()` calls reach createApplication; supertest /
node:assert calls no longer edge to the local variable), eslint -750 +827
(`new RuleTester()` reaches the class, not another test's variable), mocha
-83 +90 (`run(...)` no longer lands in another spec file), koa 65 wrong
`request` edges -> `response`, body-parser +20. zod, trpc, hono, typeorm,
axios unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:49:15 +00:00
Colby MchenryandClaude Opus 5.5 37bcd5c0ae fix(react-router): nested, lazy and constant routes; per-app tsconfig aliases (#2165)
- Route tables compose nested paths: a child's `path` is relative to the
  path-bearing routes around it (`children` of a data router, nested
  `<Route>` elements), absolute ones reset. `lazy: () => import('./x')`
  (object and JSX) renders the module's default export (`Component` export
  as fallback) via a claimed `lazy-import:` ref. `path: paths.app.root.path`
  keeps its parts on the node's signature and is named in postExtract from
  the constant's object literal (found through the file's import). The
  route keeps its extracted id; `isReactRouterRoute` accepts it. An element
  wrapped in parentheses or a guard (`<ProtectedRoute>`, `<Suspense>`,
  `*Provider`, `*Guard`) renders the first element inside.
- Import aliases: the tsconfig / jsconfig nearest the importing file that
  declares `paths` is tried before the root's, so each monorepo app resolves
  its own `@/*` / `~/*`.

A/B (edges removed / added): bulletproof-react -43/+367 (0 → 9 named,
linked routes; cross-app imports fixed), react-native-reusables -103/+193,
kit -184/+218, trpc -45/+71 (examples' `~/…` now their own), create-t3-turbo
-1/+13; excalidraw, halo, obytes, realworld byte-identical. #1348's nested
fixtures now expect composed paths (`/dashboard/settings`, `/data/prefs`).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:34:11 +00:00
Colby MchenryandClaude Opus 5.5 67744468d2 fix(resolution): Expo module bindings, imported receivers, TS type-only imports (#2134)
- Expo Modules: a JS call on a `requireNativeModule('N')` binding — typed
  (`<CameraNativeModule>`) or not, local or imported (expo-camera's
  `import CameraManager from './ExpoCameraManager'`) — resolves to module
  N's declared function, Swift first and Kotlin beside it, else to the
  member on the binding's declared type. It went to the same-named static
  method making the call.
- A JS/TS member call on an import binding never picks a method declared in
  the calling file, and ruling those out never manufactures a unique guess.
- `import type { X }` was parsed as a default import named `type` (every
  type-only import bound `type`), and `{ a, type B }` bound `type B`.
- A name the file binds from an out-of-repo package names nothing in the
  project, for every reference kind, and JS/TS fuzzy matching is exact-case:
  trpc's 402 `Record<…>` references went to a `record` property, element-plus's
  121 `mount` imports from @vue/test-utils to a test helper.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:28:28 +00:00
Colby MchenryandClaude Opus 5.5 7a51969fb0 fix(resolution): JS/TS calls never pick a class member by name alone when the code rules it out (#2120)
Found by the README-wide sweep: the "most depended on" lists of popular
repos were topped by members that nothing actually calls.

- A member call on a NAMESPACE import (`import * as z from "zod/v4"`) calls
  one of the module's exports, never some class's method; a receiver
  imported from a package outside the repository names nothing in it.
  matchMethodCall's name/word-overlap strategies no longer run for either.
  zod: `z.string()` had bound 2,640 calls to a test helper's `string`
  getter (Mocker::string topped the hub list with 3,209), `z.toJSONSchema`
  371 to ZodType::toJSONSchema; trpc `z.record` to a generated SDK class.
- A receiver-less JS/TS or Go call cannot reach a PROPERTY, FIELD or enum
  case either (it already excluded methods, #1714/#1857): typeorm's mocha
  `it(…)` bound 2,879 calls to an interface's `it` property and `describe(…)`
  1,368 to a command class's `describe` string field.
- A bare call to a name the file imports from outside the repository
  (`import { useQuery } from '@tanstack/react-query'`, CommonJS
  `require('supertest')`, vitest's `test`) gets no name-matched candidate.
- The React resolver leaves an imported component/hook/context to import
  resolution (framework resolution runs first and picked any same-named
  project hook — trpc tests' `useQuery` landed on a hook nested inside one of
  trpc's factories), and never targets a nested hook; same-file hook first.
- "Outside the repository" is the coordinator's `isOutOfRepoImport`:
  isExternalImport (aliases, workspace members) AND not the repo's own package
  name AND unresolvable by resolveImportPath — so axios's own tests'
  `import { getAdapter } from 'axios'` now resolve to lib/adapters.js instead
  of the .d.ts interface member.

A/B vs main (edge diffs, every change classified): zod −5,228, typeorm
−4,318, express −700 (calls onto the `var request = require('supertest')`
binding), trpc −350/+17, excalidraw −233/+3 (props callbacks onto Props
interface members), hono −150, axios −52/+23 (the self-import upgrade),
bulletproof-react: 156 sites that pointed at ANOTHER app's component/hook in
the three-app monorepo now point at the importing app's own, 16 wrong
cross-app links dropped, 8 imports the React heuristic had guessed right lost
(per-app tsconfig paths don't resolve — separate follow-up); gin/cobra
byte-identical, react-redux-realworld byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 04:39:30 +00:00
Colby MchenryandClaude Opus 5.5 fef37765c1 perf(resolution): remove the indexing regressions since v1.6.0 (vscode stall, per-ref rescans) — graph byte-identical (#2072)
* perf(resolution): read JS/TS parameter lists without exponential backtracking

isLocallyBoundJsName (#1759, cd4e65b5) scans a file for a parameter that
binds a bare-called name. Its per-parameter pattern `(type)?(default)?\s*`
let one `name: T = value` text split several ways, so when the name was
absent the search backtracked through every combination. On vscode's
markersModel.test.ts, whose helper takes nine typed, defaulted parameters,
each `suite`/`test` scan took 30-40s — one pool worker held a whole
resolution batch while the rest idled, a 70-400s stall per run.

Each parameter now has exactly one parse: its first non-space character
after the identifier picks the type, default or bare alternative. The
alternatives match exactly the strings the old optional groups did, so the
result is identical for every input (4M generated cases and 5.6k real
vscode file/name pairs agree); the scan is 0.1-0.3ms on that file.

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

* perf(resolution): memoize Python module-file lookups per resolution pass

#2034 (5eaa6fed) sent every Python named import whose path the import
resolver cannot place — stdlib and third-party modules such as
`from unittest import mock` — through findPythonModuleFile, which scans
every `__init__.py` file node for a path suffix. It ran once per ref:
on django that scan alone went from 0.6s to 1.4s of resolution CPU.

The candidates for a module depend only on the module path, so they are
now collected once per resolution context, in the same name-lookup order;
the importing file is still excluded per call, so the first survivor is
the node the scan returned. The memo drops with the other import-resolver
memos. Resolution output on django is hash-identical; single-threaded
resolution CPU there goes from 6.5-6.8s to 4.9s (v1.6.0: 4.5s).

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

* perf(resolution): test sealed-module candidates cheapest-first

isSealedModule (#1719, bffd50e4) masks a candidate file's comments and
strings before asking whether it is a module that exports nothing. Import
refs call it for every same-named candidate across the tree, so on vscode
it masked nearly every TypeScript file once per resolver: 3.3s of masking
in a single-threaded pass, paid again in each pool worker.

The verdict is a pure conjunction, so it now asks the cheap questions
first: the source contains `import`, no node in the file is exported, and
there is no CommonJS export. Masking runs only for files that pass all
three, which in practice are the few that can be sealed. The masker blanks
text and never deletes it, so a file without `import` in its source has
none in its masked code either. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): scan a class for a field's declared type once per field

matchTsThisFieldCall (#1496, cece0720) resolves `this.<field>.<method>()`
by finding the field's declaration in the enclosing class. It re-split the
whole file, stripped comments line by line and compiled three patterns for
every such call, although the answer depends only on the class and the
field: 16.5s of a 148s single-threaded resolution pass on vscode.

The first declaring line — `: typeof X`, `: X` or `= new X`, in that order
per line — is now memoized per (class node, field), and the last few
classes' stripped lines are kept, since calls arrive file by file. Owners
are still tried in the same order and everything after the declaration
lookup is unchanged. Resolution output is hash-identical on vscode and
django.

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

* perf(synthesis): start cross-tier receiver chains only at a word's start

The cross-tier pass (b1f40c57) scans every non-test JS/TS file whose text
has a `.add(`, `.get(` or `.on(` — nearly all of vscode — with receiver-
chain patterns such as `a.b.c.add('job')`. A chain could begin at any
letter of an identifier, so each letter re-read the dotted chain behind
it; on vscode the pass ran 35s on one pool worker and set the synthesis
phase's wall.

A match that starts mid-word implies one from the word's first letter, and
every match of these patterns ends on a non-word character, so a scan
never resumes mid-word: a lookbehind that rejects those starts skips only
attempts bound to fail. Over all 14,135 vscode JS/TS files the five
patterns return identical matches (positions and groups) before and
after, in 8.8s instead of 32.8s; the pass itself drops from ~9.8s to
~5.6s CPU standalone.

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

* perf(resolution): filter same-named candidates in one pass, cheap checks first

Interface members (#1638) and nested const-bound functions (#1669) made
common names — `editor`, `model`, `dispose` — carry thousands of nodes.
matchByExactName ran ten chained filters over that list for every
reference, copying it once per rule before the source-reading checks ran,
and the method-call strategies copied it again to put the call site's file
first before looking at only the class-like or same-file entries.

Each rule judges one candidate on its own, so the exact-name rules now run
as a single pass with the kind and language checks ahead of the lexical,
sealed-module and local-binding checks; the class strategies narrow to
owner kinds before ordering by file (the same nodes in the same order),
and the object-literal strategy, which keeps same-file holders only,
skips the reordering altogether. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): scope language-specific node scans in SQL

Eleven synthesis passes stream every node of a kind and keep only their
language's (Go, Java/Kotlin, XML, native, ArkTS, Nix, Erlang). On a
TypeScript monorepo the Go passes materialized all 113k vscode methods to
find two Go ones — goMethodContains alone went from 0.5s to ~2.9s on the
main thread after interface members (#1638) grew the method count and the
canonical ORDER BY (#1988) landed.

iterateNodesByKindIn adds the language predicate to the same query. Its
ORDER BY (file_path, start_line, id) is a total order, so it yields
exactly the nodes the callers kept, in the same sequence.

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

* perf(synthesis): run the synthesis-site index and source-gate scan beside the passes

Two serial main-thread steps landed with the incremental synthesis refresh
(#1988, #2038). The bulk-edge window rebuilt idx_edges_synthesis_site
before synthesis — its partial predicate runs json_valid over the
metadata of every edge (~1.5M rows, 2.7s on vscode) — and after the merged
insert synthesis re-read every file in the project to record its source
gates (~1.9s). Neither result is read until the index is finished: only
sync consults the index, and the gates depend only on the files.

The window's end now leaves that one index for later, and the resolver
builds it while the pool computes the passes, when the main thread was
only waiting; the source-gate scan runs in the same window. Both still
happen before resolution returns, whatever path synthesis takes, and a
crash in between is healed on the next open as before. On vscode
edge-index-recreate drops from 8.3s to 3.6s of serial time; the graph,
synthesis_inputs and the final index set are identical.

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

* perf(resolution): stop re-reading resolved targets and re-probing missing files

Every resolved reference now re-reads its target node several times —
the kind gate (#1796), the cross-family language gate (#1986), alias
following (#1482) and edge creation — through the query layer's
1,000-entry node cache, while targets recur ~5x (1.46M vscode edges onto
293k nodes). The language gate alone spent ~6s of single-threaded
resolution on vscode fetching whole nodes to read one field, and
fileExists re-probed the same missing candidate paths (every extension of
a `.js` specifier) from every importing file, now under a path.resolve
containment check (#1631).

The resolver keeps its own id → node cache and a memo of filesystem
probes, both in the stable window of its other caches and dropped by
clearCaches; the language gate returns early when the reference's
language has no code family (no target can cross one) and otherwise
memoizes each target's language. Resolution output is hash-identical on
vscode and django.

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

* perf(resolution): cheaper sealed-module and receiver-less call checks

isSealedModule read and decoded a candidate file — its source and every
node — to learn whether anything is exported, though nearly every module
does. It now asks one indexed EXISTS probe first and reads the source only
for files that export nothing (the conjunction is unchanged).

isReceiverLessCall (#1759, #1857) compiled a pattern per call and
end-anchored two more over the line's prefix, which V8 scans from the
start. It now matches the name literally with a sticky `\s*[(<]`, and reads
the prefix backwards: the last non-space character decides whether a
receiver precedes the call, and the word run ending there decides the
keyword exception — exactly what `[.\w$\])]\s*$` and
`\b(?:return|...)\s*$` accepted. Resolution output is hash-identical on
vscode and django.

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

* perf(synthesis): pull passes onto workers heaviest-first; cheaper registry and tier scans

The pooled fan-out handed every pass out at once, so each worker held a
count-based share and a heavy pass finished only with its worker's whole
share (vscode's registry pass ended ~16s in). Workers now pull one pass at
a time, heaviest first; edges still merge by registry index, so the output
is unchanged. vscode synthesis: 14.5s vs 19.6-24.4s.

The registry pass retried its greedy name at every character of every
identifier; both scans now start a name only at an identifier's first
character or a `this.` (a mid-identifier resume after a `.method` dispatch
falls back to the unguarded pattern), and dispatch lines come from a
newline-offset lookup instead of splitting the prefix per match. Checked
match-for-match against the old patterns on every vscode JS/TS file plus
300k fuzzed strings. The tier pass reads a file's nodes only when a site
needs them. Standalone on vscode: registry 5.6s -> 4.5s, tier 5.1s -> 4.4s.

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

* perf(resolution): lean export indexes; no per-name regex compiles in field and local-binding checks

Three per-lookup costs the resolver pool paid once per worker:

- A file's export index decoded every node in the file and read its source,
  for each file an import reached, in every worker. It now reads only the
  exported rows (same `start_line, id` order); the default-export binding
  and names a local `export { a as b }` clause introduces are read on first
  use, from name-targeted rows. findExportedSymbol across vscode's workers:
  12.5s -> 3.7s.
- The `this.<field>` declaration scan compiled three patterns per field of
  every class and walked every class line. Each pattern needs the field as
  a whole `[\w$#]` token, so a class's lines are indexed by token once and
  three sticky patterns, compiled once, are tried at the field's token
  occurrences. Identical on 721k vscode (file, field) pairs; 56.7s -> 1.1s
  standalone.
- isLocallyBoundJsName searched the whole file with four patterns per name.
  Each can only start at a `const`/`let`/`var` or `function`/`class`
  keyword, an `=>`, or the `(` opening a list before the name, so the
  patterns (compiled once per name, sticky) are tried there. Identical on
  198k vscode (file, name) pairs; 68.1s -> 10.7s standalone.

Replaying every vscode (2.19M) and django (270k) reference single-threaded
gives the same result hash before and after.

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

* perf(resolution): prepare kind-filtered edge reads once; memoize the inherited this-member walk

- getOutgoingEdges / getIncomingEdges with a kind filter prepared a fresh
  statement on every call, a third of the read's cost; they are issued per
  node by supertype walks and the member-lookup synthesis passes. Each SQL
  shape is now prepared once per connection (dropped on rebind).
- The post-loop `this.<member>` pass walked the same supertypes for every
  reference, reading each one's contains edges and decoding members one by
  one until a name matched. Nothing is written until the pass ends, so each
  type's supertype edges and each supertype's callable members (by name, in
  edge order) are read once. vscode: 4.1s -> 1.0s.
- Express's mount pass decoded every JS/TS file to look for `.use(`; it now
  checks the raw bytes first (an ASCII needle survives UTF-8 decoding
  unchanged) and decodes only files that have it.

A full vscode init dumps the identical graph before and after.

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

* perf(resolution): fan the next batch out before inserting edges resolution never reads

The batch loop inserted every batch's edges before fanning out the next
one, so the pool sat idle through each insert. That ordering exists for
one reason: later batches walk supertype chains over the `extends` /
`implements` edges earlier batches resolved. Those are the only edges
resolution reads, and the prerequisite phase drains every such ref first,
so after it no batch makes one. A batch without them now fans the next
batch out first and inserts while it resolves: the same writes in the same
order, and nothing the workers read differs. The kinds live in one
constant, shared with the supertype walk.

vscode resolution, three interleaved rounds against the parent commit:
50.3-51.0s vs 56.8-59.1s wall (the pool's wait per batch halves; the
overlapped inserts cost ~7s more CPU). Full vscode and django inits dump
identical graphs before and after, and identical to main.

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

* perf(db): prepare the edge-endpoint existence check once

Every edge insert checks its endpoints with a 500-id IN query, a chunk at
a time, and prepared that statement afresh for each chunk. The batch loop
now runs these inserts on the main thread while the pool resolves, so the
full-size statement is prepared once; only a final partial chunk prepares
ad hoc (as deleteReferencesByRowIds already does). Same SQL, same result.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:57:18 +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
17d09bf506 fix(resolution): resolve object-literal function references safely (#1932) (#2031)
* fix(resolution): resolve same-file calls through object-literal aliases (#1932)

`api.getUser()` where `api` is `const api = { getUser }` (or
`{ getUser: fetchUser }`) and the function is declared outside the literal
resolved to nothing in the literal's own file: the #1573 containment lookup
only finds members defined inside the literal, and the binding-following
alias path existed only on the import side, so same-file callers were
missing from callers / impact / blast radius.

- Extract the member-binding read into `objectLiteralMemberBinding`, which
  splits the literal into its own top-level members on comment- and
  string-blanked source, so a nested object, a member body, a comment or a
  string never donates a binding, and `{ fn: 1 }` names nothing. The import
  path now uses it too.
- In the same-file object-literal receiver strategy, fall back to that
  binding: a symbol of the file, else one of its imports, skipped when a
  parameter or nested declaration shadows the name where the literal is
  written.

Resolution-only change; extraction output (and kernel parity) unchanged.

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

* test(resolution): a literal wrapped in Object.freeze still names its members (#1932)

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

* fix(resolution): resolve object-literal function references safely (#1932)

Same-file object-member calls only found functions defined inside the literal.
Share lexical binding resolution across same-file, import, and alias-forwarding paths.
Respect exact source columns, top-level membership, shadowing, and property overwrites while preserving wrappers and inline members.
Add JavaScript and TypeScript regressions and verify kernel/WASM parity.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:39:18 +00:00
Colby MchenryandColby McHenry d3f9ef9bef fix(resolution): follow alias bindings to the function they name (#1482) (#1808)
Adapt upstream PR #1485 by @valkyriweb (cc791cfc51)
onto current main. Keep the upstream alias-binding module and six-test suite
verbatim, preserve target-kind gating and default-export bindings, and add
one credited Unreleased changelog entry.

Linux verification (x86_64, Node 22.19.0): TypeScript build and asset copy
pass; the fresh ./impl.js repro changes callers/impact of realImpl from
missing consumerFn to including it. All 6 upstream tests and 231 related
resolver regression tests pass.

Fixes #1482

The Forge PR will supersede upstream PR #1485.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 19:22:00 -05:00
374b3b4209 fix(resolution): constrain inheritance/import reference target kinds (#1536, #1537) (#1796)
* fix(resolution): gate extends/implements to real supertypes

An inheritance reference bound to whatever local symbol shared its name.
The name-matcher scores node kind as a bonus, never a filter, and awards
no bonus at all for inheritance refs, so `use std::error::Error;` +
`impl Error for MapperError {}` resolved to the local `MapperError::Error`
VARIANT — an implementation relationship absent from the source.

Two changes, both needed. Filtering by kind alone was measured and it
only RELOCATES the false edge: with enum members excluded, the same 7
refs moved onto an unrelated local `type Error` alias, which is a legal
supertype kind and therefore harder for a consumer to reject.

1. Eligibility before ranking. `matchByExactName` restricts its candidate
   pool to kinds that can BE a supertype, so a legitimate trait outranks
   a same-named variant instead of merely losing its edge. `resolveOne`
   is wrapped by a gate that applies the same set to every other strategy
   at one seam — filtering inside the name-matcher would have missed the
   framework, import, chain and CFML paths.
2. Locality. A name imported from outside the repository has no in-repo
   referent at all, so no candidate is correct. Only oracles that cannot
   be wrong are consulted: Rust `use` paths rooted at a stdlib crate, and
   `isExternalImport` for ES modules. Generalizing the Rust side to "the
   module path doesn't resolve to a file" was tried and reverted — a
   crate re-exporting a sibling's modules (`pub use pupil_core::ports;`)
   has no directory to walk, and that version deleted 13 real trait
   implementations.

Measured on a Rust/Tauri project (2,682 nodes): the 11 false inheritance
edges are gone, all 59 real trait relationships are preserved, and node
count is unchanged. On this repository as a control, the only edge
removed is a class recorded as extending a function. Synthesized-edge
counts are identical in both.

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

* fix(resolution): an import never resolves to a member of a type

`import * as path from 'node:path'` is unresolvable — the module is
external — so the name-matcher fell back to finding any node called
`path`, and a common word like path/url/join/get matches a class property
or interface method somewhere in almost any repo. Nothing in any
supported language lets an import bind to a member that only exists
inside a type; you import the type.

Same shape as the inheritance gate that precedes it: eligibility applied
to the candidate pool before ranking, plus the resolveOne gate as the
backstop for every other strategy.

On this repository as a control: 19 imports pointing at methods and 4 at
properties are gone (all of them coincidences — `Walker::join`,
`Telemetry::events`), 3 refs now find the module constant they actually
name, node count unchanged.

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

* fix(resolution): classify SFC script imports as ES module specifiers

`isExternalImport` had a TS/JS branch listing typescript/tsx/javascript/jsx/
arkts, so for Svelte, Vue and Astro it fell through every branch and returned
false — "not external" — for `import { Foo } from 'some-npm-pkg'`.

An SFC imports inside its `<script>` block (Astro: the `---` frontmatter) with
ordinary ES module syntax; `extractImportMappings` already routes all three
through the same `extractJSImports`. So the classifier disagreed with the
extractor about what those imports are.

Effect on the preceding commit: its locality check asks `isExternalImport`, so
it silently did nothing for SFCs. A class in a `.svelte`/`.vue`/`.astro` file
implementing a type imported from an npm package still bound to whatever local
class shared that name — verified against this branch before the fix, all three
languages.

The language set is now one constant used by both the classifier and the
locality check, so they cannot drift apart again. Relative and aliased
specifiers are unaffected: the branch returns "not external" for `./…`,
workspace members, tsconfig alias prefixes, `@/`, `~/` and `src/` exactly as it
does for `.ts`.

No edge changes on this repository as a control.

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

---------

Co-authored-by: ctype_lab <cksgud1226@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:49:10 -05:00
Colby MchenryandColby McHenry 040ba388da fix(php): resolve static calls through import aliases (#1545) (#1795)
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 15:11:20 -05:00
7be699cd91 fix(resolution): resolve Python aliased module imports (#1626) (#1785)
* fix(resolution): resolve Python module members through an aliased from-import (#1626)

resolvePythonModuleMember rebuilt the submodule's dotted path by joining the
import source with the LOCAL name. Under 'from pkg import mod as alias' that
produces 'pkg.alias' — a module that does not exist — so the file lookup found
nothing and the call fell through to unresolved_refs with status='failed'.
codegraph_callers then reported the target as having fewer callers than it
does, which is the same wrong 'is this dead code?' answer #578 produced for
the unaliased form.

Join with the exported name instead. For an unaliased import the two names are
identical, so nothing changes there; '*' (the namespace form) keeps using the
local name, which is what it already bound to.

Scope note: the issue also reports 'import top as alias' failing. That form is
a namespace import and binds at source, so it resolves on current main — a
probe against the reverted resolver confirms it already produces its call edge.
The regression test pins both halves so the working one cannot silently break.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit f7a8940e67)

* fix(resolution): restore aliased Python module import edges (#1626)

Use the exported module name in the file-import resolver, matching the member resolver from upstream PR #1635. Keep both aliased call assertions and verify the file-to-file imports edge in the #1626 regression test. Update the Unreleased note to cover file dependencies.

Validation on Node 22.19.0: npm run build; supplied cg1626 repro; vitest run __tests__/resolution.test.ts -t 1626. Pass evidence saved in /workspace/cg1626-PASS.json and /workspace/cg1626-VERIFY.json.

---------

Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
2026-09-08 12:55:13 -05:00
Colby Mchenry 28033f62f8 fix(resolution): an import naming the emitted .js extension resolves to its .ts source (#1767)
Fixes #1705. Lands #1706 (thanks @bompus), rebased onto main.
2026-09-08 08:00:52 -05:00
Colby McHenry 873f133c96 feat(expo-router): add Expo Router support for Screens and navigations and introduce Steps API
Introduce Expo Router integration with a new Screens view and API to surface screens and transitions, plus a new Steps API and UI to depict typed steps from anchors or symbols. Extend codegraph’s extraction and resolution to handle namespace objects (export default NAME, two-statement forms, and default bindings) and React hook bindings for handlers, improving accuracy of flows across JS ↔ native boundaries. Add Swift/React Native bridge receiver evidence (RCT_EXTERN_MODULE, RCT_EXTERN_METHOD) and related resolution logic, with tests covering namespace-object resolution, useCallback-driven handlers, and inline RN event listeners. Update UI to include a Steps tab and associated components (StepsView, StepNode, ScreenEdge) and wire navigation to expose steps-based exploration via /api/steps and UI routes. Documentation and changelog reflect the new Expo Router integration and steps surface capabilities.
2026-08-28 09:46:50 -05:00
Colby Mchenry 278a8edc35 fix(resolution): resolve calls to object-literal namespace members (#1573) (#1597)
Fixes #1573. Thanks @IAliceBobI — the report had the root cause exactly right, and the fix sits one layer up from the suggested spot (resolution rather than the container-kind set), for the reason below.

## What was wrong

Methods of an exported object-literal constant — `export const api = { call() {…}, get: () => {…} }` used as a module's API surface — never received a call edge from `api.call()`, same-file or through an import. The members are extracted as plain functions with **bare** qualified names (`call`, not `api::call`) sitting inside the constant's source extent, so:

- the `Container::member` lookup the class-shaped kinds use (#825) bails on kind `constant`, and even with `constant` added to that set there is no `api::call` to find;
- the declared-type inference for imported singleton instances (#1292) finds no type in a literal and falls back to the constant edge;
- the same-file strategies only consider classes and `method` kinds, so the call resolved to nothing at all.

Net effect: `callers` / impact reported zero for methods called from everywhere, with no boundary warning because nothing about `obj.method()` looks dynamic.

## What this does

Adds one helper that resolves a member **by containment** — a node named `member` whose source range lies inside the value's range, in the value's own file — and uses it from both halves:

- **Import path**: when the imported value is a constant/variable, the literal member is tried right after the `Container::member` lookup and before the #1292 instance inference, so the cross-file edge lands on the method instead of the constant.
- **Same-file path**: a same-file constant/variable receiver (TS/JS family only) is checked before the class-name strategies.

Precision rules, all tested: calls accept callable kinds only; a declaration nested inside another member's body is not a member; nothing outside the value's range can donate a match — a same-named top-level function, or a method returned by a factory the value merely holds — so those cases keep today's behavior rather than guessing. Class statics (`C.s()`) and non-literal values are untouched.

Extraction and qualified names are deliberately left alone: changing how literal members are named would have to be mirrored in the native kernel byte-for-byte, and the resolver-side lookup is contained and language-gated.

## Tests

- The issue's repro end-to-end: `sameFileCallers` and `crossFileCaller` are both callers of `m`; a decoy `m` in a third file gets none; the `C.s()` static control resolves exactly as before; `crossFileCaller` no longer has a `calls` edge to the constant.
- Arrow-property and method members both resolve; a `function call()` nested inside `get`'s body is never taken for `api.call()`.
- A value holding a factory's result (`const obj = makeObj()`) with a same-named top-level `m` in the file: no false attribution, existing behavior kept.
- The two positive tests fail on `main`; the control passes both ways, as a guard should.
- Full suite: 189 files, 3181 passed / 9 skipped.

With the built CLI on the issue's `a.ts`/`b.ts`: `codegraph callers m` → 2 callers (`sameFileCallers`, `crossFileCaller`); `callers s` unchanged; edges `sameFileCallers -> m` (0.85) and `crossFileCaller -> m` (import, 0.9), none to `obj`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
2026-08-26 10:38:13 -05:00
Max Hsu a74029105a fix(resolution): resolve ES imports targeting .xsjs/.xsjslib files (#556) (#594)
The extraction half of #556 — indexing `.xsjs` / `.xsjslib` as JavaScript — already
landed on main via #654. This PR is now scoped to the remaining resolution gap:
the JS import-resolution list did not include the SAP HANA extensions, so an
extensionless `import { x } from './helpers'` in a `.xsjs` file resolved to
nothing and the cross-file call edge was dropped.

Add `.xsjs` / `.xsjslib` to the `javascript` entry in EXTENSION_RESOLUTION so
those imports resolve to their target file and `codegraph_callers` /
`codegraph_impact` see the edge. One resolution test covers the .xsjs -> .xsjslib
import; the now-redundant extraction/detection tests were dropped (covered by #654).
2026-08-22 11:53:36 -05:00
ctype_lab e2195940fb fix(union): complete downstream container handling 2026-08-06 17:50:25 +09:00
ctype_lab 8e3cde6994 feat(kernel): preserve union nodes across extraction 2026-08-06 17:11:26 +09:00
Colby MchenryandClaude Fable 5 abb0a916f7 perf(resolution): per-context basename index for Lua/Luau require resolution — kong fresh index −16% (#1391)
The full-README competitor matrix ranked lua/kong as the largest
legitimate fresh-index gap (2.71×). Stage attribution (RESOLVE_PROFILE=2)
pinned it: resolveLuaRequire ran getAllFiles().filter(endsWith) FOUR
times per require ref — ~7.5k string suffix scans each, measured at
~0.9ms/ref, hit or miss (2.7s combined over kong's 3k requires).

Replace the per-ref full-list scans with a per-context basename →
file-paths index (the cobolCopybookIndexes pattern). Buckets preserve
getAllFiles() iteration order, so each suffix's candidate filter yields
exactly the array the full scan produced — identical matches, identical
stable sort, identical winner, dump-proven.

kong: 4.07-4.27 → 3.40-3.63s (−16%). Gates: kong old-vs-new dump
byte-identical (157,650 rows), kong pooled-vs-sequential identical,
Fusion (luau instance-path requires) old-vs-new identical; suite
2,689 ×2 with CODEGRAPH_KERNEL_EXPECT=1.

Also registers cobolCopybookIndexes in clearImportResolverMemos — it was
never dropped on cache clears, so post-sync copybook lookups could serve
a stale file list; cache-drop is the always-safe direction.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 00:02:02 -05:00
Colby MchenryandClaude Fable 5 2ec877b08c fix(resolution): calls through an imported singleton resolve to the method (#1315)
reproStore.notifyJoinGuildStatus() after `import { reproStore }`
resolved its calls edge to the exported CONSTANT (resolvedBy:'import'),
while the identical same-file call resolved to the method via
local-variable receiver inference (#1108) — so `callers <method>`
missed every cross-file use and a widely-used method could look
unused (#1292).

resolveViaImport's member-descend now handles imported VALUES alongside
the #825 static-member case: when the base resolves to a
constant/variable, the value's type is inferred from ITS OWN
declaration lines in the exporting file (the shared #1108 pattern
table: `= new T(...)` initializers and type annotations) and the member
is resolved AND VALIDATED on that type via resolveMethodOnType. A
failed inference or validation keeps the existing constant edge —
never a fabricated one. Calls only; plain member reads still reference
the value.

excalidraw control: byte-identical graph (10,653 nodes / 19,483 calls
edges before and after).

Fixes #1292

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:36:33 -05:00
Colby MchenryandClaude Fable 5 5736e24bb6 perf(index): faster fresh indexing + parallel reference resolution, byte-identical graphs (#1305)
* perf(index): ~34% faster fresh indexing, byte-identical graphs

Profiling a fresh init on a medium TS repo (excalidraw, 657 files) showed
the main thread as the critical path: per-row SQLite statement calls,
repeated import-resolution walks, and per-row FTS trigger firings, with
the parse workers ~75% idle behind it. This lands the semantics-preserving
tranche of fixes:

- Multi-row batched INSERTs (nodes/edges/unresolved refs/name segments)
  behind cached per-batch-size prepared statements; row order preserved,
  so rowid-based resolution determinism (#1015) is unchanged.
- storeFileBundle: one transaction per file instead of four; nested
  transaction() calls now flatten (BEGIN-in-BEGIN previously threw, so no
  caller depended on nested rollback).
- Dedicated store-writer thread for the fresh-DB bulk path (bundles
  applied in file order on a single writer connection; main thread does
  no DB work during the parse loop). Kill switch: CODEGRAPH_NO_STORE_WORKER=1.
- Bulk FTS mode: drop the nodes_fts sync triggers during the bulk load,
  rebuild once at the end; crash inside the window self-heals on the
  next open.
- Per-context memos for resolveImportPath/findExportedSymbol + a per-file
  exported-symbol index, invalidated exactly where clearCaches() already
  resets the resolver's own caches.
- Fast-init on completely fresh DBs (journal in memory, no fsync until
  the index completes; interrupted init re-runs from scratch). Kill
  switch: CODEGRAPH_NO_FAST_INIT=1.
- MaybeYield returns undefined on the not-due path so per-ref yield
  checks stop paying a promise + microtask hop each.
- Parse pool prewarm for bulk indexing; compile-cache enabled at CLI and
  worker entry points.

Excalidraw fresh init: 5.11s -> 3.36s median (n=5, warm cache, M-series).
Graph dumps byte-identical across init, re-index, and sync paths; full
suite green (2403 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(resolution): parallel reference resolution with canonical admission

Fan resolution batches across a pool of read-only worker threads, each
hosting a full ReferenceResolver over its own SQLite connection; results
are admitted on the main thread in chunk order, so edge insertion order,
row cleanup, failure parking, and deferred post-pass queues are exactly
the sequence the single-threaded loop produces. Per-ref inputs match the
baseline because the sequential path already resolves each batch against
the state committed BEFORE that batch.

Validated byte-identical on excalidraw (pool forced on) and apache/dubbo
(4,048 Java files): dubbo full index 39s -> 19s (2.05x) with identical
graph dumps (91,495 nodes / 223,953 edges).

The pool only engages when total pending refs clear a threshold (default
150k, CODEGRAPH_PARALLEL_RESOLVE_MIN to tune, CODEGRAPH_NO_PARALLEL_RESOLVE=1
to disable): measured on a ~58k-ref repo the workers' boot CPU contends
with resolution on the same cores and makes indexing slower, so small
repos keep the sequential path. When fast-init left the DB in
memory-journal mode, WAL is restored before resolution only when the pool
will run (readers + rollback-journal writers don't mix).

Also: sqlite adapter readOnly open support.

TreeCursor spine rewrite of the body walker was built, measured neutral
on real repos and equal in a 20k-child microbench (web-tree-sitter's
namedChild(i) is not quadratic in this binding), and rejected — per-node
JS<->WASM marshaling is the floor, which a traversal swap cannot remove.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:21:15 -05:00
7f325134e0 feat(extraction): add Nix language support with module-system option wiring (#324, #332 via #648 — carries #1084) (#1190)
Carries @TyceHerrman's #1084 as the functional base. Extraction + file wiring (imports/modules lists, callPackage), module-system option-path synthesizer, lexical-scope resolution gates, ABI-15 wasm rebuilt from upstream source. Validated on agenix, nix-darwin, home-manager, and nixpkgs (44,368 files, 3m49s, 1.30M nodes).

Co-authored-by: Tyce Herrman <Tyce.Herrman@pm.me>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:41:32 -05:00
Colby MchenryandClaude Fable 5 99152212a9 feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)
Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language:
full TypeScript-grade extraction via the harmony-contrib tree-sitter
grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0
npm tarball), plus the ArkUI constructs that make HarmonyOS apps
traceable:

- @Component/@ComponentV2 structs with decorators from both grammar
  positions; members extract as class members with qualified names.
- build() component trees: child instantiation edges via
  arkui_component_expression, no synthesizer needed.
- Attribute chains emitted dot-prefixed and resolved ONLY against
  @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) —
  bare-name fallthrough produced 36,840 wrong edges (17% of calls) on
  the OpenHarmony samples monorepo. All four grammar chain shapes
  handled, including the detached-chain forms.
- .onClick(this.handler) method-reference bindings.
- ohpm workspace modules: bare imports follow oh-package.json5 file:
  deps (ambiguous names dropped), honoring each module's main entry —
  which also lets .ts consumers resolve .ets modules.
- ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with
  wiring-site metadata: assignment-gated state->build() re-render
  (V1 @State family + V2 @Local/@Provider/@Consumer),
  @ohos.events.emitter emit->subscriber pairing on static event keys
  (numeric ids same-file, named constants same-module, fan-out capped),
  and router.pushUrl literal urls -> the target page's @Entry struct.
- $r/$rawfile resource intrinsics treated as built-ins; arkts joins the
  web language family, value-reference edges, re-export chase, and the
  other TS-applicable gates.

Also ships a language-agnostic index-completeness guard: indexAll
stamps index_state (indexing -> complete/partial/failed), reconciles
discovered vs accounted files (a loaded run silently dropped 37 files),
and codegraph status surfaces truncated/partial indexes in human and
--json output.

Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular
ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693
files, 202,890 nodes stable across re-index, attribute false-positive
audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and
#988 with credit — both informed this implementation.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:07:15 -05:00
Colby MchenryandClaude Fable 5 41620c60fa feat(extraction): add COBOL language support (.cbl/.cob/.cpy) (#590, #648) (#1161)
Programs, sections/paragraphs (reconstructed extents over the grammar's
flat header stream), PERFORM/THRU/GO TO/CALL call edges, COPY copybook
imports incl. standalone .cpy fragments, DATA DIVISION records/fields/
88-levels with write-site impact references, and CICS flows: EXEC
LINK/XCTL program targets (literal + same-file VALUE deref), EXEC SQL
INCLUDE, and pseudo-conversational RETURN/START TRANSID hops resolved
to the owning program via a CICS framework resolver. Fixed and free
source format (free format via a scanner wide-mode sentinel).

Grammar: vendored wasm built from a patched yutaro-sakamoto/
tree-sitter-cobol (EXEC blocks as an external-scanner token, copybook
fragment entry point, single-quote continuation, COPY REPLACING
pseudo-text, NOT=, CALL GIVING, ENTRY, FREE, bitwise ops, abbreviated
relations, COBOL-2002 usages, and more). Patch + provenance + upstream
PR draft in docs/grammars/. Parse health: AWS CardDemo 43/44 native
(upstream: 9/31), 44/44 through preParse; copybooks 28/29; CobolCraft
free-format 17/17 (upstream: 0); NIST COBOL85 unchanged at 373/382.

Copybook members resolve to files like C includes (basename index,
name-matcher short-circuit so compiler-supplied members stay honestly
unresolved): CardDemo imports 5 -> 285. Impact proof: ACCT-CURR-BAL
(CVACT01Y copybook) surfaces its 4 writer programs cross-file.

Also: run-all.sh now neutralizes the ambient prompt-hook in both A/B
arms (CODEGRAPH_NO_PROMPT_HOOK=1); COBOL corpus entries for agent-eval.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:17:53 -05:00
Colby MchenryandClaude Opus 4.8 f7441f2124 fix(resolution,cli): cross-file static method calls + affected path normalization (#825) (#865)
Cross-file `ClassName.staticMethod()` calls resolved to the class, not the
method: the import resolver matched the receiver `Foo` to the named class
import but dropped the `.bar` member, and createEdges then mis-promoted the
`calls` edge to `instantiates`. So callers/impact for the static method came
back empty. Descend from the resolved class into its `Container::member` so the
call links to the method; fall back to the class when no such member exists
(non-`::` languages and genuine class references are unaffected).

Also normalize `codegraph affected` inputs to the project-relative,
forward-slash form the index stores, so `./src/x.ts`, an absolute path, and a
Windows back-slash path all match (previously silently returned 0).

Validated on luxon (24 files): node/edge totals identical (no explosion), 69
mis-promoted `instantiates` edges become `calls`, and real static factories
(DateTime.fromISO, etc.) resolve their callers. Full suite: 1534 passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:52 -05:00
Colby MchenryandClaude Opus 4.8 823ffd1c3d feat(extraction+resolution): Astro support — frontmatter/template extraction + src/pages routes (#768) (#815)
.astro files were not indexed at all, leaving a typical Astro site mostly
invisible to search/impact/explore. New AstroExtractor (Svelte/Vue SFC
pattern): component node per file, TS frontmatter + <script> blocks
delegated to the TypeScript extractor, template {fn(...)} calls (incl. the
multiline `{posts.map((post) => (` opening line), PascalCase component-tag
references. New astroResolver: Astro global + astro:* virtual modules as
framework-provided, component resolution with the #764 ambiguity rule,
src/pages/ file-based routes ([param]→:param, [...rest]→*rest, _-prefixed
and *.config.* excluded). SFC languages now preload the TS/JS grammars
their extractors delegate to (a pure-SFC file set previously had none
loaded). Also fixes a pre-existing Svelte/Vue script-block off-by-one that
reported every script symbol one line low.

Validated per the playbook: stalux (the issue's repro) 54/54 .astro files
indexed, getIconNode found at its exact line, 14/14 routes, 93.0% fair
cross-file coverage; AstroPaper 27/27 components, 13/13 routes (underscore
dirs correctly excluded), explore connects page→Card→Datetime through the
jsx-render synthesizer; node/edge counts stable across re-syncs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:50:11 -05:00