mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
14 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ab077b4c29
|
feat(ingestion): TypeScript registry-primary scope resolution (Ring 3) (#1050)
* feat(ingestion): TypeScript registry-primary scope resolution (Ring 3) - Add TypeScript ScopeResolver stack (query/captures/interpret, import decomposition, hooks, arity, merge, receiver binding) and register in SCOPE_RESOLVERS. - Harden shared compound receiver and receiver-bound CALLS pass for map for-of tuple bindings, dotted typeRef shapes, and callable-alias fallbacks. - Flip TypeScript into MIGRATED_LANGUAGES; refresh AGENTS.md and type-resolution-system.md. - Shared finalize-algorithm updates for cross-file scope parity. - Tests: TS scope-resolution unit suite; legacy call-processor suite forces REGISTRY_PRIMARY_TYPESCRIPT=0; registry-primary flag test opts out TS in override scenario. Made-with: Cursor * fix(ingestion): SCC-ordered cross-file return-type propagation + multi-hop re-export resolution Fix CI failures on PR #1050 (TypeScript registry-primary migration) by making `propagateImportedReturnTypes` deterministic via reverse- topological SCC ordering and updating the multi-hop re-export contract to match `followReexportChain` behavior. Why: the legacy pass mirrored an intermediate ref instead of the terminal type when an importer was processed before its source module had its own typeBindings chain-followed (4-file alias chain regression in `ts-simple` fixture: `models.User -> service.user -> app.user` collapsed to `getUser` instead of `User`). Reverse-topological walk of `indexes.sccs` (leaves first) lets every importer see the source's already-followed terminal type in a single pass. Changes: - `imported-return-types.ts`: rewrite to walk SCCs leaves-first, chain- follow the source module's typeBindings BEFORE mirroring, and chain- follow the importer's typeBindings AFTER mirroring. Cyclic SCCs reach a partial fixpoint (no convergence guarantee, ts-circular only asserts no-throw). - `finalize-algorithm.ts`: docstring update on `FinalizeFile.localDefs` to reflect that `followReexportChain` resolves multi-hop re-exports through barrels even when intermediates do not surface the name - surfacing is now a static optimization, not a correctness requirement. - `contract/scope-resolver.ts` Invariant I3: explicitly document the SCC ordering requirement. - `pipeline/run.ts`: split PROF timer into `finalize` and `propagate` so the pass's cost is observable independently. - `ARCHITECTURE.md` Performance notes: describe SCC-ordered propagation. - `imported-return-types.ts`: expand chain-depth comment (2x effective depth from pre/post follow), add multi-ref break rationale, add `ts-simple` motivating-fixture pointer. Tests: - `finalize-algorithm.test.ts`: add 4 cases (3-hop chain, cyclic re-export visited-set guard, wildcard re-export fall-through, multi-source first-match-wins); fix misleading shared nodeId in the thick variant; rename and update the multi-hop test for the new contract (transitiveVia assertion on the thin variant). - `imported-return-types.test.ts` (NEW): unit tests for the SCC pass pinning topological collapse, local-annotation guard, missing-source skip, and cyclic-SCC no-throw. - `cross-file-binding.test.ts` + `ts-deep-alias-chain` fixture (NEW): 5-file integration regression guard for SCC-ordered propagation through 4 module boundaries. Validation: 865 scope-resolution + cross-file tests pass on Windows; typecheck clean across both packages; only pre-existing Swift overload failures remain (verified on PR base commit, environmental). Made-with: Cursor * fix(ingestion): address PR #1050 review findings — side-effect imports, resolve-cache perf, adapter signature Three independent fixes surfaced by the production-readiness review of the TypeScript registry-primary scope-resolution migration (RFC #909 Ring 3). All three pass under both REGISTRY_PRIMARY_TYPESCRIPT=0 and =1. 1. Side-effect imports were silently dropped (correctness regression). The legacy DAG emitted IMPORTS edges for `import './polyfill'` because its tree-sitter query matches `(import_statement source: (string))` regardless of clause. The new registry-primary path returned `[]` from `splitImportStatement()` for clause-less imports, so no ParsedImport / ImportEdge was ever produced — silent file-level edge loss. Add a generic 'side-effect' variant to `ParsedImport` and `ImportEdge['kind']` in `gitnexus-shared`; finalize resolves the target file and pre-finalizes the edge (no `targetDefId`, no `BindingRef`) so the SCC fixpoint loop skips it. The TypeScript provider now emits + interprets the new kind end-to-end. The variant is intentionally generic so other languages (Rust `use foo as _`, Python module-init) can adopt it. 2. Per-import re-derivation in `resolveImportTarget` (perf regression). The TS adapter built `new Set(allFilePaths)` on every call and let `resolveTsImportTarget` re-derive `allFileList` / `normalizedFileList` and discard the `resolveCache`. For a workspace with N files and M imports that's O(N × M) work per pass. Wrap the adapter in a closure that memoizes all five derived values keyed on the orchestrator's `ReadonlySet` identity; reset only when the set reference changes (start of new pass). New cost: O(N + M). 3. Misleading fake `ParsedImport` in the adapter (architecture). The adapter constructed `{ kind: 'named', localName: '_', importedName: '_', targetRaw }` to call `resolveTsImportTarget`, even though only `targetRaw` and the structural-typed context are read. Extract `resolveTsTarget(targetRaw, ctx)` so the adapter has an honest signature; `resolveTsImportTarget` still works for other callers. Also extract `narrowTsContext` for the type narrowing. Tests: - New 4-file fixture `typescript-side-effect-imports` with two side-effect imports + one named import. - New "TypeScript side-effect imports" describe in `test/integration/resolvers/typescript.test.ts` (parity-gated by `ci-scope-parity.yml` — runs under both flag states). - Updated 2 unit tests to expect 1 side-effect ParsedImport and 4 `@import.statement` matches (was 0 / 3). - 785 / 785 TS scope-resolution tests pass under both REGISTRY_PRIMARY_TYPESCRIPT=0 and =1. Made-with: Cursor * fix(scope): address Codex adversarial review findings on PR #1050 Four findings from the Codex adversarial review broke registry-primary TypeScript resolution for common patterns. All four now have unit and integration regression coverage that pass under both `REGISTRY_PRIMARY_TYPESCRIPT=0` (legacy DAG) and the default registry-primary path. [high] tsconfig path aliases dropped: Threaded `tsconfigPaths` through ScopeResolver via a new opaque `resolutionConfig` parameter and a `loadResolutionConfig(repoPath)` hook. The orchestrator (`scopeResolutionPhase` + `runScopeResolution`) loads it once per workspace pass and forwards into every `resolveImportTarget` call. TypeScript resolver now resolves `@/services/user` style imports through the standard resolver's alias branch. [high] TSX parsed with the wrong grammar: `emitTsScopeCaptures` now picks the parser/query by `filePath` (`.tsx` -> TSX grammar) and validates cached trees against the expected grammar via the new exported `tsCachedTreeMatchesGrammar` helper. Stale TS-grammar trees for `.tsx` files no longer leak through the scope query. [medium] Literal dynamic imports never linked: Added `kind: 'dynamic-resolved'` to `ParsedImport` and `ImportEdge`. The decomposer emits a synthetic `@import.literal` capture for string-literal dynamic imports; the interpreter maps that to `dynamic-resolved`; finalize pre-finalizes it as a file-level terminal (same shape as `side-effect`). `import('./feature')` now produces a real IMPORTS edge under the registry-primary path. Legacy DAG keeps its existing behavior — the new integration assertion is gated behind the flag. [medium] Namespace re-exports invisible from barrels: The decomposer now emits TWO captures for `export * as ns from './m'` — the existing `reexport-namespace` import draft AND a synthetic `@declaration.namespace` capture (via `buildNamespaceDeclarationMatch`). The latter creates a Namespace `SymbolDefinition` in the barrel's `localDefs`, so downstream `import { ns } from './barrel'` resolves through `findExportByName`. Regression fixtures under `gitnexus/test/fixtures/lang-resolution/`: - typescript-tsconfig-aliases (`@/` alias) - typescript-tsx-jsx (Button.tsx + App.tsx with JSX) - typescript-dynamic-import (`await import('./feature')`) - typescript-reexport-namespace (`export * as Models from './base'`) Validation: - gitnexus-shared builds clean - gitnexus typecheck clean - 385/385 TS scope-resolution tests pass under both `REGISTRY_PRIMARY_TYPESCRIPT=0` and default Made-with: Cursor * perf(scope): O(1) defById lookup + bounded re-export depth (PR #1050 round 3) Addresses the round-3 PR #1050 reviews (Claude adversarial + xkonjin): both flagged the existing O(N²) `findDefById` linear scan in `materializeBindings` and the unbounded recursion in `followReexportChain` as production-readiness blockers for TypeScript monorepos. Both fixes land alongside their regression tests under both `REGISTRY_PRIMARY_TYPESCRIPT=0` and the default registry-primary path. [high] materializeBindings O(N_files × N_defs × N_edges) → O(N_defs + N_edges): Build a `nodeId → SymbolDefinition` index map once at the top of `materializeBindings` (one O(N_defs) pass), then replace the per-edge `findDefById(files, edge.targetDefId)` linear scan with an O(1) `defById.get(edge.targetDefId)` lookup. Also drop the now-unused `findDefById` helper. At realistic TypeScript monorepo scale (~5k files × ~50 defs/file × ~100k linked import edges) this is the difference between ~25 s and a few ms inside finalize. Regression test in `finalize-algorithm.test.ts` builds 200 leaf files + 1 consumer importing one symbol from each, asserts every binding materializes correctly. [medium] followReexportChain unbounded recursion: The existing `visited` set caps depth at `O(N_files)` but allows recursion proportional to barrel-chain depth, mismatching the explicit "Iterative DFS to avoid stack overflow" policy in `tarjanSccs`. Added a `MAX_REEXPORT_DEPTH = 100` constant and a `depth` parameter to `followReexportChain` (defaults to 0); each recursive call passes `depth + 1` and the function returns `null` when the cap is exceeded. 100 is comfortably above any realistic hand-authored barrel chain (typical depth 1-5; auto-generated barrels rarely exceed 20) while staying well below JS engine call stack limits. Regression test wires a 200-link reexport chain and verifies the crawl terminates cleanly with `linkStatus: 'unresolved'` (no terminal def reachable within the budget). [low] synthesizeInstanceofNarrowings bare-identifier-only limitation: xkonjin's review #4 noted that the LHS narrowing only handles bare identifiers (`if (x instanceof Foo)`), not member expressions (`if (user.address instanceof Address)`). Added a JSDoc note explaining the constraint and pointing readers at field-type resolution as the workaround for member-chain receivers. Validation: - gitnexus-shared builds clean - gitnexus typecheck clean - 413/413 tests pass under both flag states for finalize-algorithm + TS unit + TS integration suites - 972/972 tests pass across full scope-resolution + Python + C# integration smoke (no cross-language regression) Made-with: Cursor * refactor(finalize): replace recursive followReexportChain with SCC-condensed iterative closure The legacy `followReexportChain` walked re-export drafts via mutual recursion guarded by a per-call visited set + a `MAX_REEXPORT_DEPTH` ceiling. Recursion is fragile (call-stack ceiling, no bound on depth that's actually meaningful), so this replaces it with a structurally better algorithm: a precomputed per-file re-export closure built by running Tarjan SCC over the re-export sub-graph and propagating names in reverse-topological order with a bounded intra-SCC fixpoint. Algorithm (`buildReexportClosures` in finalize-algorithm.ts): 1. Sub-graph: build the directed graph of `reexport` + `wildcard` drafts only (regular/namespace/dynamic imports do not contribute). 2. SCC condensation: run the same iterative `tarjanSccs` already used for the file-level import graph; output is in reverse-topo order so out-of-SCC neighbors are always already-finalized. 3. Per-SCC propagation: - Acyclic singleton: one pass populates from neighbors' closures. - Cyclic SCC: bounded fixpoint capped at |SCC|+1 iterations. With first-wins precedence the closure map is monotone, so each name needs at most |SCC| hops to traverse the cycle. Precedence (preserved from the recursive crawl): - Named re-exports take precedence over wildcards. - Within each kind, declaration order wins. Lookup at finalize time becomes O(1) (`lookupReexportedName`), down from O(chain_depth × drafts) per consult and recursive at that. Properties vs the legacy implementation: - Stack-safe by construction; no `MAX_REEXPORT_DEPTH` guard needed. - 1000-hop barrel chains now resolve in full (legacy capped at 100 and surfaced anything deeper as `unresolved`). - Cycles handled structurally via SCC, not via per-call visited set. - Same observable semantics: every existing test passes unchanged. Tests: - Replace the obsolete `MAX_REEXPORT_DEPTH (200-hop chain stops cleanly without stack overflow)` test (which asserted the OLD bug — that deep chains failed to resolve) with a positive 1000-hop test that asserts full resolution + accurate `transitiveVia`. Proves both the recursion is gone AND the closure correctly inherits the leaf def across all hops. - Update commentary on adjacent re-export tests to reference the closure mechanism. - Update `FinalizeFile.localDefs` JSDoc + import-decomposer.ts inline doc to point at `buildReexportClosures` instead of the removed function name. Validation: - gitnexus-shared builds cleanly. - gitnexus typechecks cleanly. - 28/28 finalize-algorithm.test.ts tests pass (incl. new 1000-hop). - 801/801 TypeScript scope-resolution tests pass under default (registry-primary) AND `REGISTRY_PRIMARY_TYPESCRIPT=0` (legacy DAG). - 404/404 Python + C# integration tests pass — no regression in cross-language consumers of the shared `finalize`. Made-with: Cursor * fix(scope): remove non-null assertions from scope resolution Made-with: Cursor * fix(scope): address TypeScript review follow-ups Made-with: Cursor * fix(scope): address TypeScript import review follow-ups Add regression coverage for non-binding import edges and circular TypeScript bindings so PR #1050 review concerns stay visible without changing runtime semantics. Made-with: Cursor |
||
|
|
ff4ae89aaa
|
feat(python): scope-based call resolution + registry-primary flip + perf + generalization (RFC #909 Ring 3) (#980)
* Initial plan
* plan: Python scope-based resolution migration
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* feat(python): scope-based resolution provider hooks + 62 tests
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/0eee6c69-fc17-4df5-9ac6-358ab41f5740
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* refactor(python): split scope-hooks monolith into focused modules
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(python): integration-style scope-resolution tests + suffixResolve fallback
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/db76e937-4b0e-4c4d-82b1-265a1fb3673d
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* wire python scope-based resolution end-to-end (initial pass)
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* keep legacy IMPORTS for python (heritage needs importMap), scope phase owns CALLS only
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c474dc66-5cf7-445d-8eb4-76501c5e6d67
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
* test(python): remove parallel scope-resolution integration test
The new test/integration/python-scope-resolution.test.ts duplicated coverage
the reviewer explicitly rejected. The existing
test/integration/resolvers/python.test.ts (191 tests, driven by
runPipelineFromRepo) is the source of truth for Ring 3 parity.
Also document the IMPORTS-emission follow-up gap: wiring emitImportEdges
in python-scope-emit.ts today regresses 10 IMPORTS-edge fixtures because
the scope-extractor's ImportEdge coverage is narrower than legacy
pythonImportConfig.importResolver. Tracked as a follow-up.
Baseline with REGISTRY_PRIMARY_PYTHON=1 is unchanged: 109/191 pass.
* feat(ingestion): scope-resolution phase owns Python IMPORTS edges (RFC #909 Ring 3)
When `REGISTRY_PRIMARY_PYTHON=1`, IMPORTS graph edges for Python files are now
emitted exclusively by the new scope-resolution path. The legacy
`import-processor` still runs — heritage resolution needs its importMap /
namedImportMap / moduleAliasMap population — but its graph edge emission is
gated per-language so Python no longer double-emits.
This closes the reviewer's second change request on PR #980: "the legacy path
must be turned off". Legacy IMPORTS edges for Python are now off by default
when the flag is enabled.
Three bugs were fixed to make the new path's coverage match legacy:
1. **Root-file bailout** (import-resolvers/python.ts): `resolvePythonImportInternal`
returned null immediately when the importer file lived at the repo root
(importerDir === ''). The ancestor directory walk further down already
handles this case correctly; the early return was the bug. Proximity check
now only runs when importerDir is non-empty, and the ancestor walk sees
root-level files for the first time.
2. **External dotted imports** (languages/python/import-target.ts): the new
path fell straight through to `suffixResolve` for multi-segment imports,
which happily matched `django.apps` to a local `accounts/apps.py`. Mirror
`pythonImportStrategy`'s `hasRepoCandidate` guard — suffix-match only when
the leading segment exists somewhere in-repo as a package, __init__.py,
or namespace directory.
3. **suffixResolve ambiguity** (languages/python/import-target.ts): the
shared `suffixResolve` helper requires a pre-built `SuffixIndex` to
disambiguate ties. Without one it falls back to an O(files) scan that
silently picks the first match when the last segment collides across
directories (e.g. `accounts.models` matching `billing/models.py`).
Replaced with `resolveAbsoluteFromFiles` — exact lookup first, then a
deterministic suffix match.
Validation:
- Flag OFF: 191/191 pass (no regression).
- Flag ON: 109/191 pass (82 fail — exact baseline match; remaining 82 are
unchanged CALLS-edge provider-feature gaps tracked as Phase B follow-ups).
- `tsc --noEmit`: clean.
The 82 CALLS failures cluster into 44 describe blocks covering type-inference
features (assignment chains, walrus, class-level annotations, constructor
inference, C3 MRO, overload dispatch, return-type inference) that need
dedicated Ring 3 follow-up work. Each cluster is tracked against the RFC #909
shadow-parity gate (>=99% fixtures / >=98% corpus) in the per-language ticket.
* ci(scope-resolution): automatic parity gate driven by MIGRATED_LANGUAGES
Adds the Ring 3 parity gate the RFC §6.4 requires: when a language's
scope-resolution migration is marked complete, CI runs its resolver
integration test twice on every PR (once with the legacy DAG, once with
the registry-primary path) and both must pass.
The "is this language migrated" signal is a single TypeScript constant:
// gitnexus/src/core/ingestion/registry-primary-flag.ts
export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> =
new Set([ /* SupportedLanguages.Python when ready */ ]);
Adding a language here has three simultaneous effects:
1. `isRegistryPrimary(lang)` defaults to true for that language in
production (env-var override still wins if set explicitly).
2. `.github/workflows/ci-scope-parity.yml` auto-discovers the set via
`npx tsx scripts/ci-list-migrated-languages.ts`, builds a parity
matrix, and runs:
- `REGISTRY_PRIMARY_<LANG>=0 npx vitest run resolvers/<slug>.test.ts`
- `REGISTRY_PRIMARY_<LANG>=1 npx vitest run resolvers/<slug>.test.ts`
Both legs must pass for the job to succeed.
3. Legacy-path gating in call-processor.ts / import-processor.ts kicks
in automatically through the same `isRegistryPrimary` lookup.
No JSON registry, no manual workflow edit, no second source of truth —
contributors update the Set and CI picks it up. Empty Set = parity job
is a skipped matrix (workflow still reports success).
The new `scope-parity` reusable workflow is added to ci.yml's `needs`
graph and ci-status gate. Its result must be `success` (skipped would
mean upstream discover job failed and should block).
Validation (with empty MIGRATED_LANGUAGES set):
- flag OFF: 191/191 pass (no behavior change)
- flag ON (manual REGISTRY_PRIMARY_PYTHON=1): 82 fails = baseline exact match
- `npx tsc --noEmit`: clean
- concurrency-convention script: pass
- tsx discovery script: emits `[]` correctly
* ci(scope-resolution): keep MIGRATED_LANGUAGES empty; fix linter auto-uncomment
Previous commit's example entry got auto-uncommented (linter preferred a
type-checkable `SupportedLanguages.Python` over a commented-out reference).
That would have triggered the parity CI gate against Python, which today
has 82 known flag-on failures — unintended and would block the PR.
Use the explicit generic `new Set<SupportedLanguages>([])` so an empty set
still type-checks without needing an uncommented-out sample member.
Example in the comment now has `// SupportedLanguages.Python,` so it
remains illustrative without participating in the set.
* feat(python): capture constructor-inferred + annotated type bindings
Extends the Python scope-extractor with two new type-binding capture
patterns so receiver-typed method dispatch has concrete type bindings
to work from:
1. `u: User = ...` / `u: User` — variable annotations. `@type-binding.annotation`
anchor, `source: 'annotation'`.
2. `u = User("alice")` — assignment RHS is a bare-identifier call (Python
has no `new` keyword; constructor-shaped calls are syntactically
identical to function calls). `@type-binding.constructor` anchor,
`source: 'constructor-inferred'`.
The runtime query lives in `query.ts` (the `.scm` file is documentation
per the comment at its top); both are updated.
Fixes 19 failures across these resolver fixtures (flag-on 82 → 63):
- Python constructor-inferred type resolution (3)
- Python class-level annotation resolution (3)
- Python nullable receiver resolution (3)
- Python member-call / receiver-constrained / constructor-call (3)
- Python assignment chain propagation (2)
- Python walrus / match-case / chained method (3)
- Python member access iterable for-loop (2)
* feat(python): strip nullable unions + prefer annotations over inference
Two linked changes that together fix the 4 nullable-receiver tests:
1. `stripNullable` in Python's `interpretTypeBinding` unwraps `User | None`,
`None | User`, and `Optional[User]` to `User`, so receiver-typed
resolution treats nullable receivers identically to non-nullable ones.
Three-arm unions (`User | Error | None`) are left unchanged — truly
ambiguous for single-receiver inference.
2. Source-strength ordering in `pass4CollectTypeBindings`. When multiple
matches fire for the same bound name in the same scope — e.g. the
`u: User = find()` idiom where both the annotation and
constructor-inferred patterns match — the explicit annotation now
wins regardless of query-match arrival order. Rank:
explicit (annotation / parameter-annotation / return-annotation / self) > inferred
Also reorders the two Python patterns in query.ts / scopes.scm so the
constructor-inferred pattern appears first — a belt-and-braces fallback
that keeps behavior deterministic if the shared priority ranking is ever
revisited.
Fixes 4 failures (flag-on 63 → 59):
- Python nullable receiver resolution (4 tests)
Flag-off regression check: 191/191 still pass.
* feat(python): walrus, qualified-call, match-case type bindings
Extends the constructor-inferred family of captures with three more
assignment-shaped patterns that all bind a variable to a class-like type:
- Walrus: `(u := User(...))` → `u: User` via `(named_expression)`.
- Qualified call RHS: `u = models.User(...)` → `u: models.User` via
`(attribute)` node .text. Falls through resolveTypeRef Phase 2
(QualifiedNameIndex dotted fallback).
- Match as-pattern: `case User() as u:` → `u: User` via `(as_pattern)`
+ `(class_pattern (dotted_name))`.
Fixes 2 failures (flag-on 59 → 57):
- Python walrus operator type inference
- Python match/case as-pattern type binding
Qualified-call constructor tests still fail because they require
cross-module qualifiedName registration (models.User → models.py's User
class) which isn't yet wired in the Python extractor. Tracked as
follow-up alongside module-import CALLS (#337) resolution.
* feat(python): chain type bindings + strip list[T] generic for for-loop
Adds two capture patterns and a shared transitive-closure pass that
together handle Python's variable-aliasing and for-loop-over-typed-
iterable patterns:
1. `(assignment left: (identifier) right: (identifier))` — `alias = u`.
2. `(for_statement left: (identifier) right: (identifier))` — `for u in users`.
Both emit `@type-binding.alias` with the RHS identifier as rawName. The
shared `pass4CollectTypeBindings` now runs a final transitive-closure
walk that follows identifier-chain TypeRefs through the declaring scope
and its ancestors (depth-capped, cycle-guarded) so `alias` ultimately
points at the class type instead of another local variable name.
Generic stripping in `interpret.ts` unwraps single-arg collection
wrappers — `list[User]`, `set[User]`, `Iterable[User]`, etc. — to the
element type. Multi-arg generics (`dict[str, User]`, `Callable[...]`)
are left alone; their semantics aren't unambiguous.
Fixes 8 failures (flag-on 57 → 49):
- Python assignment chain propagation (4)
- Python nullable + assignment chain (2)
- Python walrus operator (:=) assignment chain (2)
Flag-off still 191/191.
* feat(python): namespace & class receiver resolution + file-level caller fallback
Adds a Python-specific post-resolution pass `emitReceiverBoundCalls`
that closes two receiver gaps the shared `MethodRegistry.lookup` doesn't
cover:
1. **Namespace receivers** — `import models; models.User()` /
`import models as m; m.User()`. The shared `lookupReceiverType` only
walks `scope.typeBindings`; namespace imports never land there
(they're filtered out of `scope.bindings` when the target module
has no self-named def, per `finalize-algorithm.ts:540`). The new
pass walks `indexes.imports` directly, builds a per-file
`localName → targetFilePath` map, and emits CALLS/ACCESSES edges
against the target file's `localDefs`.
2. **Class-name receivers** — `Dog.classify("dog")`. The shared resolver
requires typeBindings; class bindings in `scope.bindings` are never
consulted as receivers. The new pass checks class-kind bindings in
the call scope's chain and resolves members via `ownerId`.
Also fixes module-level call attribution: `resolveCallerGraphId` now
falls back to the File node id (`generateId('File', filePath)`) when no
enclosing function/method/class is found. Matches legacy DAG behavior
for module-scope calls like `u = models.User()` at the top of app.py.
Fixes 4 failures (flag-on 49 → 45):
- Python module import CALLS resolution (Issue #337) (4 of 7)
Flag-off still 191/191.
* feat(python): dotted-typebinding receiver resolution
Adds case 3 to `emitReceiverBoundCalls`: when a receiver's typeBinding
has a dotted rawName like `u: models.User` (the constructor-inferred
form fired by `u = models.User(...)`), walk the namespace map + target
file's defs to find the class, then look up the member via ownerId.
`resolveTypeRef`'s QualifiedNameIndex fallback can't cover this because
the target class's qualifiedName in models.py is just `"User"`, not
`"models.User"` — the dotted form only exists in the call-site file's
receiver expression. This pass bridges that gap without modifying the
shared registry.
Fixes 9 more failures (flag-on 45 → 36):
- Python qualified constructor inference (2)
- Python module import CALLS resolution (Issue #337) (3)
- (cluster overlap — several downstream tests in assignment/nullable/
walrus that propagate through qualified-ctor bindings also benefit)
Flag-off still 191/191.
* feat(python): consult finalized bindings for receiver resolution
`findClassBindingInScope` now walks BOTH:
1. `scope.bindings` — pre-finalize local declarations (origin: 'local')
2. `indexes.bindings` — post-finalize cross-file imports/namespaces
Without (2) we were blind to any class brought in via
`from models import Dog` at the call site's file, because the
scope-extractor's Pass 2 only populates local bindings and the
cross-file finalize produces a separate bindings map that never lands
on `scope.bindings`.
Case 2 (`Dog.classify()`) now walks MRO so inherited static/class
methods resolve — `Dog.classify()` where `classify` lives on `Animal`.
Case 4 (simple typeBinding like `u: U` from aliased import) now uses
`findClassBindingInScope` instead of the shared `resolveTypeRef`,
because `resolveTypeRef`'s `ctx.scopes` only sees pre-finalize local
bindings too.
Fixes 4 more failures (flag-on 36 → 32):
- Python method enrichment > Dog.classify static (1)
- Python static/classmethod class-as-receiver (2)
- Python alias import resolution (1)
Flag-off still 191/191.
* refactor(python-scope): extract language-agnostic emit-core/
Unit 1 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).
Splits python-scope-emit.ts (~945 → 481 lines) by lifting 14 generic
graph-feeding primitives into emit-core/:
- graph-node-lookup, graph-id, emit-edge
- emit-references, emit-imports
- scope-walkers (findReceiverTypeBinding, findClassBindingInScope,
findOwnedMember, findExportedDef)
- namespace-targets, method-dispatch-bridge
Each file carries a "Next-consumer contract" JSDoc so future language
migrations (TS #927, JS #928, Java, Kotlin, Ruby) import from emit-core
rather than re-implementing. python-scope-emit.ts keeps only the four
Python-specific pieces: runPythonScopeResolution (orchestrator),
buildPythonMro, emitReceiverBoundCalls (4 cases), populateMethodOwnerIds
— these move to languages/python/emit/ in Unit 11.
Pure refactor, zero behavior change:
- flag-off: 191/191 python.test.ts pass (identical baseline).
- flag-on (REGISTRY_PRIMARY_PYTHON=1): 32 fail / 159 pass (identical
baseline — the refactor neither fixes nor regresses any test).
- tsc --noEmit clean.
* feat(python-scope): arity metadata + bind function decls in parent scope
Unit 2 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).
Two changes that the registry-primary path needs before any of the
arity-sensitive failures can move:
1. Arity metadata on scope-extracted Function/Method defs.
- New helper `languages/python/arity-metadata.ts` reuses
`pythonMethodConfig.extractParameters` so self/cls stripping,
defaults, and *args/**kwargs detection match legacy semantics.
- `emit-captures.ts` synthesizes
`@declaration.parameter-count` /
`@declaration.required-parameter-count` /
`@declaration.parameter-types` captures on every
`@declaration.function` match.
- Generic `scope-extractor.ts buildDefFromDeclarationMatch` reads
the three optional captures into `SymbolDefinition`. Absence is
still the no-op default for non-Python providers.
2. Hoist function/class declaration bindings to the enclosing scope.
The "innermost scope containing the anchor" default placed
`def greet(...)` inside greet's OWN body — invisible to other
module-level callers, so every flag-on free-call resolved to
`unresolved`. The hoist condition (`anchor range == innermost
range`) only fires for scope-creating declarations, so variable /
for-loop captures whose anchor is a child identifier stay put.
Hooks can still override via `bindingScopeFor`.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on (REGISTRY_PRIMARY_PYTHON=1): 31 fail / 160 pass
(was 32/159; the hoist unblocks free-call resolution end-to-end).
- tsc --noEmit clean.
Per-(source,target) edge collapse for multi-call-site cases
(default-params, variadic) still pending — landing it without
regressing the static-method find_user fixture (which expects two
distinct edges through different targets) needs the ownership-aware
qualified-id work that lands with Unit 4 / Unit 11.
* feat(python-scope): capture function return-type annotations
Unit 3 of the python migration architectural plan
(docs/plans/2026-04-19-001-refactor-python-migration-architectural-plan.md).
Wires the `def get_user() -> User` return-type annotation into the
typeBindings stream so the existing constructor-inferred + transitive
chain machinery can resolve `u = get_user(); u.save()` to `User#save`
without any orchestrator change.
Changes:
- `query.ts` + `scopes.scm`: new `@type-binding.return` pattern keyed by
the function name (matches RFC §5.1 canonical vocabulary).
- `interpret.ts`: maps `@type-binding.return` to the existing
`'return-annotation'` source label (no shared change needed).
- `scope-extractor.ts pass4CollectTypeBindings`: extends the Pass 2
auto-hoist (anchor range == innermost scope range → bind in parent)
to type bindings as well — return-type bindings whose anchor IS the
function_definition land in the function's enclosing scope so
callers see them.
Same-file return-type inference is now end-to-end:
`def get_user() -> User: ...` + `u = get_user()` produces
`u: User (return-annotation)` in the caller's scope via
`followChainedRef`.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 31 fail / 160 pass (no change — every remaining
return-type test in this fixture set is *cross-file*; carrying
`get_user → User` across module boundaries lands with the
cross-file typeBinding propagation work in Unit 5/7).
- tsc --noEmit clean.
* feat(python-scope): resolve dotted receivers via class-scope field types
Unit 4 partial — the dotted-receiver case (`user.address.save()`).
Class-body annotations like `class User: address: Address` already
land in the class scope's typeBindings via the existing
`@type-binding.annotation` capture. This commit consumes that signal:
- Build a `Map<classDefId, Scope>` from every parsed file's class
scopes once per resolution pass.
- New Case 0 in `emitReceiverBoundCalls`: when the receiver's name
contains a dot, walk the chain — resolve the head's type, then for
each remaining segment look up that field's type in the owner
class's scope.typeBindings, then emit the call against the final
class with MRO walk.
- Cross-scope lookups use each TypeRef's `declaredAtScope` so an
imported `Address` resolves in the file that owns the field
declaration, not the file holding the call site.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 29 fail / 162 pass (was 31/160; both `Field type
resolution` fixtures now pass — same-file and cross-file disambig).
- tsc --noEmit clean.
Remaining Unit 4 work (write ACCESSES, `self.X` for-loop iteration)
needs Unit 6's tuple/iterable destructuring before it can land —
`for u in self.users` requires the iterable typing path.
* feat(python-scope): chain receiver via call-expression return types
Unit 5 — extends the compound-receiver case to handle call-expression
receivers (`svc.get_user().save()`).
`resolveCompoundReceiverClass` is the single recursive entry point for
all compound receivers. Three shapes:
- bare identifier — typeBinding chain
- dotted `obj.field[.field]…` — class-scope field types
- call `expr.method()` — recurse into expr, look up method's
return-type typeBinding on its class scope
Method return-type bindings auto-hoist to the parent (class) scope per
Unit 3, so `methodClassScope.typeBindings.get(methodName)` is the
canonical lookup. Free-call return types (`get_user()`) walk the
caller's scope chain.
Depth-capped at 4 hops to bound recursion.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 28 fail / 163 pass (was 29/162; `Python chained method
call resolution` now passes).
- tsc --noEmit clean.
Two related tests (`city.save() via method chain`, `c.greet().save()
depth-2 MRO`) still fail because the captures yield typeBindings
shaped like `city → user.get_city` (no trailing parens — the capture
grabs the attribute text). Resolving those needs a follow step that
detects the call-shape rawName and feeds it through the compound
recurser. Lands with the chain-typeBinding work in a follow-up.
* feat(python-scope): free-call fallback consults finalized bindings
Unit 7 — closes the cross-file free-call gap.
The shared `MethodRegistry.lookup` walks `scope.bindings` (pre-finalize
local-only) for free-call resolution. Cross-file imports land in
`indexes.bindings` (post-finalize). Without the dual-source lookup,
`from x import f; f()` resolves to "unresolved" and no CALLS edge is
emitted.
Two changes:
- `emit-core/scope-walkers.ts`: new `findCallableBindingInScope` —
same dual-source pattern as `findClassBindingInScope`, but accepts
Function/Method/Constructor. Promoted to emit-core because every
language with cross-file imports needs the same lookup.
- `python-scope-emit.ts emitFreeCallFallback`: post-pass that walks
every free-call reference site, looks up the callee with the new
helper, and emits via `tryEmitEdge`. Pre-seeds `seen` from the
shared resolver's emissions so we never double-count.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 22 fail / 169 pass (was 28/163; +6 tests including
the Python overload dispatch fixtures, ancestor-directory imports,
and same-name module-alias collision).
- tsc --noEmit clean.
* feat(python-scope): super() receiver dispatches up the MRO
Unit 8 — `super().method()` inside a class method walks the enclosing
class's MRO chain (skipping self) and resolves to the first ancestor
that owns the method.
New receiver branch in `emitReceiverBoundCalls` recognizes
`super(...)` syntactically (regex-cheap), finds the enclosing class
via a new `findEnclosingClassDef` scope-walk helper, then re-uses
`scopes.methodDispatch.mroFor` + `findOwnedMember` from the existing
class-receiver path. Handled before the compound-receiver case so
`super()` doesn't fall into the bare-identifier branch where `super`
isn't a binding.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 21 fail / 170 pass (was 22/169; `super().save() inside
User to BaseModel.save` now passes).
- tsc --noEmit clean.
* feat(python-scope): suppress shared resolver on member-call sites
Unit 9 — `app_metrics.get_metrics()` (namespace import alias) was
emitting two CALLS edges: a wrong self-call from the shared
resolver's free-call fallback, plus the correct namespace-receiver
edge from the Python post-pass.
Mechanism:
- `emit-core/emit-references.ts`: new optional `skipSites` parameter
(`Set<string>` of `${filePath}:${line}:${col}` keys). When supplied,
references at those positions are skipped — the provider has
already emitted (or chosen not to emit) for that site.
- `python-scope-emit.ts`: reorders Phase 4 — receiver-bound + free-
call fallback run FIRST, populating `handledSites`. The shared
`emitReferencesViaLookup` then runs with that set so the resolver's
fallback can't fight a precise per-receiver emission. Site keys are
added only on successful tryEmitEdge (not for sites the post-pass
saw but couldn't resolve — those still get a chance from the shared
path).
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 20 fail / 171 pass (was 21/170; same-name module-alias
collision now resolves correctly).
- tsc --noEmit clean.
* feat(python-scope): propagate return-type bindings across imports
Closes the cross-file return-type propagation gap that left tests
like `u = get_user(); u.save()` (where get_user lives in another
file) with `u` typed as the function name instead of its return type.
The shared finalize pass copies callable bindings (`from x import f`
puts `f` in the importer's bindings) but typeBindings stay file-local
because they live on `Scope.typeBindings`, not on the index. Mutate
post-finalize:
- For each module-scope import binding (`origin: 'import'` or
`'reexport'`), look up the source file's module-scope typeBinding
for the def's simple name. If present (return-annotation source),
mirror it under the importer's local alias. Skip when the importer
already has its own typeBinding for the name (explicit local always
wins).
- After propagation, re-run a chain-follow on every scope's
typeBindings — pass-4 ran before propagation and missed any chain
whose terminal lived in a foreign file. Same algorithm as
`followChainedRef` in scope-extractor, but operates on the
finalized scopes so propagated entries are visible.
Mutating `Scope.typeBindings` is safe — `draftToScope` constructs a
plain `new Map(...)`, not a frozen one.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 16 fail / 175 pass (was 20/171; +4 — both cross-file
return-type tests, plus two related propagation cases).
- tsc --noEmit clean.
* feat(python-scope): for-loop call-iterable typeBinding
Adds `(for_statement left: (identifier) right: (call function:
(identifier)))` to the typeBinding capture set. Combined with Unit 3's
return-type capture and the cross-file return-type propagation pass,
this makes `for u in get_users(): u.save()` resolve to `User.save`
even when `get_users` is imported from another module.
Captured as `@type-binding.alias` (rawName = function identifier,
without parens) so the existing chain-follow walks the alias to the
function's return-type binding without any new code path.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 12 fail / 179 pass (was 16/175; +4 for-loop call-iterable
tests across get_users / get_repos fixtures).
- tsc --noEmit clean.
* feat(python-scope): collapse free-call edges per (caller, target)
Free calls (no explicit receiver) now emit a single CALLS edge per
(caller, target) pair regardless of how many call sites the caller
contains. Mirrors the legacy DAG's per-pair dedup contract — what
the `default-params`, `variadic`, and `overload` fixtures expect.
Member calls keep position-based dedup so distinct resolved targets
(e.g. UserService.find_user vs AdminService.find_user from the same
caller) still produce distinct edges.
Implementation: bypass `tryEmitEdge` (which dedupes positionally) and
hand-roll the relationship with a position-independent rel.id
(`rel:CALLS:<caller>-><target>`). Site handling is now unconditional —
even when the dedup-collapse skips the actual emit, we mark the site
handled so the shared `emit-references` doesn't fight us with its
fallback.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 10 fail / 181 pass (was 12/179; +2 — both `default
parameter arity` tests now pass).
- tsc --noEmit clean.
* fix(python-scope): match legacy CALLS reason for import-resolved free calls
The arity-narrowing test asserts \`rel.reason === 'import-resolved'\`
for cross-file free-call edges. Switch the free-call fallback's
reason to mirror legacy DAG semantics:
- target-file !== source-file → 'import-resolved'
- same file → 'local-call'
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 9 fail / 182 pass (was 10/181; +1 arity-narrowing test).
- tsc --noEmit clean.
* fix(python-scope): drop dead pre-seeding from receiver-bound pass
The pre-seeding loop at the top of \`emitReceiverBoundCalls\` populated
\`seen\` with every reference the shared resolver had already resolved.
That was useful when emit-references ran FIRST. After Unit 9 reversed
the order (emit-references runs after the Python passes and uses
\`handledSites\` to skip what we processed), the pre-seed only causes
harm: when an MRO walk in Case 0 (compound receiver) and Case 4
(simple typeBinding) both touch the same site at the same position
but resolve to different targets, the pre-seed suppresses the second
emission because the shared resolver had already entered the wrong
target into \`seen\`.
Concrete case: \`c.greet().save()\` — Case 0 emits the outer save edge
to Greeting.save; Case 4 then resolves the inner \`c.greet()\` to
A.greet via MRO walk. With pre-seed both edges should emit (different
targets, different rel.ids); without removing the pre-seed the inner
emission was being deduped against an already-seeded entry and the
A.greet edge was lost.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 8 fail / 183 pass (was 9/182; +1 — \`c.greet() to A#greet
via MRO walk\` now passes).
- tsc --noEmit clean.
* feat(python-scope): enumerate(X) for-loop tuple destructuring
Adds two new typeBinding capture patterns for the canonical enumerate
pattern:
for (i, u) in enumerate(users): ... ; tuple_pattern
for i, u in enumerate(users): ... ; pattern_list
Both bind the second tuple element (u) to the iterable identifier
(users). The chain-follow then unwraps users → its element type via
the existing generic-strip in interpret.ts (List[User] → User).
The #eq? predicate scopes the pattern to enumerate specifically;
generic tuple destructuring of arbitrary callables is left to a
future iteration once we have a richer signal for "what does this
call yield".
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 7 fail / 184 pass (was 8/183; +1 — `parenthesized tuple:
for (i, u) in enumerate(users)` now passes).
- tsc --noEmit clean.
* feat(python-scope): dict.items() value-type unwrapping
Two changes that together resolve `for k, v in data.items(): v.save()`:
- `interpret.ts stripGeneric`: extends to `dict[K, V]` /
`Dict[K, V]` / `Mapping[K, V]` etc., stripping to the value type V.
Previously only single-arg generics (list[User] → User) were
stripped; multi-arg ones returned the raw text.
- `query.ts` + `scopes.scm`: new typeBinding patterns for
`for k, v in X.items()` (both pattern_list and tuple_pattern). The
second tuple element binds to X; the chain-follow then unwraps X's
dict annotation to V via the new stripGeneric branch.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 6 fail / 185 pass (was 7/184; +1 — `dict.items() loop`
test now passes).
- tsc --noEmit clean.
* feat(python-scope): nested tuple destructuring for enumerate(d.items())
Two more for-loop typeBinding patterns:
- `for i, (k, v) in enumerate(d.items())` — nested tuple destructuring
where v is the value of the dict's items() yield.
- `for v in d.values()` — explicit values() form (companion to items).
Both bind the loop var to the dict identifier; the chain-follow
unwraps via the dict-aware stripGeneric to the value type.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 5 fail / 186 pass (was 6/185; +1 nested tuple test).
- tsc --noEmit clean.
* feat(python-scope): 3-var flat destructuring for enumerate(d.items())
Adds the \`for i, k, v in enumerate(d.items())\` shape — flat
3-variable destructuring of the (i, (k, v)) tuple yielded by
\`enumerate\` over \`items()\`. Binds v (the last identifier in the
pattern_list) to the dict identifier; the existing dict-aware
stripGeneric unwraps to the value type.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 4 fail / 187 pass (was 5/186; +1).
- tsc --noEmit clean.
* feat(python-scope): write ACCESSES edges for attribute assignments
Three changes that together produce ACCESSES (write) edges for
\`obj.field = value\` assignments:
- New \`@reference.write.member\` capture in query.ts and scopes.scm
matching \`(assignment left: (attribute object: ... attribute: ...))\`.
Reuses the existing receiver/name capture shape so the
receiver-bound emit pass can resolve obj's class and look up the
field.
- \`populateMethodOwnerIds\` now sets ownerId on class-body fields too,
not only on methods. Previously it only walked Function scopes
whose parent was Class; class-body annotations like \`name: str\`
live directly in the Class scope's ownedDefs and were missed, so
\`findOwnedMember(User, "name")\` returned undefined.
- \`emit-core isLinkableLabel\` extends to Variable and Property so
field nodes appear in the graph-node lookup (the legacy parser
emits both kinds for class-body annotations).
- Case 4 in receiver-bound pass now uses the kind word as the edge
reason for read/write sites — matches the legacy DAG convention
the test asserts on.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 3 fail / 188 pass (was 4/187; +1 — write-ACCESSES test).
- tsc --noEmit clean.
* feat(python-scope): chain-typebinding + field-fallback method lookup
Reaches the architectural-plan target of >= 189/191 flag-on passing.
Two intertwined changes:
- Field-fallback in resolveCompoundReceiverClass: when method lookup
on the receiver's class (and its MRO) fails, walk the class's
fields and try the same lookup on each field's type. Matches the
"unified fixpoint" intent of the method-chain fixture where
`user.get_city()` reaches `Address.get_city` through User's
`address: Address` field.
- New Case 3b in receiver-bound emit pass: when the receiver's
typeBinding rawName has a dot but isn't a namespace prefix
(e.g. `city -> user.get_city` from the constructor-inferred capture
for `city = user.get_city()`), treat it as a method-call chain and
pipe through the compound resolver. The chain unwraps to the
terminal class (City) and the call resolves normally.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 2 fail / 189 pass (was 3/188; +1 city.save method chain).
- tsc --noEmit clean.
Remaining 2 failures are fixture-driven (self.users / self.repos
fixtures reference fields that aren't declared on the class) and
documented as known-limitation in Unit 10.
* feat(python-scope): flip Python to registry-primary (191/191 parity)
Adds the \`for u in self.X\` heuristic typeBinding capture (binds u to
the attribute name X so the chain-follow can resolve via the enclosing
method's parameter typeBinding) — closes the last two failing
fixtures whose classes reference \`self.X\` for fields that are
actually method parameters.
With 191/191 passing on BOTH legacy and registry-primary paths,
flips \`MIGRATED_LANGUAGES\` to include \`SupportedLanguages.Python\`.
Effects:
- Production default for Python files: registry-primary path.
- CI parity gate auto-discovers Python via the script + workflow
(\`scripts/ci-list-migrated-languages.ts\` /
\`.github/workflows/ci-scope-parity.yml\`) and runs the resolver
integration test BOTH ways on every PR.
- Operators retain the \`REGISTRY_PRIMARY_PYTHON=0\` escape hatch.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (unset, post-flip): 191/191 (uses registry).
- tsc --noEmit clean.
This concludes RFC #909 Ring 3 — Python migration.
* refactor(emit-core): EmitProvider interface + promote 5 generic helpers
G-Units 1-2 of the emit-pipeline generalization plan.
Adds:
- emit-core/emit-provider.ts — typed EmitProvider contract (6 required +
2 optional fields). Will be consumed by the generic orchestrator in
G-Unit 6. Documents the LanguageProvider vs EmitProvider boundary.
- emit-core/emit-free-call.ts — emitFreeCallFallback promoted as-is
(drops the unused referenceIndex pre-seed parameter; underscore-prefixed
to keep the signature compatible).
- emit-core/propagate-return-types.ts — propagateImportedReturnTypes +
followChainPostFinalize. Documents the mutation contract (Invariant
I3 + I6 from the plan): runs after finalize, before resolve, mutates
the non-frozen Scope.typeBindings map.
- emit-core/scope-walkers.ts: + findEnclosingClassDef +
findExportedDefByName. Both were already generic in the Python
source.
python-scope-emit.ts shrinks 1055 → 799 lines (–256). Imports the
promoted helpers from emit-core. No behavior change.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.
* refactor(emit-core): promote receiver-bound dispatcher + compound resolver
G-Unit 3 of the emit-pipeline generalization plan.
- emit-core/emit-compound-receiver.ts — resolveCompoundReceiverClass
+ matchingOpenParen + COMPOUND_RECEIVER_MAX_DEPTH. Field-fallback
is now an option (default true) so strictly-typed languages can
opt out via EmitProvider.fieldFallbackOnMethodLookup.
- emit-core/emit-receiver-bound.ts — the 7-case dispatcher (super,
Cases 0/1/2/3/3b/4). Accepts a ReceiverBoundProviderSubset
(isSuperReceiver + fieldFallbackOnMethodLookup) so partial wiring
works during the rest of the migration. Documents Contract
Invariants I4 (case order) and I5 (no pre-seeding).
python-scope-emit.ts shrinks 799 → 384 lines. The orchestrator now
calls the generic emitReceiverBoundCalls with an inline minimal
provider (pythonEmitProviderInline) — full provider lands in G-Unit 6
when the orchestrator itself moves to languages/python/emit/.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.
* refactor(emit-core): promote MRO walk + populateClassOwnedMembers
G-Units 4-5 of the emit-pipeline generalization plan.
- emit-core/build-mro.ts — generic buildMro takes a LinearizeStrategy
hook receiving (classDefId, directParents, parentsByDefId). Three
shared steps (collect EXTENDS, build defId-by-graphId, walk per
class) + parametric linearization. Default strategy is BFS-with-
visited (Python's depth-first first-seen, also correct for
single-inheritance languages).
- emit-core/scope-walkers.ts: + populateClassOwnedMembers — generic
OO ownership rule (methods + class-body fields). Both rules ship
together because every OO language migrated so far (Python; planned
TS/JS/Java/Kotlin) wants both. Languages that need different rules
can compose with this as a base step.
python-scope-emit.ts shrinks 384 → 255 lines.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.
* refactor(scope-resolution): generic orchestrator + language-agnostic phase
G-Units 6-7 of the emit-pipeline generalization plan, plus the
pipeline-phase generalization (the user's observation that the phase
itself is generic once the orchestrator is).
Changes:
- emit-core/orchestrator.ts — runScopeResolution(input, provider).
The 180 lines of pipeline glue moved here, parametrized by
EmitProvider. Provider supplies LanguageProvider, importEdgeReason,
and the 6 emit-side hooks.
- emit-core/emit-provider.ts — EmitProvider gains languageProvider
and importEdgeReason fields so the orchestrator needs nothing else.
resolveImportTarget now takes (targetRaw, fromFile, allFilePaths).
- languages/python/emit/index.ts — pythonEmitProvider + thin
runPythonScopeResolution wrapper. The first reference impl every
next-language migration copies.
- emit-providers-registry.ts (NEW) — registry of per-language
EmitProviders keyed by SupportedLanguages. Adding a language is
one line here + the provider file.
- pipeline-phases/scope-resolution.ts (NEW) — language-agnostic phase
iterating EMIT_PROVIDERS ∩ MIGRATED_LANGUAGES. Replaces
pipeline-phases/python-scope.ts (deleted).
- python-scope-emit.ts deleted.
- pipeline.ts swaps pythonScopePhase → scopeResolutionPhase.
The next language migration is now: implement EmitProvider, register
it, add to MIGRATED_LANGUAGES. No new pipeline phase, no orchestrator
copy-paste. The Python migration's 700+ lines of glue collapse to
~80 lines per future language.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.
* docs(emit-provider): migration cookbook for next-language porters
* refactor(scope-resolution): rename emit-core/ → scope-resolution/, EmitProvider → ScopeResolver
Reorganizes the registry-primary resolution layer for clarity and
contributor onboarding. Driven by feedback that "emit" was triple-
overloaded (graph-edge emission + tree-sitter capture extraction +
the provider name itself), and the flat 16-file emit-core/ folder
mixed five concerns.
External research (rust-analyzer hir-def/nameres, Pyright analyzer/,
TypeScript binder/checker, Roslyn Binder, IntelliJ Resolver, swc
semantic/, biome semantic/, semgrep naming/, JDT Binding, clangd
Sema) consistently uses **the phase name** for this layer, never an
output verb. "Scope resolution" matches our pipeline-phase name, the
plan, and the RFC.
## Folder rename
emit-core/ → scope-resolution/
├── (16 flat files) → ├── contract/scope-resolver.ts
├── pipeline/{run,registry,phase}.ts
├── passes/{receiver-bound-calls,
│ free-call-fallback,
│ compound-receiver,
│ imported-return-types,
│ mro}.ts
├── graph-bridge/{node-lookup,ids,
│ edges,references-to-edges,
│ imports-to-edges,
│ method-dispatch}.ts
└── scope/{walkers,namespace-targets}.ts
Each subfolder maps to one concern a new contributor needs to find:
*the contract I implement / the runner that calls me / the helpers I
reuse / the graph layer I shouldn't touch / the scope walkers*.
## Symbol renames
EmitProvider → ScopeResolver
pythonEmitProvider → pythonScopeResolver
runPythonScopeResolution → resolvePythonScope
EMIT_PROVIDERS → SCOPE_RESOLVERS
getEmitProvider → getScopeResolver
RunPythonScopeResolution{Input,Stats} → ResolvePythonScope{Input,Stats}
## File renames (per-language)
languages/python/emit/index.ts → languages/python/scope-resolver.ts
languages/python/emit-captures.ts → languages/python/captures.ts
(kills the parse-side "emit" collision)
## Mechanics
- Used `git mv` for all files so blame history is preserved.
- Updated ~30 import lines across 18 files plus the pipeline-phases
barrel and pipeline.ts.
- Updated JSDoc cross-references throughout to match the new vocabulary.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- Default (post MIGRATED_LANGUAGES flip): 191/191.
- tsc --noEmit clean.
Migration cookbook in `scope-resolution/contract/scope-resolver.ts`
JSDoc points the next-language porter at all the new names and
folder locations.
* docs(scope-resolution): finalize phase JSDoc + drop python emoji from generic log line
* perf(scope-resolution): O(1) workspace lookup index
Introduces `WorkspaceResolutionIndex` — a precomputed bundle of
lookup tables built ONCE per resolution run, after `populateOwners`
and after finalize, before any pass that needs to find members,
exported defs, or class scopes by id.
What it replaces (all are pre-existing O(N×D) linear scans of
parsedFiles, called inside the receiver-bound MRO chain):
- `findOwnedMember(ownerId, name, parsedFiles)` → `Map.get` via
`index.memberByOwner.get(ownerId)?.get(name)`. Was the worst
offender — receiver-bound dispatcher calls this O(sites × MRO
depth) times.
- `findExportedDef(filePath, name, parsedFiles)` → `Map.get` via
`index.defsByFileAndName`. Hot for namespace-receiver case.
- `findExportedDefByName` workspace-wide fallback scan → `Map.get`
via `index.callablesBySimpleName`.
- `classScopeByDefId` (rebuilt inside `emitReceiverBoundCalls` on
every invocation) — moved to one-shot build during finalize, read
from `index.classScopeByDefId` everywhere.
- `moduleScopeByFile` (rebuilt inside `propagateImportedReturnTypes`
on every invocation) — read from `index.moduleScopeByFile`.
Findings from a synthetic 100-file Python workload (60 model files
each defining 5 classes × 3 methods + 40 user files calling them
heavily):
scope-resolution wall time: 764ms → 710ms (median, 5 iters)
That's a ~7% in-layer win. The smaller-than-expected gain was
informative: profiling the synthetic workload shows scope-resolution
breakdown is `extract=62% resolve=30% emit=4%`; the index touched
the 4% slice (emit + walker calls inside it). Larger O(D) per owner
classes will benefit more.
Profiling the FULL pipeline (49 fixtures × 3 iters) shows
scope-resolution accounts for ~1% of pipeline wall time — the
remaining 99% is parse (tree-sitter), heritage, ORM, MRO, processes,
and DB writes. So further optimization of this specific layer has
marginal pipeline impact; the next-biggest wins live in those
phases. Documented as the "double-parse" finding in the audit
(captures.ts re-parses each Python file even though the parse phase
already produced a tree-sitter Tree) — that's a separate plumbing
project across phase boundaries.
Bonus: opt-in PROF_SCOPE_RESOLUTION=1 env var prints a per-phase
ms breakdown to stderr, so future perf work can measure without
extra code changes.
Verification:
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- tsc --noEmit clean.
* perf(parse/heritage/mro): typed graph iterator + cross-phase tree cache
Two structural perf wins targeting the parse / heritage / MRO
layers, identified by the post-WorkspaceResolutionIndex profiling
(scope-resolution = ~1% of pipeline; the bulk lives upstream).
## 1. KnowledgeGraph.iterRelationshipsByType (PHM-Units 1-2)
- Adds a per-type `Map<RelationshipType, Map<id, Relationship>>`
index inside `createKnowledgeGraph`, maintained on add / remove /
removeNode / removeNodesByFile.
- New `iterRelationshipsByType(type)` returns a typed iterator that
yields only the requested type. Backwards-compatible: existing
`iterRelationships()` / `forEachRelationship()` callers untouched.
- Migrated two MRO call sites:
- `mro-processor.ts buildAdjacency`: split the single
`forEachRelationship` (which scanned every edge in the graph and
type-filtered per-iteration) into three typed iterations
(EXTENDS, IMPLEMENTS, HAS_METHOD).
- `scope-resolution/passes/mro.ts buildMro`: replaced
`for (const rel of graph.iterRelationships()) if (rel.type !== 'EXTENDS') continue`
with `for (const rel of graph.iterRelationshipsByType('EXTENDS'))`.
- Heritage-processor (PHM-Unit 3) was a no-op: it only WRITES
EXTENDS/IMPLEMENTS edges, never re-reads. Index is still useful
for the seven other graph-iter consumers (community-processor,
csv-generator, wildcard-synthesis, process-processor, etc.) — those
follow-ups can switch to the typed iterator without touching the
graph layer.
- Adds 5 unit tests for the new method (add/remove/dedupe semantics,
empty-type fresh iterator, removeNode index sync).
## 2. Cross-phase tree cache (PHM-Units 4-5)
The audit's #2 finding: Python files are parsed by tree-sitter once
in the parse phase, then re-parsed inside scope-resolution's
`captures.ts`. Eliminate the second parse by sharing the Tree across
phases.
- `parse-impl.ts` now maintains TWO ASTCaches with distinct lifetimes:
- `astCache` (chunk-local, cleared between chunks) — unchanged;
used by call/heritage/import processors during parse.
- `scopeTreeCache` (total-parseable-sized, never cleared) — new,
exposed via `ParseOutput.astCache` for cross-phase consumption.
- `parsing-processor.ts` writes every sequentially-parsed Tree to
BOTH caches. Worker-mode parses skip the persistent cache too
(Trees can't cross MessageChannels).
- `LanguageProvider.emitScopeCaptures` gains an optional `cachedTree`
parameter (typed `unknown` to keep the tree-sitter dep out of the
contract).
- `captures.ts` short-circuits its own `parser.parse(sourceText)`
when a cached Tree is supplied. Cache miss falls back to a fresh
parse — same correctness path as before.
- `runScopeResolution` accepts an optional `treeCache` and forwards
per-file `cachedTree` to `extractParsedFile`.
- `scope-resolution/pipeline/phase.ts` reads
`getPhaseOutput<{astCache}>(deps, 'parse')` and passes through.
Verified end-to-end: a small fixture run with PROF_SCOPE_RESOLUTION=1
shows 6/6 cache hits (100% hit rate) on the python-grandparent fixture
that exercises the full pipeline below the worker-pool threshold.
## Verification
- REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191.
- REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191.
- New graph.test.ts: 25/25 (was 20).
- tsc --noEmit clean.
## Where the win lands
Wall-clock on the 49-fixture integration suite: 14050ms → 14080ms
(within noise). Fixtures are 1-3 files each, dominated by per-fixture
pipeline overhead (worker-pool init, DB writes, fixture startup).
The cache + typed-iterator wins are constant-factor improvements
that scale linearly with workload size and visible only on larger
repos. The dev-mode `PROF_SCOPE_RESOLUTION` instrumentation +
`getPythonCaptureCacheStats()` are kept for future perf work.
## Plan
docs/plans/2026-04-20-002-perf-parse-heritage-mro-plan.md.
PHM-Unit 3 (heritage-processor migration) intentionally collapsed
to a no-op — heritage only writes, never re-reads.
* perf(scope-resolution): bound tree-cache lifetime + gate population
Address P1 residuals from ce:review of
|
||
|
|
3c896cdbcd
|
fix: close remaining Dart language support gaps (#524)
* fix: close remaining Dart language support gaps Four issues that were not addressed in PR #204: 1. extractFunctionName: add function_signature/method_signature handlers and add both to FUNCTION_NODE_TYPES. Without this, findEnclosingFunctionId cannot resolve Dart function scopes — all calls inside Dart functions have no sourceId, breaking CALLS edge attribution. 2. formal_parameter_list: add to paramListTypes in extractMethodSignature. Dart's tree-sitter grammar uses this node type (not formal_parameters), so parameter counting returns 0 for all Dart functions. 3. Write-access queries: add @assignment patterns for obj.field = value and this.field = value. Without these, no ACCESSES write edges are emitted for Dart code. 4. initialized_identifier guard in extractDartDeclaration: comma-separated declarations (String a, b, c) produce initialized_identifier nodes which are in DART_DECLARATION_NODE_TYPES but were unhandled — the type lives on the parent node. Also adds Dart column to the feature matrix in type-resolution-system.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(dart): field-type resolution, call attribution, import resolution, and integration tests Fixes five Dart language support gaps with integration tests and architectural alignment: **Tree-sitter queries** — Add field declaration patterns for typed and nullable class fields (`String name = ''`, `String? name`). Without these, Dart class fields were invisible to the pipeline (zero Property nodes, zero HAS_PROPERTY edges). **Import resolution** — Dart relative imports (`import 'models.dart'`) don't use a leading `./`. The standard resolver only recognises paths starting with `.` as relative; bare paths fell through to a Java-style dot-to-slash conversion that mangled `models.dart` into `models/dart`. Fix: prepend `./` before calling resolveStandard. **Call attribution** — Dart's tree-sitter grammar places `function_body` as a sibling of `function_signature`, not as a child wrapping both. The `findEnclosingFunction` parent-walk never found the function because the call lives inside `function_body` which is a sibling of the signature. Fix: add `enclosingFunctionFinder` hook to LanguageProvider interface (following the same strategy pattern as `labelOverride`), with the Dart-specific logic in `languages/dart.ts`. Both `parse-worker.ts` and `call-processor.ts` consume the hook generically — no Dart-specific code in the generic processors. **Receiver chain extraction** — Add `unconditional_assignable_selector` to `MEMBER_ACCESS_NODE_TYPES` so `inferCallForm` returns `'member'` for Dart method calls. Add Dart-specific receiver extraction blocks in `extractReceiverName`, `extractReceiverNode`, and a `selector` handler in `extractMixedChain` for Dart's flat sibling-selector model (vs the nested member-expression model used by all other languages). **Integration tests** — New `dart.test.ts` with field-type resolution and call-result-binding describe blocks. Fixtures: `dart-field-types/` (models.dart + app.dart) and `dart-call-result-binding/` (models.dart + app.dart). 9 passing tests, 1 skipped (ACCESSES edges for field reads depend on type-env parameter binding propagation — tracked for follow-up). --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Gergo Magyar <gergomagyar@icloud.com> |
||
|
|
fcf8fb9bdf
|
docs: add Swift ingestion gaps tracker and update feature matrix
- Create swift-ingestion-gaps.md with prioritized gap tracker (High/Medium/Low) - Update type-resolution-system.md feature matrix: 5 Swift entries corrected (for-loop→Yes, pattern binding→Partial, call-result/field/method→Yes) - Add footnotes explaining Swift-specific semantics - Document resolved items with commit references Addresses @magyargergo's request to document missing Swift features. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fb20a3c752 |
feat: implement cross-file binding propagation for multiple languages
- Enhance C++ tree-sitter queries to support inline class method declarations and return types. - Introduce `importedRawReturnTypes` in `BuildTypeEnvOptions` for cross-file raw return type handling. - Add `FileTypeEnvBindings` interface to capture file-scope type bindings for exported symbols. - Implement logic in `parse-worker.ts` to extract and serialize file-scope type bindings for cross-file type resolution. - Create test fixtures for C++, Go, Ruby, and Rust to validate cross-file binding propagation. - Update integration tests to verify correct resolution of method calls across files for C++, Go, Ruby, and Rust. - Document Phase 14: Cross-File Binding Propagation in the type resolution roadmap and system documentation. |
||
|
|
228c993bb7 |
fix(type-resolution): review fixes, sizeBefore optimization, and test coverage
Address code review findings from PR #392 senior compiler review: - Fix Java "Yes" → "No" in optional-param-arity matrix (Java has no defaults) - Simplify Kotlin hasDefaultValue while-as-if to direct const/if check - Update OPTIONAL_PARAM_TYPES comment to include Ruby - Replace per-declaration Set allocation with size-based Map iteration skip - Add 11 unit tests for multi-declarator type association and constructorTypeMap |
||
|
|
c3a2815186 |
feat(type-resolution): optional parameter arity resolution
Add requiredParameterCount to SymbolDefinition and MethodSignature, enabling range-based arity filtering in filterCallableCandidates. Calls with omitted optional/default arguments now resolve correctly. Supported: TS, Python, Kotlin, C#, C++, PHP, Ruby (7 languages). Detection via OPTIONAL_PARAM_TYPES set + hasDefaultValue helper. 9 integration tests added across all 7 languages. |
||
|
|
8273324f3c |
fix: address PR review — Rust await unwrap, stale doc claims, this-receiver footnote
Review follow-ups from compiler front-end review (#379): - Rust extractPendingAssignment now calls unwrapAwait() on value before type checks, so `let user = get_user().await` resolves correctly - type-resolution-system.md: removed "no fixpoint inference" from limitations, updated "Single-pass" to "Walk + fixpoint", replaced stale single-pass Tier 2 description with fixpoint loop explanation - type-resolution-roadmap.md: Phase 9 body updated — 9C is delivered, 9B walk-order dependency documented (for-loop Tier 0b runs before fixpoint, so fixpoint-resolved types can't update loop variables) - Added this/self/$this fixpoint gap footnote to feature matrix |
||
|
|
e6b8edc1ac |
feat: Phase 9C unified fixpoint with field access and method-call-result binding
Replace the sequential Tier 2b/2a propagation with a unified fixpoint loop that handles four binding kinds: callResult, copy, fieldAccess, and methodCallResult. The loop iterates until no new bindings are produced (max 10 iterations), enabling arbitrary-depth mixed chains: const user = getUser(); // callResult → User const addr = user.address; // fieldAccess → Address const city = addr.getCity(); // methodCallResult → City city.save(); // resolves to City#save Infrastructure: - PendingAssignment union extended with fieldAccess and methodCallResult - resolveFieldType helper: typeName → class nodeId → lookupFieldByOwner - resolveMethodReturnType helper: typeName → class nodeId → lookupFuzzyCallable filtered by ownerId - Fixpoint also resolves reverse-order copy chains that single-pass missed Languages: TS, JS, Java, Kotlin, C#, Go, Rust, Python, PHP, Ruby, C++. Each gets field access and/or method-call-with-receiver detection in extractPendingAssignment, plus method-chain-binding test fixtures. |
||
|
|
5769872b70 |
feat: Phase 9 call-result variable binding across 11 languages
Activate the dormant Tier 2b pendingCallResults infrastructure in
type-env.ts by extending each language's extractPendingAssignment to
emit { kind: 'callResult', lhs, callee } when the RHS of an untyped
variable declaration is a simple function call.
This enables `var user = getUser(); user.save()` to resolve at TypeEnv
build time. Tier 2b now runs before Tier 2a copy-propagation, enabling
mixed chains like `const user = getUser(); const alias = user;
alias.save()`.
Languages: TS, JS, Java, Kotlin, C#, Go, Rust, Python, PHP, Ruby, C++.
Swift excluded. Each language gets a call-result-binding test fixture
and integration tests.
Conservative: only simple calls (no method calls with receivers), only
when exactly one callable matches, first-writer-wins.
|
||
|
|
973c7bfbf0
|
feat: ACCESSES edge type with read/write field access tracking (#372)
* feat: Phase 1 ACCESSES edge type — read tracking from chain resolution Add ACCESSES relationship type to track field read access during call chain resolution. When walkMixedChain resolves a field access (e.g., user.address.save()), an ACCESSES edge with reason 'read' is emitted from the calling function to the Property node. Schema: ACCESSES added to RelationshipType, REL_TYPES, VALID_RELATION_TYPES, context queries, tools/resources descriptions. Excluded from default impact BFS to prevent traversal explosion. Implementation: resolveFieldAccessType now returns FieldResolution with fieldNodeId. walkMixedChain accepts optional onFieldResolved callback. makeAccessEmitter factory provides Set-based dedup per source node. Bug fix: Added Java 'field_access' to FIELD_ACCESS_NODE_TYPES — was missing, causing extractMixedChain to fail for Java member access. * feat: Phase 2 ACCESSES write edges — assignment detection across 12 languages Add tree-sitter query patterns for field write detection (obj.field = value) across all supported languages: TS/JS, Python, Java, Go, C++, C#, Rust, PHP, Ruby (setter syntax), Kotlin, Swift. Processing: Sequential path handles assignment captures inline. Worker path extracts ExtractedAssignment data for deferred resolution via new processAssignmentsFromExtracted function. Bug fix: Kotlin/Swift assignment queries used invalid navigation_expression wrapper — fixed to match actual directly_assignable_expression AST structure. Tests: Write access integration tests for TS, Java, Python, Go with dedicated fixtures. All use strict toBe() assertions. * test: add unit tests for call-routing, shared type extractors, and symbol-table branches Add 215 new unit tests across 3 files to increase branch coverage toward the 23% global threshold (was 21.49%): - call-routing.test.ts (49 tests): Ruby call routing — require/require_relative, include/extend/prepend heritage, attr_accessor properties with YARD types - shared-type-extractors.test.ts (108 tests): pure string functions — extractElementTypeFromString, stripNullable, extractReturnTypeName, methodToTypeArgPosition, getContainerDescriptor - symbol-table.test.ts (+29 tests): Property/fieldByOwner index, metadata spread branches, lazy callable index, lookupExactFull shape * fix: defer write-access resolution to fix Ruby cross-file property timing Ruby attr_accessor properties are registered during processCalls (not the parsing phase), so lookupFieldByOwner fails when service.rb is processed before models.rb. Fix by collecting pending write-access edges during the file loop and resolving them after all files are done. Also adds write-access integration tests and fixtures for 7 languages (C++, C#, JS, Kotlin, PHP, Ruby, Rust), Ruby compound assignment query, PHP static property write query, and Kotlin property type extraction. * fix: address PR #372 review — write-access constructor bindings parity and docs - Add verified constructor bindings fallback to write-access resolution in both sequential path (receiverIndex lookup) and worker path (constructorBindings param for processAssignmentsFromExtracted), closing the read/write ACCESSES edge asymmetry for factory-returned receivers - Clarify inner guard control flow comment in processCalls match loop - Document Go inc_statement/dec_statement gap in roadmap - Clarify PHP nullsafe write footnote (invalid syntax, not just untracked) - Update symbol-table tests for intentional fieldByOwner behavior change (Properties without declaredType now indexed for dynamic language write-access tracking) |
||
|
|
11a3d0515c
|
feat: Phase 8 field/property type resolution (#354)
* feat: Phase 8 field/property type resolution — resolve chained member access
Add field/property type extraction to the type resolution system so that
chained member access like `user.address.save()` resolves the intermediate
receiver type (`address → Address`) through Property symbols in SymbolTable.
Key changes:
- SymbolTable: add `declaredType` field, `fieldByOwner` O(1) index,
`lookupFieldByOwner()` method, P0 conditional callableIndex invalidation,
P2 exclude Properties from globalIndex to prevent namespace pollution
- tree-sitter queries: add `definition.property` for TypeScript, Java, Go
- parse-worker: extract declared types for Property nodes via
`extractPropertyDeclaredType()`, capture field-access receiver info
- call-processor: add `resolveFieldAccessType()` helper and field-access
branch in both sequential and worker receiver resolution paths
- Integration tests: new field-types test suite verifying end-to-end
`user.address.save() → Address#save` resolution
* fix: Go tree-sitter query captures field_declaration not field_declaration_list
Post-review fix: the Go struct field query incorrectly put @definition.property
on field_declaration_list (the list container) instead of field_declaration
(the individual field). Also removed unused `language` parameter from
extractPropertyDeclaredType.
* feat: expand field-type tests to 6 languages, fix Go ownerId and Kotlin navigation_expression
- Add integration test fixtures for Java, C#, Go, Kotlin, PHP (alongside existing TS)
- Fix Go: add type_declaration handling in findEnclosingClassId for struct fields
(field_declaration → field_declaration_list → struct_type → type_spec → type_declaration)
- Fix Kotlin: add navigation_expression handling in field-access resolution
(Kotlin uses navigation_expression + navigation_suffix, not member_expression)
- Add extractMemberAccessParts helper in call-processor for cross-language member access
- All 24 field-type tests pass across 6 languages, 181 Go+Kotlin tests pass with no regressions
* refactor: split HAS_METHOD into HAS_METHOD + HAS_PROPERTY edge types
Property nodes now use HAS_PROPERTY edges instead of HAS_METHOD, giving
the graph schema proper semantic separation between methods and fields.
- HAS_METHOD: Method, Constructor, Function (when inside a class)
- HAS_PROPERTY: Property nodes (class fields, struct fields, attributes)
MRO processor only reads HAS_METHOD — properties correctly excluded from
method resolution order. Impact analysis accepts both edge types.
Updated 12 files: graph types, schema, tools docs, parse-worker,
parsing-processor, call-processor, and 6 test files.
* fix(test): update security test to expect 7 VALID_RELATION_TYPES (added HAS_PROPERTY)
* test: add unit tests for Phase 8 SymbolTable features (39 tests, up from 19)
Cover all new branches: declaredType metadata, Property exclusion from
globalIndex, conditional callableIndex invalidation, lookupFieldByOwner
(happy path + edge cases), lookupFuzzyCallable filtering, and clear()
with fieldByOwner. Fixes branch coverage threshold (21.8% → 23%+).
* feat: Phase 8B mixed field+method chain resolution, C++/Rust chain fixes
Unify field and method chain resolution into a single `extractMixedChain`
walker that handles interleaved patterns like `svc.getUser().address.save()`.
Fix C++ chain calls (tree-sitter-cpp `field_expression` uses `argument` not
`object`), Rust unit struct instantiation (`let svc = TypeName;`), and add
stdlib passthrough for `unwrap()`/`clone()`/`expect()` in chain loops.
Key changes:
- Replace `receiverCallChain` + `receiverFieldAccess` with unified
`receiverMixedChain: MixedChainStep[]` on ExtractedCall
- Add `extractMixedChain` in utils.ts (handles both call_expression and
field_expression nodes, including C++ `argument` field)
- Add `TYPE_PRESERVING_METHODS` set for stdlib identity operations
- Add C++ inline method double-indexing guard in parsing-processor.ts
and parse-worker.ts
- Add Rust unit struct recognition in type-extractors/rust.ts
- Split field-types.test.ts into per-language test files
- Add ts-mixed-chain fixture and integration tests
- Resolve rust.test.ts todo: Option<T>.unwrap().save() now works
- Update roadmap: Phases 7+8 complete, Phase 9 is next
* fix: Python declaredType extraction and sequential-path property registration
- Move @definition.property capture from expression_statement to assignment
node in Python queries so Strategy 1 childForFieldName('type') succeeds
- Pass item.declaredType through ctx.symbols.add in sequential call-processor
path, matching worker path behavior (fixes Ruby YARD declaredType drop)
- Add Python chain resolution integration test (user.address.save → Address#save)
- Update Rust/Python status in roadmap and system docs to reflect actual coverage
* fix: Python/Ruby field type disambiguation and Rust chain test
Three fixes from PR #354 third review:
1. Python typed_parameter name extraction: tree-sitter-python's
typed_parameter uses positional children for the name, not a named
field. TypeEnv and extractParameter now fall back to firstNamedChild.
2. Ruby/Python call-step field resolution: Ruby's AST uses `call` nodes
for both property access and method calls. The chain walker now tries
resolveFieldAccessType before resolveCallTarget for call steps, so
attr_accessor properties resolve via declaredType.
3. Rust chain resolution test: added missing integration test asserting
user.address.save() resolves to Address#save.
Also splits C/C++ and TS/JS columns in type-resolution-system.md
language matrix with footnotes for accuracy.
1062 resolver integration tests passing, 0 failures.
* refactor: Phase 8 code review cleanup — extract walkMixedChain, fix MCP agent gaps
- Extract duplicated chain resolution loop into shared walkMixedChain() helper,
eliminating ~60 lines of copy-pasted code between sequential and worker paths
- Add returnType to ResolveResult, removing redundant lookupFuzzy+find per chain step
- Fix context() tool to include HAS_METHOD, HAS_PROPERTY, OVERRIDES in queries
so agents can discover class members
- Fix p.declaredType Cypher example (column doesn't exist) → p.description
- Add HAS_METHOD, HAS_PROPERTY, OVERRIDES to schema resource
- Document HAS_METHOD/HAS_PROPERTY in impact tool description
- Delete dead code extractMemberAccessParts (superseded by extractMixedChain)
- Replace any with SyntaxNode on extractPropertyDeclaredType
- Add Rust deep-field-chain test (5 tests), Java mixed-chain (4), Go mixed-chain (4)
- All 1075 tests pass (13 new, 0 regressions)
* refactor: type SymbolDefinition.type as NodeLabel, add O(1) receiver index
- Change SymbolDefinition.type from string to NodeLabel union (35 members)
across symbol-table.ts, parse-worker.ts, parsing-processor.ts — compiler
now enforces correctness at all comparison/assignment sites
- Replace O(N*M) linear scan in lookupReceiverType with pre-built
ReceiverTypeIndex (Map<funcName, Map<varName, Entry>>) for O(1) lookups
with proper ambiguity handling and file-level fallback
- All 1075 tests pass, 0 regressions
* fix: capture C++ pointer/ref fields, Kotlin data class props, PHP constructor promotion
Add tree-sitter query patterns for three previously missed property declaration
forms: C++ pointer/reference member fields (Address* addr; Address& ref;),
Kotlin primary constructor val/var parameters (data class User(val name: String)),
and PHP 8.0+ constructor property promotion (public Address $address).
Fix "10 languages" off-by-one in docs (Ruby is single-level only, not deep chain).
Update Python feature matrix cell from No* to Yes* after
|
||
|
|
604b575e4b
|
feat: Phase 7 type resolution — return-aware loop inference & PHP class-property iterables (#341)
* feat(type-resolution): Phase 7.1+7.2 foundation — ReturnTypeLookup, context object, pendingCallResults - Move extractReturnTypeName + helpers from call-processor.ts to type-extractors/shared.ts (breaks circular import risk: call-processor → type-env → type-extractors → call-processor) - Add SymbolTable.lookupFuzzyCallable(name) — lazy callable-only index, O(1) per call, invalidated on add(); avoids per-call .filter() on lookupFuzzy results - Add ReturnTypeLookup interface (conservative: undefined when 0 or 2+ callables match) - Add ForLoopExtractorContext interface — replaces 4 positional params with context object; update all 10 language extractor implementations (go, ts, py, jvm×2, cs, rs, rb, php, c-cpp) - Add PendingAssignment discriminated union (kind: 'copy' | 'callResult'); update PendingAssignmentExtractor in all 9 language extractors that implement it - Wire buildTypeEnv: build ReturnTypeLookup from optional symbolTable; split pendingAssignments into pendingCopies + pendingCallResults; add Tier 2b call-result propagation loop - Update call-processor.test.ts to import extractReturnTypeName from shared.ts * feat(type-resolution): Phase 7.3 — call_expression iterables in for-loop extractors (7 languages) Extends for-loop type extraction in all 7 typed-iteration languages to resolve element types when the iterable is a direct function call. **New capability**: `for (var u : getUsers())` in Java, `for u in get_users()` in Python, `for user in getUsers()` in TypeScript, etc. now resolve `u`/`user` to the callee's return element type via lookupRawReturnType + extractElementTypeFromString. Changes per language: - types.ts: extend ReturnTypeLookup with lookupRawReturnType (raw return string for container-type extraction); update ForLoopExtractorContext with returnTypeLookup field - type-env.ts: implement lookupRawReturnType on the concrete ReturnTypeLookup built in buildTypeEnv (same guards as lookupReturnType, no extractReturnTypeName) - go.ts: call_expression branch in range_clause — identifier func or selector_expression method; existing isChannelType guards updated - typescript.ts: identifier fn branch inside call_expression handler - python.ts: identifier fn branch inside call handler - jvm.ts (Java): method_invocation without object field in enhanced_for_statement - jvm.ts (Kotlin): simple_identifier callee branch in call_expression node - csharp.ts: identifier fn branch in invocation_expression handler - rust.ts: identifier func branch in call_expression handler (alongside existing field_expression/method-call path) All branches follow the same conservative pattern: lookupRawReturnType(callee) → extractElementTypeFromString → bind loop var * feat(type-resolution): Phase 7.4 — PHP \$this->property iterable via @var class property scan Adds Strategy C to PHP's extractForLoopBinding for the pattern: foreach (\$this->property as \$item) when Strategy A (resolveIterableElementType) and Strategy B (scopeEnv lookup) both fail to find the element type. Strategy C: when the iterable is a member_access_expression with object '$this', walk up the AST to the enclosing class_declaration, scan its declaration_list for a property_declaration whose variable_name matches the property, and extract the element type from: 1. PHPDoc @var annotation on a preceding comment sibling (/** @var User[] */) 2. PHP 7.4+ native type field (e.g. UserRepo \$repo — skips generic 'array') This eliminates the @param workaround that was previously required in the php-foreach-member-access fixture (which used @param User[] \$users on the method to populate the method's scopeEnv with a \$users binding). New helpers in php.ts: - PHPDOC_VAR_RE: regex for @var extraction - extractClassPropertyElementType: reads @var or native type from a property_declaration - findClassPropertyElementType: scans class body for a named property Tests added (type-env.test.ts): - PHP: resolves from @var User[] without @param workaround - PHP: conservative — no binding for unknown property - PHP: multi-class file — both classes resolve independently Fixture updated (php-foreach-member-access/App.php): - Removed the @param User[] \$users workaround from processMembers() - Test now validates the natural class-property-based resolution path * docs: mark Phase 7 complete in type-resolution-roadmap.md Records that 7A (call_expression iterables, 7 languages), 7B (PHP $this->property via @var scan), and 7C (ReturnTypeLookup + context object) are all shipped. Adds implementation notes and strikethroughs on resolved language-specific gaps. * fix(docs): update project references to feat-phase7-type-resolution in AGENTS.md and CLAUDE.md * feat(type-resolution): Phase 7.5 — PHP call_expression foreach + integration tests for 7 languages Add integration test coverage for Phase 7.3's call_expression iterable resolution across all 7 languages (Go, TypeScript, Python, Java, Kotlin, PHP, Rust). Each test creates a fixture with competing User/Repo classes that both define save(), then verifies for-loop iteration over a function call's return value resolves to the correct class. PHP was missing function_call_expression support in its for-loop extractor. Three changes fix this: - php.ts extractForLoopBinding: handle function_call_expression and member_call_expression iterables via returnTypeLookup - php.ts normalizePhpReturnType: preserve array notation (User[]) in SymbolTable so lookupRawReturnType returns useful container types - parse-worker.ts + parsing-processor.ts: upgrade uninformative AST return types (array, iterable) with PHPDoc @return annotations 35 new integration tests (5 per language), 2525 total tests passing. * fix(type-resolution): address PR #341 review findings — PHP asymmetry + dormant infrastructure docs - Replace normalizePhpType with extractElementTypeFromString in PHP call-expression foreach paths, aligning with all 6 other language extractors and preventing incorrect binding of bare non-container types like User - Add NOTE comments clarifying pendingCallResults Tier 2b is infrastructure-ready but no extractor populates it yet - Expand Go channel-type comments explaining why non-channel assumption is safe * fix(type-resolution): address verification review — docs accuracy + PHP fallback guard - Roadmap lines 86/100: correct pendingCallResults from "active" to "dormant infrastructure (Phase 9)" - type-resolution-system.md line 363: update to reflect Phase 7.3 loop inference is delivered - type-resolution-system.md line 409: clarify for-loop call-expression resolution (done) vs general assignment propagation (pending) - php.ts:127: add declaration_list type guard on fallback to prevent silent wrong results |
||
|
|
f0132c1077
|
feat: Phase 6 type resolution — for-loop Tier 1c, pattern matching, container descriptors, 10-language coverage (#318)
* feat: Phase 6 type resolution — pattern matching, for-loop Tier 1c, coverage completion
- Add patternBindingNodeTypes gate to LanguageTypeConfig for 50% perf improvement
- Expand ForLoopExtractor signature with optional declarationTypeNodes + scope
- Add extractElementTypeFromString shared utility for container type parsing
- Python match/case: extractPatternBinding for `case User() as u:` pattern
- C# refactor: move is_pattern_expression from extractDeclaration to extractPatternBinding
- Ruby: add extractPendingAssignment for assignment chain propagation
- TS/JS: add for-loop Tier 1c for `for (const user of users)` with User[] inference
- Python: add for-loop Tier 1c for `for user in users:` with type annotation inference
- Go: add for-loop Tier 1c for `for _, user := range users` with []User inference
- Fix 'Property' as any stale cast in call-processor.ts
- Add dual return-type string length cap (2048 pre-cap, 512 post-cap)
- Add chain call integration tests for C#, Go, Rust, Python, JS, C++
- Add Python match/case integration test fixtures
- 27 new extractElementTypeFromString unit tests
- 3 for-loop edge cases skipped (declarationTypeNodes scope key lookup)
* fix: address code review findings for Phase 6
- Add missing patternBindingNodeTypes to C# typeConfig (perf gate)
- Add 2048-char input length guard to extractElementTypeFromString
- Skip Python match/case integration tests (call extraction needs query updates)
* reorganise
* fix: Phase 1 bug fixes — Go range semantics, typed_parameter, bracket depth
- Go single-var range correctly returns early for slices/maps (index, not element)
- Go single-var range on channels correctly resolves element type
- Added map_type and channel_type to extractGoElementTypeFromTypeNode
- Added isChannelType helper for channel detection before skip decision
- Added 'typed_parameter' to TYPED_PARAMETER_TYPES for Python annotated params
- Fixed bracket depth tracking in extractElementTypeFromString — only match
selected closeChar at depth 0, return undefined for mismatched brackets
- Un-skipped 3 prematurely skipped tests (TS local const, Python List/Sequence)
- Added tests for map range, single-var range semantics, bracket edge cases
* refactor: Phase 2 architecture — shared helper, required params, decoupled type nodes
- Extract resolveIterableElementType shared helper in shared.ts implementing
3-strategy fallback (declarationTypeNodes → scopeEnv string → AST walk)
- Refactor TS, Python, Go extractors to use shared helper (eliminates 3x duplication)
- Make ForLoopExtractor params required (aligned with PatternBindingExtractor)
- Update Java, Kotlin, C# extractor signatures to accept required params
- Decouple declarationTypeNodes from scopeEnv — capture raw type annotation
nodes BEFORE extractDeclaration for container types (User[], []User, List[User])
- Hybrid approach: direct name extraction + keysBefore fallback for multi-declarator
- Document declarationTypeNodes invariant change (superset of scopeEnv)
* feat: Phase 3 partial — Rust for-loop + C# var foreach Tier 1c
- Rust: add extractForLoopBinding with for_expression support
- Handles &users, &mut users via reference_expression unwrapping
- extractRustElementTypeFromTypeNode: generic_type, reference_type, slice/array
- findRustParamElementType: AST walk with reference/mut pattern unwrapping
- 4 unit tests (Vec<User>, &[User], range expr negative, no-annotation negative)
- C#: upgrade foreach to handle var (implicit_type) via Tier 1c
- extractCSharpElementTypeFromTypeNode: generic_name, array_type, nullable_type
- findCSharpParamElementType: AST walk to method_declaration parameters
- 3 unit tests (var foreach, explicit type regression, no-annotation negative)
* feat: Phase 3 complete — all language gaps + pattern matching
Kotlin Tier 1c:
- Unannotated for-loop resolves via shared helper
- extractKotlinElementTypeFromTypeNode handles type_projection unwrapping
- findKotlinParamElementType walks to function_declaration
Java Tier 1c:
- var foreach resolves via shared helper
- extractJavaElementTypeFromTypeNode handles generic_type, array_type
- findJavaParamElementType walks to method_declaration
TypeScript:
- readonly User[] unwrapped via readonly_type → array_type recursion
C# switch patterns:
- declaration_pattern added to patternBindingNodeTypes
- extractPatternBinding handles standalone declaration_pattern (switch case/expr)
Rust match arms:
- match_arm added to patternBindingNodeTypes
- extractPatternBinding extended with match_arm → match_expression parent traversal
Python:
- as_pattern tries childForFieldName('alias') before positional fallback
Tests: 237 pass (was 224), 13 new tests added
* feat: Phase 4 — known limitation tests, match arm fix, final verification
- Fix Rust match_arm pattern extraction: unwrap match_pattern to get
tuple_struct_pattern inside (tree-sitter-rust wraps in match_pattern node)
- Add first-writer-wins regression test for match arm scope leakage
- Add 5 documented skip tests for known limitations:
- TS destructured for-of (tuple destructuring)
- Python tuple unpacking in for-loops
- TS instanceof narrowing (block-level scoping)
- Rust for with .iter() (method call iterable)
- Ruby block parameters (closure param inference)
Final: 238 passed, 5 skipped (documented limitations), tsc clean
* test: integration tests for all Phase 6 language gaps + fix Rust param pattern field
Integration test fixtures and tests (30 new tests, all with exact match + negative):
Rust for-loop (5 tests):
- for user in &users with Vec<User> → User#save, negative Repo#save
- for repo in &repos with Vec<Repo> → Repo#save, negative User#save
Rust match arm (5 tests):
- match opt { Some(user) => user.save() } → User#save, negative Repo#save
- if let Ok(repo) = res → Repo#save, negative User#save
C# var foreach (5 tests):
- foreach (var user in users) with List<User> → User#Save, negative Repo#Save
- foreach (var repo in repos) with List<Repo> → Repo#Save
C# switch pattern (4 tests):
- is User user → User#Save, case Repo repo → Repo#Save
Kotlin unannotated for (4 tests):
- for (user in users) with List<User> → user.save, negative repo.save
Go map range (3 tests):
- for _, user := range userMap with map[string]User → User#Save, negative
TypeScript readonly (4 tests):
- for (const user of users) with readonly User[] → user.save, negative
Bug fix: type-env.ts parameter branch now falls back to childForFieldName('pattern')
for Rust parameters (Rust uses 'pattern' not 'name' for parameter names)
* test: add assertion bodies to known limitation skip tests
Convert empty skip test stubs to proper tests with parse/buildTypeEnv/expect
assertions following the codebase convention (e.g., call-processor.test.ts:319).
Each skip test now documents the exact expected behavior, so removing .skip
will cause a meaningful failure when the limitation is eventually fixed.
Also clarify Python integration skip tests as call-extraction issues (not
type-env) and Swift integration skips as build-dep issues (self/super
resolution code already exists in type-env.ts).
* feat: resolve 4 known limitation skip tests + method-aware type arg selection
Unskip 4 of 5 type-env known limitations with full integration test coverage:
1. TS destructured for-of: handle array_pattern by binding last named child
to element type. Fix Map<K,V> to return last generic arg (value type).
2. Python dict.items() loop: handle `call` iterables + `pattern_list` left
side. Fix dict[K,V] extraction via type_parameter with last-arg heuristic.
Unwrap `type` wrapper in extractPyElementTypeFromAnnotation.
3. TS instanceof narrowing: add extractPatternBinding for binary_expression
with positional child access. First-writer-wins (not block-scoped).
4. Rust .iter() for-loops: handle call_expression in for_expression value
node by extracting receiver from field_expression.
Method-aware type arg resolution:
- Add TypeArgPosition ('first'|'last') to resolveIterableElementType
- .keys()/.keySet()/.Keys → first type arg (key); all else → last (value)
- Thread position through all 3 strategy callbacks in TS/Rust/Python
- Add predefined_type to extractSimpleTypeName for TS primitives (string etc)
New fixtures: rust-iter-for-loop, typescript-destructured-for-of,
typescript-instanceof-narrowing, python-dict-items-loop.
248 unit tests pass (6 new), 1 skip (Ruby block params).
* feat: container descriptor table for generic type arg resolution
Replace simple KEY_METHODS heuristic with CONTAINER_DESCRIPTORS table
that maps 30+ container types across all languages to their type parameter
semantics per access method.
Key improvements:
- Container-aware resolution: HashMap.iter() correctly yields V (arity 2),
while Vec.iter() yields T (arity 1) — same method, different semantics
- Cross-language coverage: Map/HashMap/BTreeMap/dict/Dict/Dictionary/
ConcurrentHashMap + List/Vec/Set/HashSet/Queue/Deque/Stack etc.
- Method categorization: keyMethods (keys/keySet/Keys) vs valueMethods
(values/get/pop/iter/first/last) per container type
- Fallback for unknown containers: still uses method name heuristic,
so MyCache<K,V>.keys() correctly returns first arg
- Exported getContainerDescriptor() for future heritage-chain lookups
Each language extractor now passes containerTypeName from scopeEnv to
methodToTypeArgPosition for descriptor-aware resolution.
252 unit tests pass (4 new descriptor tests), 1 skip (Ruby).
* feat: method-aware for-loop extractors + integration tests for all languages
Upgrade 4 existing extractors + create 3 new ones for full cross-language
coverage of call_expression iterables and container descriptor resolution:
Upgraded (add call expr iterable + methodToTypeArgPosition):
- Java: method_invocation (data.keySet(), data.values())
- Kotlin: navigation_expression + call_expression (data.keys, data.values())
- C#: member_access_expression + invocation_expression (data.Keys, data.Values)
- Go: TypeArgPosition threading for Go 1.18+ generics
New for-loop extractors:
- C++: for_range_loop with auto& unwrapping, template_type + qualified_identifier
(std::vector<User>) extraction, explicit vs auto type handling
- PHP: foreach_statement with simple/key-value/by-reference forms, PHPDoc
@param priority over AST array type
- Ruby: for-in with YARD @param type resolution via comment parsing
Integration test fixtures + tests for all 6 languages:
- java-map-keys-values (Map.values() + List iteration)
- kotlin-map-keys-values (HashMap.values + List iteration)
- csharp-dictionary-keys-values (Dictionary.Values foreach)
- cpp-range-for (auto& + const auto& range-based for)
- php-foreach-loop (foreach with PHPDoc @param User[])
- ruby-for-in-loop (for-in with YARD @param Array<User>)
Bugs fixed during integration testing:
- C++: qualified_identifier (std::vector) not unwrapped to template_type
- PHP: extractParameter overwrote PHPDoc-derived types with bare 'array'
252 unit tests pass, 201 integration tests pass across 6 languages.
* fix: update extractElementTypeFromString tests for last-arg default
TypeArgPosition change (default 'last') broke 5 existing tests expecting
first arg from multi-arg generics. Updated expectations and added explicit
pos='first' tests for key type extraction.
* fix: rename C++ fixture files to correct case for case-sensitive CI
On case-sensitive filesystems (Linux/macOS CI), git tracked both the old
lowercase files (app.cpp, user.h) and the new uppercase files (App.cpp,
User.h) as separate files. The pipeline processed both, causing the old
app.cpp (with explicit User& type) to interfere with the new auto& test.
Removes old lowercase entries and re-adds with uppercase casing to match
the #include directives in the fixture.
* feat: PR #318 review findings — pattern bindings, member access iterables, structured bindings
Address all 7 genuine gaps identified in PR #318 deep code review:
- Kotlin: add extractKotlinPatternBinding for when/is (type_test AST node)
with allowPatternBindingOverwrite for smart-cast semantics
- Java: add type_pattern branch for Java 17+ switch pattern variables
- TypeScript: explicit object_pattern skip in for-of (no false bindings)
- Cross-language: member access iterables (self.users, this.users, repo.users)
across all 10 language extractors
- C++: structured_binding_declarator handling in range-for (last-child heuristic)
- Rust: closure_parameter added to TYPED_PARAMETER_TYPES
- PHP: normalizePhpType handles angle-bracket generics (Collection<User>)
Code review fixes applied:
- Remove 4 debug console.log statements (c-cpp.ts, call-processor.ts)
- Hoist KNOWN_CONTAINER_PROPS to module scope (csharp.ts)
- Guard keysBefore allocation behind typeNode check (type-env.ts)
- Add depth limits (50) to 7 recursive type extraction functions
- Add 2048-char length cap to extractSimpleTypeName
- Fix PHP/Ruby missing typeArgPos parameter in resolveIterableElementType
Integration test fixtures: kotlin-when-pattern, java-switch-pattern,
cpp-structured-binding, typescript-member-access-for-loop,
python-member-access-for-loop
* fix: position-indexed when/is bindings, Kotlin param extraction, HashMap.values for-loop
Three root causes for failing Kotlin integration tests:
1. When/is multi-arm resolution: flat scopeEnv stored only the last arm's
type (last-writer-wins). Added PatternOverrides with AST range indexing
so each when arm resolves to its narrowed type independently.
2. HashMap.values for-loop: navigation_expression without call_suffix was
classified as bare property access (iterableName='values' instead of
'data'). Now tries object-as-iterable + property-as-method first, with
fallback to property-as-iterable for this.users patterns.
3. Kotlin parameter extraction: tree-sitter-kotlin parameter nodes use
positional children (simple_identifier, user_type) not named fields
(name, type). Added fallback to findChildByType in both
extractKotlinParameter and extractTypeBinding.
Integration tests added for .keys/.values/Set/MutableMap iteration,
3-arm when/is, multi-call within arms, and when+else branch.
* feat: enhance PHP type resolution for generics and member access in foreach loops
* feat: Phase 6.1 type resolution gap closure — container descriptors, recursive_pattern, class fields
Add 13 missing container type descriptors (Collection, MutableMap, Stream, SortedSet, etc.)
to CONTAINER_DESCRIPTORS for correct element type extraction across C#, Kotlin, and Java.
Extend C# pattern binding to handle recursive_pattern (obj is User { Name: "Alice" } u)
in both is-expression and switch expression contexts.
Add TypeScript class field declaration support (public_field_definition) so for-loop
iteration over this.fieldName resolves element types from class field type annotations.
Includes file-scope fallback in resolveIterableElementType and nested member_expression
handling for this.field.method() patterns.
* docs: add type resolution system documentation with roadmap
Covers the full architecture, resolution tiers (0-2), scope model,
language feature matrix, container descriptors, pipeline integration,
and the Phase 7-9 roadmap for cross-scope propagation, field-type
resolution, and return-type-aware binding.
* feat: Phase 6.2 review findings — C# nested member foreach, C++ deref range-for, Java field_access
Close two gaps found during fourth-pass review of PR #318:
- C# foreach (var user in this.data.Values): nested member_access_expression
now extracts intermediate property name for scopeEnv lookup
- C++ for (auto& user : *ptr): pointer_expression dereference now recognized
as range-for iterable
Root causes fixed in shared infrastructure:
- extractSimpleTypeName: add template_type (C++) and generic_name (C#)
- extractGenericTypeArgs: add generic_name for consistency
- type-env.ts: unwrap variable_declaration wrapper in field_declaration
for declarationTypeNodes capture (zero-allocation manual loop)
Additional review findings addressed:
- Java: add field_access handler for this.data.values() in method_invocation
- C++ pointer_expression: document limitation (*identifier only)
- TypeScript: fix stale comment about property_identifier
All 525 tests pass (278 unit + 247 integration).
* perf: optimize type resolution pipeline — worker threshold, skip graph phases, AST pruning
- Skip worker pool creation for small repos (<15 files or <512KB) — saves 100-400ms
- Add skipGraphPhases option to runPipelineFromRepo to skip MRO/community/process phases
- Add conservative SKIP_SUBTREE_TYPES for leaf-only AST nodes (string, comment, number)
- Pre-compute interestingNodeTypes set — single Set.has() replaces 3 checks per node
- Add fastStripNullable — skip full stripNullable for simple identifiers (90%+ case)
- Replace .children?.find() with manual for loops in extractFunctionName (no array alloc)
- Add hookTimeout: 120000 to vitest.config.ts for CI beforeAll hooks
* fix: review findings — remove template_string from SKIP_SUBTREE_TYPES, handle bare nullable keywords
- Remove template_string and concatenated_string from SKIP_SUBTREE_TYPES
(template literals contain interpolated expressions with typed code)
- Add FAST_NULLABLE_KEYWORDS check to fastStripNullable for behavioral
parity with stripNullable on bare null/undefined/void/None/nil
- Add explanatory comment on extractPendingAssignment scopeEnv guard
* feat: add type resolution system and roadmap documentation
|