gitnexus 1.6.3-rc.8 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/_shared/graph/types.d.ts +16 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +20 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +11 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/def-index.js +2 -2
- package/dist/_shared/scope-resolution/def-index.js.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +54 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/position-index.js +2 -2
- package/dist/_shared/scope-resolution/position-index.js.map +1 -1
- package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.js +3 -2
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +11 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.js +35 -4
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +31 -1
- package/dist/cli/clean.js +19 -1
- package/dist/cli/group.js +73 -0
- package/dist/cli/index-repo.js +8 -1
- package/dist/cli/index.js +26 -1
- package/dist/cli/list.js +11 -1
- package/dist/cli/remove.d.ts +30 -0
- package/dist/cli/remove.js +99 -0
- package/dist/cli/setup.js +185 -57
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +42 -0
- package/dist/config/ignore-service.d.ts +9 -0
- package/dist/config/ignore-service.js +80 -13
- package/dist/core/embedding-mode.d.ts +30 -0
- package/dist/core/embedding-mode.js +30 -0
- package/dist/core/embeddings/ast-utils.js +22 -22
- package/dist/core/embeddings/chunker.js +30 -25
- package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
- package/dist/core/embeddings/embedding-pipeline.js +15 -6
- package/dist/core/embeddings/text-generator.d.ts +1 -1
- package/dist/core/embeddings/text-generator.js +33 -24
- package/dist/core/embeddings/types.d.ts +43 -1
- package/dist/core/embeddings/types.js +101 -29
- package/dist/core/git-staleness.d.ts +18 -0
- package/dist/core/git-staleness.js +108 -0
- package/dist/core/graph/graph.js +115 -20
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/group/config-parser.d.ts +4 -0
- package/dist/core/group/config-parser.js +18 -1
- package/dist/core/group/cross-impact.d.ts +41 -0
- package/dist/core/group/cross-impact.js +441 -0
- package/dist/core/group/extractors/http-patterns/php.js +126 -18
- package/dist/core/group/group-path-utils.d.ts +17 -0
- package/dist/core/group/group-path-utils.js +40 -0
- package/dist/core/group/resolve-at-member.d.ts +10 -0
- package/dist/core/group/resolve-at-member.js +31 -0
- package/dist/core/group/service.d.ts +9 -0
- package/dist/core/group/service.js +259 -25
- package/dist/core/group/types.d.ts +30 -0
- package/dist/core/ingestion/ast-cache.d.ts +16 -1
- package/dist/core/ingestion/ast-cache.js +14 -2
- package/dist/core/ingestion/call-processor.js +9 -0
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/filesystem-walker.js +6 -4
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +139 -0
- package/dist/core/ingestion/framework-detection.js +6 -2
- package/dist/core/ingestion/import-processor.js +4 -0
- package/dist/core/ingestion/import-resolvers/python.js +9 -6
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -33
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
- package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
- package/dist/core/ingestion/languages/csharp/arity.js +37 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/captures.js +249 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
- package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
- package/dist/core/ingestion/languages/csharp/index.js +82 -0
- package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
- package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
- package/dist/core/ingestion/languages/csharp/query.js +515 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
- package/dist/core/ingestion/languages/csharp.js +14 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +106 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/python/import-target.js +99 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +9 -0
- package/dist/core/ingestion/languages/python/query.js +267 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
- package/dist/core/ingestion/languages/python.js +14 -0
- package/dist/core/ingestion/model/method-registry.d.ts +9 -0
- package/dist/core/ingestion/model/method-registry.js +4 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
- package/dist/core/ingestion/model/semantic-model.js +55 -0
- package/dist/core/ingestion/mro-processor.js +38 -22
- package/dist/core/ingestion/parsing-processor.d.ts +18 -1
- package/dist/core/ingestion/parsing-processor.js +45 -11
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
- package/dist/core/ingestion/registry-primary-flag.js +111 -0
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
- package/dist/core/ingestion/scope-extractor.d.ts +86 -0
- package/dist/core/ingestion/scope-extractor.js +758 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
- package/dist/core/ingestion/utils/ast-helpers.js +70 -0
- package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
- package/dist/core/ingestion/utils/max-file-size.js +52 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
- package/dist/core/ingestion/workers/parse-worker.js +57 -21
- package/dist/core/lbug/lbug-adapter.d.ts +22 -2
- package/dist/core/lbug/lbug-adapter.js +58 -14
- package/dist/core/lbug/pool-adapter.d.ts +17 -0
- package/dist/core/lbug/pool-adapter.js +24 -14
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +74 -19
- package/dist/core/search/bm25-index.d.ts +18 -0
- package/dist/core/search/bm25-index.js +125 -12
- package/dist/core/tree-sitter/parser-loader.js +6 -1
- package/dist/mcp/local/local-backend.d.ts +67 -3
- package/dist/mcp/local/local-backend.js +296 -34
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +100 -17
- package/dist/mcp/tools.d.ts +4 -1
- package/dist/mcp/tools.js +75 -54
- package/dist/server/api.js +6 -2
- package/dist/storage/git.d.ts +49 -0
- package/dist/storage/git.js +111 -0
- package/dist/storage/repo-manager.d.ts +246 -1
- package/dist/storage/repo-manager.js +391 -9
- package/package.json +7 -6
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
- package/skills/gitnexus-cli.md +1 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ScopeExtractor` — the central, source-agnostic driver that turns a
|
|
3
|
+
* language provider's `CaptureMatch[]` into a `ParsedFile`
|
|
4
|
+
* (RFC §5.3 + §3.2 Phase 1; Ring 2 PKG #919).
|
|
5
|
+
*
|
|
6
|
+
* Exactly one entry point: `extract(matches, filePath, provider) → ParsedFile`.
|
|
7
|
+
* Runs a five-pass pipeline over the matches. Each pass is internal; the
|
|
8
|
+
* public contract is the output `ParsedFile`.
|
|
9
|
+
*
|
|
10
|
+
* ## Design principles
|
|
11
|
+
*
|
|
12
|
+
* - **Source-agnostic.** Consumes `CaptureMatch[]` from providers;
|
|
13
|
+
* doesn't know whether they came from tree-sitter queries or COBOL's
|
|
14
|
+
* regex tagger. No `Tree` / `SyntaxNode` types leak into this file.
|
|
15
|
+
* - **One AST walk per language.** Providers do the AST walk inside
|
|
16
|
+
* their `emitScopeCaptures` hook; this driver does zero further
|
|
17
|
+
* traversal — it consumes captures only.
|
|
18
|
+
* - **Pure-ish.** The extractor itself is pure (same matches →
|
|
19
|
+
* same ParsedFile) when providers are pure. No side effects, no I/O.
|
|
20
|
+
* - **Centralized invariant enforcement.** Structural invariants on the
|
|
21
|
+
* scope tree (non-module has parent; parent contains child; siblings
|
|
22
|
+
* don't overlap) are enforced by `buildScopeTree` from Ring 2 SHARED
|
|
23
|
+
* (#912). Malformed inputs throw `ScopeTreeInvariantError`.
|
|
24
|
+
*
|
|
25
|
+
* ## The five passes
|
|
26
|
+
*
|
|
27
|
+
* 1. **Build scope tree.** Walk `@scope.*` matches. For each, consult
|
|
28
|
+
* `provider.resolveScopeKind` (default: suffix of the capture name).
|
|
29
|
+
* Derive parent by lexical-range containment. Hand the resulting
|
|
30
|
+
* `Scope[]` to `buildScopeTree` for validation.
|
|
31
|
+
* 2. **Attach declarations + local bindings.** Walk `@declaration.*`
|
|
32
|
+
* matches. For each, build a `SymbolDefinition` and attach it to
|
|
33
|
+
* `provider.bindingScopeFor` (default: innermost containing scope)
|
|
34
|
+
* as `ownedDefs` + a local `BindingRef { origin: 'local' }`.
|
|
35
|
+
* 3. **Collect raw imports.** Walk `@import.*` matches. Call
|
|
36
|
+
* `provider.interpretImport` per match; attach the returned
|
|
37
|
+
* `ParsedImport` to the ParsedFile (not to any `Scope` — finalize
|
|
38
|
+
* reconstructs the owning scope via `provider.importOwningScope`
|
|
39
|
+
* during Phase 2).
|
|
40
|
+
* 4. **Collect type bindings.** Walk `@type-binding.*` matches. Call
|
|
41
|
+
* `provider.interpretTypeBinding` per match. Attach the resulting
|
|
42
|
+
* `TypeRef` to the innermost containing scope's `typeBindings`
|
|
43
|
+
* (or override via `provider.bindingScopeFor` if set).
|
|
44
|
+
* 5. **Collect reference sites.** Walk `@reference.*` matches. Emit
|
|
45
|
+
* one `ReferenceSite` per match. Classify call form via
|
|
46
|
+
* `provider.classifyCallForm` (default: the capture's sub-tag if
|
|
47
|
+
* present; else `'free'`).
|
|
48
|
+
*
|
|
49
|
+
* ## What gets attached where
|
|
50
|
+
*
|
|
51
|
+
* - `Scope.bindings` — **local bindings only** at this stage (Pass 2).
|
|
52
|
+
* Finalize (#915) merges imports/wildcards on top.
|
|
53
|
+
* - `Scope.ownedDefs` — declarations structurally owned by this scope.
|
|
54
|
+
* - `Scope.typeBindings` — local type facts (parameter annotations, `self`).
|
|
55
|
+
* - `Scope.imports` — empty here. Populated by the finalize algorithm
|
|
56
|
+
* when it resolves `ParsedImport.targetRaw`.
|
|
57
|
+
* - `ParsedFile.parsedImports` — every raw import in this file.
|
|
58
|
+
* - `ParsedFile.localDefs` — flattened union of `Scope.ownedDefs`.
|
|
59
|
+
* - `ParsedFile.referenceSites` — pre-resolution usage facts.
|
|
60
|
+
*/
|
|
61
|
+
import { buildPositionIndex, buildScopeTree, makeScopeId } from '../../_shared/index.js';
|
|
62
|
+
// ─── Public entry point ─────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Drive the five extraction passes and return a `ParsedFile`.
|
|
65
|
+
*
|
|
66
|
+
* Throws `ScopeTreeInvariantError` (from #912) when the provider emits
|
|
67
|
+
* captures that violate structural scope invariants. The error surfaces
|
|
68
|
+
* upward rather than being silently corrected — a malformed capture set
|
|
69
|
+
* is a bug in the provider's `emitScopeCaptures`, not a data condition
|
|
70
|
+
* to tolerate.
|
|
71
|
+
*/
|
|
72
|
+
export function extract(matches, filePath, provider) {
|
|
73
|
+
// Partition matches by topic up front — one linear pass over the input.
|
|
74
|
+
const partitioned = partitionByTopic(matches);
|
|
75
|
+
// ── Pass 1: build the scope tree ─────────────────────────────────────
|
|
76
|
+
const scopeDrafts = pass1BuildScopes(partitioned.scope, filePath, provider);
|
|
77
|
+
const scopes = scopeDrafts.map(draftToScope);
|
|
78
|
+
// buildScopeTree validates invariants (throws on violation) and exposes
|
|
79
|
+
// the lookup contract consumed by Passes 2-5.
|
|
80
|
+
//
|
|
81
|
+
// **Snapshot semantics.** Both `scopeTree` and `positionIndex` are built
|
|
82
|
+
// from the post-Pass-1 `scopes` — parent/range/kind are accurate, but
|
|
83
|
+
// `bindings`, `ownedDefs`, and `typeBindings` are all empty here. Later
|
|
84
|
+
// passes write into the *drafts*, not into these snapshots; any hook
|
|
85
|
+
// that reads `scope.bindings` etc. via the `scopeTree` argument sees a
|
|
86
|
+
// structural view only. This is by design — hooks use scopeTree for
|
|
87
|
+
// "what's the parent chain?" queries, not for content queries.
|
|
88
|
+
const scopeTree = buildScopeTree(scopes);
|
|
89
|
+
const positionIndex = buildPositionIndex(scopes);
|
|
90
|
+
const moduleScope = scopeDrafts.find((s) => s.kind === 'Module');
|
|
91
|
+
if (moduleScope === undefined) {
|
|
92
|
+
throw new Error(`ScopeExtractor: no Module scope found for '${filePath}'. ` +
|
|
93
|
+
`Provider must emit at least one @scope.module capture per file.`);
|
|
94
|
+
}
|
|
95
|
+
// ── Pass 2: attach declarations + local bindings ────────────────────
|
|
96
|
+
const localDefs = [];
|
|
97
|
+
pass2AttachDeclarations(partitioned.declaration, scopeDrafts, positionIndex, localDefs, filePath, provider, scopeTree);
|
|
98
|
+
// ── Pass 3: collect raw imports ─────────────────────────────────────
|
|
99
|
+
const parsedImports = [];
|
|
100
|
+
pass3CollectImports(partitioned.import_, parsedImports, provider);
|
|
101
|
+
// ── Pass 4: collect type bindings ───────────────────────────────────
|
|
102
|
+
pass4CollectTypeBindings(partitioned.typeBinding, scopeDrafts, positionIndex, filePath, provider, scopeTree);
|
|
103
|
+
// ── Pass 5: collect reference sites ─────────────────────────────────
|
|
104
|
+
const referenceSites = [];
|
|
105
|
+
pass5CollectReferences(partitioned.reference, positionIndex, filePath, referenceSites, provider, scopeTree);
|
|
106
|
+
// Freeze Scope drafts into final shape and return.
|
|
107
|
+
const frozenScopes = scopeDrafts.map(draftToScope);
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
filePath,
|
|
110
|
+
moduleScope: moduleScope.id,
|
|
111
|
+
scopes: Object.freeze(frozenScopes),
|
|
112
|
+
parsedImports: Object.freeze(parsedImports.slice()),
|
|
113
|
+
localDefs: Object.freeze(localDefs.slice()),
|
|
114
|
+
referenceSites: Object.freeze(referenceSites.slice()),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Bucket each match by the topic of its anchor capture. The anchor is the
|
|
119
|
+
* capture whose name is prefixed with the match's topic (`@scope.*`,
|
|
120
|
+
* `@declaration.*`, `@import.*`, `@type-binding.*`, `@reference.*`).
|
|
121
|
+
*
|
|
122
|
+
* A match may contain additional captures (e.g., `@import.source`,
|
|
123
|
+
* `@declaration.class.name`) that are used by the provider hooks to
|
|
124
|
+
* decode details. Those live inside the `CaptureMatch` and are surfaced
|
|
125
|
+
* to hooks verbatim — the extractor itself only routes by anchor.
|
|
126
|
+
*/
|
|
127
|
+
function partitionByTopic(matches) {
|
|
128
|
+
const scope = [];
|
|
129
|
+
const declaration = [];
|
|
130
|
+
const import_ = [];
|
|
131
|
+
const typeBinding = [];
|
|
132
|
+
const reference = [];
|
|
133
|
+
for (const match of matches) {
|
|
134
|
+
const topic = topicOf(match);
|
|
135
|
+
switch (topic) {
|
|
136
|
+
case 'scope':
|
|
137
|
+
scope.push(match);
|
|
138
|
+
break;
|
|
139
|
+
case 'declaration':
|
|
140
|
+
declaration.push(match);
|
|
141
|
+
break;
|
|
142
|
+
case 'import':
|
|
143
|
+
import_.push(match);
|
|
144
|
+
break;
|
|
145
|
+
case 'type-binding':
|
|
146
|
+
typeBinding.push(match);
|
|
147
|
+
break;
|
|
148
|
+
case 'reference':
|
|
149
|
+
reference.push(match);
|
|
150
|
+
break;
|
|
151
|
+
case 'unknown':
|
|
152
|
+
// Unrecognized anchor — silently skip. Providers may emit extra
|
|
153
|
+
// captures (e.g., `@comment`) that the extractor has no topic for.
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { scope, declaration, import_, typeBinding, reference };
|
|
158
|
+
}
|
|
159
|
+
function topicOf(match) {
|
|
160
|
+
// The anchor is the capture whose name uses one of the known topic
|
|
161
|
+
// prefixes. For multi-capture matches, ALL captures share the topic;
|
|
162
|
+
// we pick the first matching key for efficiency.
|
|
163
|
+
for (const name of Object.keys(match)) {
|
|
164
|
+
if (name.startsWith('@scope.'))
|
|
165
|
+
return 'scope';
|
|
166
|
+
if (name.startsWith('@declaration.'))
|
|
167
|
+
return 'declaration';
|
|
168
|
+
if (name.startsWith('@import.'))
|
|
169
|
+
return 'import';
|
|
170
|
+
if (name.startsWith('@type-binding.'))
|
|
171
|
+
return 'type-binding';
|
|
172
|
+
if (name.startsWith('@reference.'))
|
|
173
|
+
return 'reference';
|
|
174
|
+
}
|
|
175
|
+
return 'unknown';
|
|
176
|
+
}
|
|
177
|
+
function draftToScope(draft) {
|
|
178
|
+
const frozenBindings = new Map();
|
|
179
|
+
for (const [name, refs] of draft.bindings) {
|
|
180
|
+
frozenBindings.set(name, Object.freeze(refs.slice()));
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
id: draft.id,
|
|
184
|
+
parent: draft.parent,
|
|
185
|
+
kind: draft.kind,
|
|
186
|
+
range: draft.range,
|
|
187
|
+
filePath: draft.filePath,
|
|
188
|
+
bindings: frozenBindings,
|
|
189
|
+
ownedDefs: Object.freeze(draft.ownedDefs.slice()),
|
|
190
|
+
imports: Object.freeze(draft.imports.slice()),
|
|
191
|
+
typeBindings: new Map(draft.typeBindings),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// ─── Pass 1: build scope tree ──────────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Convert `@scope.*` matches into `ScopeDraft[]`. Parent relationships
|
|
197
|
+
* are derived from range containment (outermost scope containing `range`
|
|
198
|
+
* becomes the parent).
|
|
199
|
+
*/
|
|
200
|
+
function pass1BuildScopes(matches, filePath, provider) {
|
|
201
|
+
const candidates = [];
|
|
202
|
+
for (const match of matches) {
|
|
203
|
+
const anchor = anchorCaptureFor(match, '@scope.');
|
|
204
|
+
if (anchor === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
const kind = resolveKindForScopeMatch(match, anchor, provider);
|
|
207
|
+
if (kind === null)
|
|
208
|
+
continue;
|
|
209
|
+
const id = makeScopeId({ filePath, range: anchor.range, kind });
|
|
210
|
+
candidates.push({ match, range: anchor.range, kind, id });
|
|
211
|
+
}
|
|
212
|
+
// Sort by (startLine, startCol) ASC, (endLine, endCol) DESC so outer
|
|
213
|
+
// scopes appear before their children for parent-resolution.
|
|
214
|
+
candidates.sort((a, b) => {
|
|
215
|
+
if (a.range.startLine !== b.range.startLine)
|
|
216
|
+
return a.range.startLine - b.range.startLine;
|
|
217
|
+
if (a.range.startCol !== b.range.startCol)
|
|
218
|
+
return a.range.startCol - b.range.startCol;
|
|
219
|
+
if (a.range.endLine !== b.range.endLine)
|
|
220
|
+
return b.range.endLine - a.range.endLine;
|
|
221
|
+
return b.range.endCol - a.range.endCol;
|
|
222
|
+
});
|
|
223
|
+
const drafts = [];
|
|
224
|
+
const stack = []; // enclosing real scopes, outermost at [0]
|
|
225
|
+
for (const cand of candidates) {
|
|
226
|
+
// Pop the stack until the top strictly contains this candidate.
|
|
227
|
+
while (stack.length > 0 && !rangeStrictlyContains(stack[stack.length - 1].range, cand.range)) {
|
|
228
|
+
stack.pop();
|
|
229
|
+
}
|
|
230
|
+
const parent = stack.length > 0 ? stack[stack.length - 1].id : null;
|
|
231
|
+
drafts.push(makeDraft(cand.id, parent, cand.kind, cand.range, filePath));
|
|
232
|
+
stack.push(cand);
|
|
233
|
+
}
|
|
234
|
+
return drafts;
|
|
235
|
+
}
|
|
236
|
+
function resolveKindForScopeMatch(match, anchor, provider) {
|
|
237
|
+
// Provider override takes precedence.
|
|
238
|
+
const override = provider.resolveScopeKind?.(match);
|
|
239
|
+
if (override !== undefined && override !== null)
|
|
240
|
+
return override;
|
|
241
|
+
// Default: derive from capture name suffix (`@scope.function` → 'Function').
|
|
242
|
+
const suffix = anchor.name.slice('@scope.'.length);
|
|
243
|
+
switch (suffix.toLowerCase()) {
|
|
244
|
+
case 'module':
|
|
245
|
+
return 'Module';
|
|
246
|
+
case 'namespace':
|
|
247
|
+
return 'Namespace';
|
|
248
|
+
case 'class':
|
|
249
|
+
return 'Class';
|
|
250
|
+
case 'function':
|
|
251
|
+
return 'Function';
|
|
252
|
+
case 'block':
|
|
253
|
+
return 'Block';
|
|
254
|
+
case 'expression':
|
|
255
|
+
return 'Expression';
|
|
256
|
+
default:
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function makeDraft(id, parent, kind, range, filePath) {
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
parent,
|
|
264
|
+
kind,
|
|
265
|
+
range,
|
|
266
|
+
filePath,
|
|
267
|
+
bindings: new Map(),
|
|
268
|
+
ownedDefs: [],
|
|
269
|
+
imports: [],
|
|
270
|
+
typeBindings: new Map(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// ─── Pass 2: attach declarations + local bindings ──────────────────────────
|
|
274
|
+
function pass2AttachDeclarations(matches, drafts, positionIndex, localDefs, filePath, provider, scopeTree) {
|
|
275
|
+
const draftById = new Map();
|
|
276
|
+
for (const d of drafts)
|
|
277
|
+
draftById.set(d.id, d);
|
|
278
|
+
for (const match of matches) {
|
|
279
|
+
const anchor = anchorCaptureFor(match, '@declaration.');
|
|
280
|
+
if (anchor === undefined)
|
|
281
|
+
continue;
|
|
282
|
+
const def = buildDefFromDeclarationMatch(match, anchor, filePath);
|
|
283
|
+
if (def === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
// Find the innermost scope that contains the declaration's anchor range.
|
|
286
|
+
const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
|
|
287
|
+
if (innermostId === undefined)
|
|
288
|
+
continue;
|
|
289
|
+
const innermost = draftById.get(innermostId);
|
|
290
|
+
if (innermost === undefined)
|
|
291
|
+
continue;
|
|
292
|
+
// Ownership: attach the def to the innermost scope's `ownedDefs` — that
|
|
293
|
+
// is the structural owner. `def.ownerId` is NOT populated here — the
|
|
294
|
+
// extractor has no clean path to the parent's own DefId mid-extraction
|
|
295
|
+
// (the parent declaration may not yet have been processed, or may live
|
|
296
|
+
// in a different scope entirely). Providers that need `ownerId` should
|
|
297
|
+
// set it directly from the declaration hook (e.g., derive from the
|
|
298
|
+
// `@declaration.owner` capture or the parent scope id); otherwise
|
|
299
|
+
// `finalize` populates method/field `ownerId` via `MethodDispatchIndex`
|
|
300
|
+
// (#914) in a follow-up pass that sees every def already in place.
|
|
301
|
+
innermost.ownedDefs.push(def);
|
|
302
|
+
localDefs.push(def);
|
|
303
|
+
// Binding visibility: default to innermost; allow hoisting via
|
|
304
|
+
// `provider.bindingScopeFor`. `draftToScope(innermost)` here is a
|
|
305
|
+
// **structural** snapshot — parent/range/kind only. Hooks MUST NOT
|
|
306
|
+
// rely on `scope.bindings`, `ownedDefs`, or `typeBindings` being
|
|
307
|
+
// populated during Pass 2: those fields are written across passes,
|
|
308
|
+
// so reading them mid-extraction yields a partial view. The
|
|
309
|
+
// `scopeTree` argument is similarly snapshot-before-mutation.
|
|
310
|
+
//
|
|
311
|
+
// Auto-hoist for scope-creating declarations: when the declaration's
|
|
312
|
+
// anchor range is the same node that produced `innermost` (e.g. a
|
|
313
|
+
// `function_definition` is both `@scope.function` and the
|
|
314
|
+
// `@declaration.function` anchor), the name is visible OUTSIDE the
|
|
315
|
+
// body, not inside. Hoisting to the parent scope is what every
|
|
316
|
+
// mainstream language wants for function/class declarations. Hooks
|
|
317
|
+
// can override by returning a non-null scope id.
|
|
318
|
+
const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
|
|
319
|
+
? innermost.parent
|
|
320
|
+
: innermost.id;
|
|
321
|
+
const bindingScopeId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
|
|
322
|
+
const bindingHost = draftById.get(bindingScopeId) ?? innermost;
|
|
323
|
+
const nameKey = deriveDeclarationName(match, def);
|
|
324
|
+
if (nameKey === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
const existing = bindingHost.bindings.get(nameKey) ?? [];
|
|
327
|
+
existing.push({ def, origin: 'local' });
|
|
328
|
+
bindingHost.bindings.set(nameKey, existing);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function buildDefFromDeclarationMatch(match, anchor, filePath) {
|
|
332
|
+
// Anchor name pattern: `@declaration.<kind>` where <kind> maps to NodeLabel.
|
|
333
|
+
const kindStr = anchor.name.slice('@declaration.'.length);
|
|
334
|
+
const type = normalizeNodeLabel(kindStr);
|
|
335
|
+
if (type === undefined)
|
|
336
|
+
return undefined;
|
|
337
|
+
const nameCap = match['@declaration.name'] ?? match[`@declaration.${kindStr}.name`] ?? match[anchor.name];
|
|
338
|
+
if (nameCap === undefined)
|
|
339
|
+
return undefined;
|
|
340
|
+
const qualifiedCap = match['@declaration.qualified_name'];
|
|
341
|
+
const qualifiedName = qualifiedCap?.text;
|
|
342
|
+
// Optional arity metadata — producers (e.g. Python emit-captures)
|
|
343
|
+
// synthesize these on function/method declarations. Their absence is
|
|
344
|
+
// the normal case for other producers; readers treat undefined as
|
|
345
|
+
// "unknown" per `SymbolDefinition` contract.
|
|
346
|
+
const parameterCount = parseIntCapture(match['@declaration.parameter-count']);
|
|
347
|
+
const requiredParameterCount = parseIntCapture(match['@declaration.required-parameter-count']);
|
|
348
|
+
const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
|
|
349
|
+
return {
|
|
350
|
+
nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
|
|
351
|
+
filePath,
|
|
352
|
+
type,
|
|
353
|
+
...(qualifiedName !== undefined ? { qualifiedName } : { qualifiedName: nameCap.text }),
|
|
354
|
+
...(parameterCount !== undefined ? { parameterCount } : {}),
|
|
355
|
+
...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
|
|
356
|
+
...(parameterTypes !== undefined ? { parameterTypes } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function parseIntCapture(cap) {
|
|
360
|
+
if (cap === undefined)
|
|
361
|
+
return undefined;
|
|
362
|
+
const n = Number.parseInt(cap.text, 10);
|
|
363
|
+
return Number.isFinite(n) ? n : undefined;
|
|
364
|
+
}
|
|
365
|
+
function parseJsonStringArrayCapture(cap) {
|
|
366
|
+
if (cap === undefined)
|
|
367
|
+
return undefined;
|
|
368
|
+
try {
|
|
369
|
+
const parsed = JSON.parse(cap.text);
|
|
370
|
+
if (!Array.isArray(parsed))
|
|
371
|
+
return undefined;
|
|
372
|
+
return parsed.every((x) => typeof x === 'string') ? parsed : undefined;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function deriveDeclarationName(match, def) {
|
|
379
|
+
const nameCap = match['@declaration.name'] ??
|
|
380
|
+
match[Object.keys(match).find((k) => k.startsWith('@declaration.') && k.endsWith('.name')) ?? ''];
|
|
381
|
+
if (nameCap !== undefined)
|
|
382
|
+
return nameCap.text;
|
|
383
|
+
// Fall back to qualifiedName tail.
|
|
384
|
+
const q = def.qualifiedName;
|
|
385
|
+
if (q !== undefined && q.length > 0) {
|
|
386
|
+
const dot = q.lastIndexOf('.');
|
|
387
|
+
return dot === -1 ? q : q.slice(dot + 1);
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Map a lower-case declaration kind (from `@declaration.<kind>`) to a
|
|
393
|
+
* graph `NodeLabel`. Silently returns `undefined` for kinds we don't
|
|
394
|
+
* recognize — providers can emit richer captures without breaking the
|
|
395
|
+
* driver.
|
|
396
|
+
*/
|
|
397
|
+
function normalizeNodeLabel(kindStr) {
|
|
398
|
+
switch (kindStr.toLowerCase()) {
|
|
399
|
+
case 'class':
|
|
400
|
+
return 'Class';
|
|
401
|
+
case 'interface':
|
|
402
|
+
return 'Interface';
|
|
403
|
+
case 'enum':
|
|
404
|
+
return 'Enum';
|
|
405
|
+
case 'struct':
|
|
406
|
+
return 'Struct';
|
|
407
|
+
case 'union':
|
|
408
|
+
return 'Union';
|
|
409
|
+
case 'trait':
|
|
410
|
+
return 'Trait';
|
|
411
|
+
case 'method':
|
|
412
|
+
return 'Method';
|
|
413
|
+
case 'function':
|
|
414
|
+
return 'Function';
|
|
415
|
+
case 'constructor':
|
|
416
|
+
return 'Constructor';
|
|
417
|
+
case 'field':
|
|
418
|
+
case 'property':
|
|
419
|
+
return 'Property';
|
|
420
|
+
case 'variable':
|
|
421
|
+
case 'const':
|
|
422
|
+
return 'Variable';
|
|
423
|
+
case 'typealias':
|
|
424
|
+
case 'type_alias':
|
|
425
|
+
return 'TypeAlias';
|
|
426
|
+
case 'typedef':
|
|
427
|
+
return 'Typedef';
|
|
428
|
+
case 'record':
|
|
429
|
+
return 'Record';
|
|
430
|
+
case 'delegate':
|
|
431
|
+
return 'Delegate';
|
|
432
|
+
case 'annotation':
|
|
433
|
+
return 'Annotation';
|
|
434
|
+
case 'namespace':
|
|
435
|
+
return 'Namespace';
|
|
436
|
+
default:
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function makeDefId(filePath, range, type, name) {
|
|
441
|
+
return `def:${filePath}#${range.startLine}:${range.startCol}:${type}:${name}`;
|
|
442
|
+
}
|
|
443
|
+
// ─── Pass 3: collect raw imports ───────────────────────────────────────────
|
|
444
|
+
function pass3CollectImports(matches, parsedImports, provider) {
|
|
445
|
+
if (provider.interpretImport === undefined)
|
|
446
|
+
return;
|
|
447
|
+
for (const match of matches) {
|
|
448
|
+
const anchor = anchorCaptureFor(match, '@import.');
|
|
449
|
+
if (anchor === undefined)
|
|
450
|
+
continue;
|
|
451
|
+
const parsed = provider.interpretImport(match);
|
|
452
|
+
if (parsed === null)
|
|
453
|
+
continue;
|
|
454
|
+
parsedImports.push(parsed);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ─── Pass 4: collect type bindings ─────────────────────────────────────────
|
|
458
|
+
function pass4CollectTypeBindings(matches, drafts, positionIndex, filePath, provider, scopeTree) {
|
|
459
|
+
const draftById = new Map();
|
|
460
|
+
for (const d of drafts)
|
|
461
|
+
draftById.set(d.id, d);
|
|
462
|
+
for (const match of matches) {
|
|
463
|
+
const anchor = anchorCaptureFor(match, '@type-binding.');
|
|
464
|
+
if (anchor === undefined)
|
|
465
|
+
continue;
|
|
466
|
+
const parsed = provider.interpretTypeBinding?.(match);
|
|
467
|
+
if (parsed === null || parsed === undefined)
|
|
468
|
+
continue;
|
|
469
|
+
const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
|
|
470
|
+
if (innermostId === undefined)
|
|
471
|
+
continue;
|
|
472
|
+
const innermost = draftById.get(innermostId);
|
|
473
|
+
if (innermost === undefined)
|
|
474
|
+
continue;
|
|
475
|
+
// Auto-hoist for scope-creating type bindings (e.g. Python's
|
|
476
|
+
// `@type-binding.return` whose anchor is the function_definition
|
|
477
|
+
// itself). Same condition as Pass 2 — when the anchor coincides
|
|
478
|
+
// with the innermost scope's range, the binding belongs in the
|
|
479
|
+
// enclosing scope (callers, not the function body, look up the
|
|
480
|
+
// return type by the function's name).
|
|
481
|
+
const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
|
|
482
|
+
? innermost.parent
|
|
483
|
+
: innermost.id;
|
|
484
|
+
// `bindingScopeFor` may hoist the type binding to an outer scope.
|
|
485
|
+
const hostId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
|
|
486
|
+
const host = draftById.get(hostId) ?? innermost;
|
|
487
|
+
const typeRef = {
|
|
488
|
+
rawName: parsed.rawTypeName,
|
|
489
|
+
declaredAtScope: host.id,
|
|
490
|
+
source: parsed.source,
|
|
491
|
+
};
|
|
492
|
+
// Prefer stronger sources when multiple matches fire for the same
|
|
493
|
+
// bound name in the same scope. Example: `u: User = find()` matches
|
|
494
|
+
// both the annotation and constructor-inferred patterns; the explicit
|
|
495
|
+
// annotation (stronger source) must win over the call-site guess
|
|
496
|
+
// regardless of query-match arrival order.
|
|
497
|
+
const existing = host.typeBindings.get(parsed.boundName);
|
|
498
|
+
if (existing === undefined ||
|
|
499
|
+
typeBindingStrength(typeRef.source) >= typeBindingStrength(existing.source)) {
|
|
500
|
+
host.typeBindings.set(parsed.boundName, typeRef);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// ── Transitive closure over identifier-chain type bindings ─────────
|
|
504
|
+
// Captures like `(assignment left: (ident) right: (ident))` emit a
|
|
505
|
+
// TypeRef whose `rawName` is the RHS identifier. When the RHS name is
|
|
506
|
+
// itself a bound variable with a known type in the same scope (or a
|
|
507
|
+
// parent scope), follow the chain so `alias` ultimately points at the
|
|
508
|
+
// class type — not at another local variable name. Without this,
|
|
509
|
+
// `resolveTypeRef` hits the chained name, sees it's a local Variable
|
|
510
|
+
// (non-type kind), and strict-returns null.
|
|
511
|
+
for (const draft of drafts) {
|
|
512
|
+
for (const [name, ref] of draft.typeBindings) {
|
|
513
|
+
const resolved = followChainedRef(ref, draftById);
|
|
514
|
+
if (resolved !== ref)
|
|
515
|
+
draft.typeBindings.set(name, resolved);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Max chain depth: practical programs rarely exceed 4-5 re-bindings;
|
|
520
|
+
* the cap just prevents runaway loops when providers emit cycles. */
|
|
521
|
+
const CHAIN_MAX_DEPTH = 16;
|
|
522
|
+
/**
|
|
523
|
+
* Follow an identifier-chain TypeRef through successive typeBindings
|
|
524
|
+
* lookups in the declaring scope and its ancestors. Returns the terminal
|
|
525
|
+
* TypeRef (or the original if the chain dead-ends or cycles).
|
|
526
|
+
*/
|
|
527
|
+
function followChainedRef(start, draftById) {
|
|
528
|
+
let current = start;
|
|
529
|
+
const visited = new Set();
|
|
530
|
+
for (let depth = 0; depth < CHAIN_MAX_DEPTH; depth++) {
|
|
531
|
+
// A rawName containing a dot (`models.User`) goes through
|
|
532
|
+
// `QualifiedNameIndex` at resolution time — don't follow it here.
|
|
533
|
+
if (current.rawName.includes('.'))
|
|
534
|
+
return current;
|
|
535
|
+
// Look up the current rawName in the declaring scope and walk up
|
|
536
|
+
// the chain until we hit a scope that has a binding for it.
|
|
537
|
+
let scopeId = current.declaredAtScope;
|
|
538
|
+
let next;
|
|
539
|
+
while (scopeId !== null) {
|
|
540
|
+
const scope = draftById.get(scopeId);
|
|
541
|
+
if (scope === undefined)
|
|
542
|
+
break;
|
|
543
|
+
next = scope.typeBindings.get(current.rawName);
|
|
544
|
+
if (next !== undefined)
|
|
545
|
+
break;
|
|
546
|
+
scopeId = scope.parent;
|
|
547
|
+
}
|
|
548
|
+
if (next === undefined)
|
|
549
|
+
return current; // dead end — nothing to chain to
|
|
550
|
+
if (next === current)
|
|
551
|
+
return current; // self-ref
|
|
552
|
+
if (visited.has(next.rawName))
|
|
553
|
+
return current; // cycle guard
|
|
554
|
+
visited.add(next.rawName);
|
|
555
|
+
current = next;
|
|
556
|
+
}
|
|
557
|
+
return current;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Priority ordering when multiple `TypeRef`s compete for the same bound
|
|
561
|
+
* name in the same scope. Higher number wins; ties keep the later match
|
|
562
|
+
* (last-write-wins preserves historical order within a tier).
|
|
563
|
+
*
|
|
564
|
+
* Rationale: explicit annotations always beat inferred ones because they
|
|
565
|
+
* reflect user intent. `self`/`cls` are treated as strongly as annotations
|
|
566
|
+
* because they are language-required receiver types.
|
|
567
|
+
*/
|
|
568
|
+
function typeBindingStrength(source) {
|
|
569
|
+
switch (source) {
|
|
570
|
+
case 'annotation':
|
|
571
|
+
case 'parameter-annotation':
|
|
572
|
+
case 'return-annotation':
|
|
573
|
+
case 'self':
|
|
574
|
+
return 2;
|
|
575
|
+
case 'assignment-inferred':
|
|
576
|
+
case 'constructor-inferred':
|
|
577
|
+
case 'receiver-propagated':
|
|
578
|
+
return 1;
|
|
579
|
+
default:
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ─── Pass 5: collect reference sites ───────────────────────────────────────
|
|
584
|
+
function pass5CollectReferences(matches, positionIndex, filePath, referenceSites, provider, scopeTree) {
|
|
585
|
+
for (const match of matches) {
|
|
586
|
+
const anchor = anchorCaptureFor(match, '@reference.');
|
|
587
|
+
if (anchor === undefined)
|
|
588
|
+
continue;
|
|
589
|
+
const kind = referenceKindFromAnchor(anchor.name);
|
|
590
|
+
if (kind === undefined)
|
|
591
|
+
continue;
|
|
592
|
+
const nameCap = match['@reference.name'] ?? anchor;
|
|
593
|
+
const inScopeId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
|
|
594
|
+
if (inScopeId === undefined)
|
|
595
|
+
continue;
|
|
596
|
+
const callForm = kind === 'call'
|
|
597
|
+
? classifyCallFormForMatch(match, anchor.name, provider, scopeTree, inScopeId)
|
|
598
|
+
: undefined;
|
|
599
|
+
const explicitReceiver = extractExplicitReceiver(match);
|
|
600
|
+
const arity = extractArity(match);
|
|
601
|
+
const argumentTypes = extractArgumentTypes(match);
|
|
602
|
+
const site = {
|
|
603
|
+
name: nameCap.text,
|
|
604
|
+
atRange: anchor.range,
|
|
605
|
+
inScope: inScopeId,
|
|
606
|
+
kind,
|
|
607
|
+
...(callForm !== undefined ? { callForm } : {}),
|
|
608
|
+
...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
|
|
609
|
+
...(arity !== undefined ? { arity } : {}),
|
|
610
|
+
...(argumentTypes !== undefined ? { argumentTypes } : {}),
|
|
611
|
+
};
|
|
612
|
+
referenceSites.push(site);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function referenceKindFromAnchor(name) {
|
|
616
|
+
const suffix = name.slice('@reference.'.length);
|
|
617
|
+
// Strip sub-tag after the kind (`@reference.call.member` → `call`).
|
|
618
|
+
const firstDot = suffix.indexOf('.');
|
|
619
|
+
const head = firstDot === -1 ? suffix : suffix.slice(0, firstDot);
|
|
620
|
+
switch (head.toLowerCase()) {
|
|
621
|
+
case 'call':
|
|
622
|
+
return 'call';
|
|
623
|
+
case 'read':
|
|
624
|
+
return 'read';
|
|
625
|
+
case 'write':
|
|
626
|
+
return 'write';
|
|
627
|
+
case 'type':
|
|
628
|
+
case 'type_reference':
|
|
629
|
+
return 'type-reference';
|
|
630
|
+
case 'inherits':
|
|
631
|
+
return 'inherits';
|
|
632
|
+
case 'import_use':
|
|
633
|
+
case 'import-use':
|
|
634
|
+
return 'import-use';
|
|
635
|
+
default:
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function classifyCallFormForMatch(match, anchorName, provider, scopeTree, inScopeId) {
|
|
640
|
+
// Declarative sub-tag path first: `@reference.call.member` → 'member'.
|
|
641
|
+
const suffix = anchorName.slice('@reference.call.'.length);
|
|
642
|
+
switch (suffix.toLowerCase()) {
|
|
643
|
+
case 'free':
|
|
644
|
+
return 'free';
|
|
645
|
+
case 'member':
|
|
646
|
+
return 'member';
|
|
647
|
+
case 'constructor':
|
|
648
|
+
return 'constructor';
|
|
649
|
+
case 'index':
|
|
650
|
+
return 'index';
|
|
651
|
+
}
|
|
652
|
+
// Hook-based path: provider knows.
|
|
653
|
+
const hook = provider.classifyCallForm;
|
|
654
|
+
if (hook !== undefined) {
|
|
655
|
+
const scope = scopeTree.getScope(inScopeId);
|
|
656
|
+
if (scope !== undefined)
|
|
657
|
+
return hook(match, scope);
|
|
658
|
+
}
|
|
659
|
+
return 'free';
|
|
660
|
+
}
|
|
661
|
+
function extractExplicitReceiver(match) {
|
|
662
|
+
const cap = match['@reference.receiver'];
|
|
663
|
+
if (cap === undefined)
|
|
664
|
+
return undefined;
|
|
665
|
+
return { name: cap.text };
|
|
666
|
+
}
|
|
667
|
+
function extractArity(match) {
|
|
668
|
+
const cap = match['@reference.arity'];
|
|
669
|
+
if (cap === undefined)
|
|
670
|
+
return undefined;
|
|
671
|
+
const n = Number.parseInt(cap.text, 10);
|
|
672
|
+
return Number.isFinite(n) ? n : undefined;
|
|
673
|
+
}
|
|
674
|
+
function extractArgumentTypes(match) {
|
|
675
|
+
const cap = match['@reference.parameter-types'];
|
|
676
|
+
if (cap === undefined)
|
|
677
|
+
return undefined;
|
|
678
|
+
try {
|
|
679
|
+
const parsed = JSON.parse(cap.text);
|
|
680
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === 'string'))
|
|
681
|
+
return parsed;
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
/* malformed — fall through */
|
|
685
|
+
}
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
// ─── Internal: range + capture utilities ───────────────────────────────────
|
|
689
|
+
function rangesEqual(a, b) {
|
|
690
|
+
return (a.startLine === b.startLine &&
|
|
691
|
+
a.startCol === b.startCol &&
|
|
692
|
+
a.endLine === b.endLine &&
|
|
693
|
+
a.endCol === b.endCol);
|
|
694
|
+
}
|
|
695
|
+
function rangeStrictlyContains(outer, inner) {
|
|
696
|
+
if (outer.startLine === inner.startLine &&
|
|
697
|
+
outer.startCol === inner.startCol &&
|
|
698
|
+
outer.endLine === inner.endLine &&
|
|
699
|
+
outer.endCol === inner.endCol) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
const startsBefore = outer.startLine < inner.startLine ||
|
|
703
|
+
(outer.startLine === inner.startLine && outer.startCol <= inner.startCol);
|
|
704
|
+
const endsAfter = outer.endLine > inner.endLine ||
|
|
705
|
+
(outer.endLine === inner.endLine && outer.endCol >= inner.endCol);
|
|
706
|
+
return startsBefore && endsAfter;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Capture names that are never anchors — they are sub-tags nested inside a
|
|
710
|
+
* larger anchor (e.g., the receiver expression inside a `@reference.call`
|
|
711
|
+
* may span more source than the called name, but is not the call itself).
|
|
712
|
+
*
|
|
713
|
+
* The list is maintained here centrally rather than per-pass because the
|
|
714
|
+
* set is small and stable; adding a new sub-tag convention is a one-line
|
|
715
|
+
* change.
|
|
716
|
+
*/
|
|
717
|
+
const KNOWN_SUB_TAGS = new Set([
|
|
718
|
+
'@declaration.name',
|
|
719
|
+
'@declaration.qualified_name',
|
|
720
|
+
'@import.name',
|
|
721
|
+
'@import.source',
|
|
722
|
+
'@import.alias',
|
|
723
|
+
'@type-binding.name',
|
|
724
|
+
'@type-binding.type',
|
|
725
|
+
'@reference.name',
|
|
726
|
+
'@reference.receiver',
|
|
727
|
+
'@reference.arity',
|
|
728
|
+
'@reference.parameter-types',
|
|
729
|
+
'@declaration.parameter-count',
|
|
730
|
+
'@declaration.required-parameter-count',
|
|
731
|
+
'@declaration.parameter-types',
|
|
732
|
+
]);
|
|
733
|
+
/**
|
|
734
|
+
* Return the anchor capture for a match — the one whose name begins with
|
|
735
|
+
* `prefix` AND is not in the known-sub-tag set. When multiple candidates
|
|
736
|
+
* remain, the broadest-ranged one wins: tree-sitter queries often tag
|
|
737
|
+
* both a whole statement and a sub-token under the same topic
|
|
738
|
+
* (`@scope.function` + `@scope.function.name`); the anchor is the
|
|
739
|
+
* statement-level one.
|
|
740
|
+
*/
|
|
741
|
+
function anchorCaptureFor(match, prefix) {
|
|
742
|
+
let best;
|
|
743
|
+
let bestSpan = -1;
|
|
744
|
+
for (const name of Object.keys(match)) {
|
|
745
|
+
if (!name.startsWith(prefix))
|
|
746
|
+
continue;
|
|
747
|
+
if (KNOWN_SUB_TAGS.has(name))
|
|
748
|
+
continue;
|
|
749
|
+
const cap = match[name];
|
|
750
|
+
const span = (cap.range.endLine - cap.range.startLine) * 1_000_000 +
|
|
751
|
+
(cap.range.endCol - cap.range.startCol);
|
|
752
|
+
if (span > bestSpan) {
|
|
753
|
+
bestSpan = span;
|
|
754
|
+
best = cap;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return best;
|
|
758
|
+
}
|