gitnexus 1.4.7 → 1.4.9
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 +29 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index-repo.d.ts +15 -0
- package/dist/cli/index-repo.js +115 -0
- package/dist/cli/index.js +13 -3
- package/dist/cli/setup.js +90 -10
- package/dist/cli/wiki.d.ts +4 -0
- package/dist/cli/wiki.js +174 -53
- package/dist/config/supported-languages.d.ts +33 -1
- package/dist/config/supported-languages.js +32 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/graph.js +9 -1
- package/dist/core/graph/types.d.ts +11 -2
- package/dist/core/ingestion/call-processor.d.ts +66 -2
- package/dist/core/ingestion/call-processor.js +650 -30
- package/dist/core/ingestion/call-routing.d.ts +9 -18
- package/dist/core/ingestion/call-routing.js +0 -19
- package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
- package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
- package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
- package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
- package/dist/core/ingestion/cobol-processor.d.ts +54 -0
- package/dist/core/ingestion/cobol-processor.js +1186 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
- package/dist/core/ingestion/entry-point-scoring.js +52 -28
- package/dist/core/ingestion/export-detection.d.ts +47 -8
- package/dist/core/ingestion/export-detection.js +29 -50
- package/dist/core/ingestion/field-extractor.d.ts +29 -0
- package/dist/core/ingestion/field-extractor.js +25 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
- package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
- package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
- package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
- package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
- package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
- package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
- package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
- package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
- package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
- package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
- package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
- package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
- package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
- package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
- package/dist/core/ingestion/field-extractors/generic.js +111 -0
- package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
- package/dist/core/ingestion/field-extractors/typescript.js +291 -0
- package/dist/core/ingestion/field-types.d.ts +59 -0
- package/dist/core/ingestion/field-types.js +2 -0
- package/dist/core/ingestion/framework-detection.d.ts +97 -2
- package/dist/core/ingestion/framework-detection.js +114 -14
- package/dist/core/ingestion/heritage-processor.js +62 -66
- package/dist/core/ingestion/import-processor.d.ts +9 -10
- package/dist/core/ingestion/import-processor.js +150 -196
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
- package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
- package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/dart.js +44 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
- package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
- package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
- package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
- package/dist/core/ingestion/import-resolvers/php.js +80 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
- package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
- package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
- package/dist/core/ingestion/import-resolvers/swift.js +23 -0
- package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
- package/dist/core/ingestion/import-resolvers/types.js +6 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
- package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
- package/dist/core/ingestion/language-config.d.ts +6 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/language-provider.d.ts +121 -0
- package/dist/core/ingestion/language-provider.js +24 -0
- package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
- package/dist/core/ingestion/languages/c-cpp.js +71 -0
- package/dist/core/ingestion/languages/cobol.d.ts +1 -0
- package/dist/core/ingestion/languages/cobol.js +26 -0
- package/dist/core/ingestion/languages/csharp.d.ts +8 -0
- package/dist/core/ingestion/languages/csharp.js +49 -0
- package/dist/core/ingestion/languages/dart.d.ts +12 -0
- package/dist/core/ingestion/languages/dart.js +58 -0
- package/dist/core/ingestion/languages/go.d.ts +11 -0
- package/dist/core/ingestion/languages/go.js +28 -0
- package/dist/core/ingestion/languages/index.d.ts +38 -0
- package/dist/core/ingestion/languages/index.js +63 -0
- package/dist/core/ingestion/languages/java.d.ts +9 -0
- package/dist/core/ingestion/languages/java.js +29 -0
- package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
- package/dist/core/ingestion/languages/kotlin.js +53 -0
- package/dist/core/ingestion/languages/php.d.ts +8 -0
- package/dist/core/ingestion/languages/php.js +145 -0
- package/dist/core/ingestion/languages/python.d.ts +12 -0
- package/dist/core/ingestion/languages/python.js +39 -0
- package/dist/core/ingestion/languages/ruby.d.ts +9 -0
- package/dist/core/ingestion/languages/ruby.js +44 -0
- package/dist/core/ingestion/languages/rust.d.ts +12 -0
- package/dist/core/ingestion/languages/rust.js +44 -0
- package/dist/core/ingestion/languages/swift.d.ts +12 -0
- package/dist/core/ingestion/languages/swift.js +133 -0
- package/dist/core/ingestion/languages/typescript.d.ts +10 -0
- package/dist/core/ingestion/languages/typescript.js +60 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +22 -18
- package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
- package/dist/core/ingestion/named-binding-processor.js +42 -0
- package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/csharp.js +37 -0
- package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/java.js +29 -0
- package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
- package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/php.js +61 -0
- package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/python.js +49 -0
- package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/rust.js +64 -0
- package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
- package/dist/core/ingestion/named-bindings/types.js +6 -0
- package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
- package/dist/core/ingestion/named-bindings/typescript.js +58 -0
- package/dist/core/ingestion/parsing-processor.d.ts +6 -2
- package/dist/core/ingestion/parsing-processor.js +125 -85
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +1235 -317
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +8 -5
- package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
- package/dist/core/ingestion/route-extractors/expo.js +36 -0
- package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
- package/dist/core/ingestion/route-extractors/middleware.js +143 -0
- package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
- package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
- package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
- package/dist/core/ingestion/route-extractors/php.js +21 -0
- package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
- package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
- package/dist/core/ingestion/symbol-table.d.ts +16 -0
- package/dist/core/ingestion/symbol-table.js +20 -6
- package/dist/core/ingestion/tree-sitter-queries.d.ts +10 -9
- package/dist/core/ingestion/tree-sitter-queries.js +274 -11
- package/dist/core/ingestion/type-env.d.ts +42 -18
- package/dist/core/ingestion/type-env.js +481 -106
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
- package/dist/core/ingestion/type-extractors/csharp.js +149 -16
- package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
- package/dist/core/ingestion/type-extractors/dart.js +371 -0
- package/dist/core/ingestion/type-extractors/jvm.js +169 -66
- package/dist/core/ingestion/type-extractors/rust.js +35 -1
- package/dist/core/ingestion/type-extractors/shared.d.ts +1 -15
- package/dist/core/ingestion/type-extractors/shared.js +14 -112
- package/dist/core/ingestion/type-extractors/swift.js +338 -7
- package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
- package/dist/core/ingestion/type-extractors/typescript.js +141 -9
- package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
- package/dist/core/ingestion/utils/ast-helpers.js +817 -0
- package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/utils/call-analysis.js +527 -0
- package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
- package/dist/core/ingestion/utils/event-loop.js +5 -0
- package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
- package/dist/core/ingestion/utils/language-detection.js +70 -0
- package/dist/core/ingestion/utils/verbose.d.ts +1 -0
- package/dist/core/ingestion/utils/verbose.js +7 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
- package/dist/core/ingestion/workers/parse-worker.js +415 -225
- package/dist/core/lbug/csv-generator.js +51 -1
- package/dist/core/lbug/lbug-adapter.d.ts +10 -0
- package/dist/core/lbug/lbug-adapter.js +75 -4
- package/dist/core/lbug/schema.d.ts +8 -4
- package/dist/core/lbug/schema.js +65 -4
- package/dist/core/tree-sitter/parser-loader.js +7 -1
- package/dist/core/wiki/cursor-client.d.ts +31 -0
- package/dist/core/wiki/cursor-client.js +127 -0
- package/dist/core/wiki/generator.d.ts +28 -9
- package/dist/core/wiki/generator.js +115 -18
- package/dist/core/wiki/graph-queries.d.ts +4 -0
- package/dist/core/wiki/graph-queries.js +7 -1
- package/dist/core/wiki/llm-client.d.ts +2 -0
- package/dist/core/wiki/llm-client.js +8 -4
- package/dist/core/wiki/prompts.d.ts +3 -3
- package/dist/core/wiki/prompts.js +6 -0
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +5 -0
- package/dist/mcp/core/lbug-adapter.js +23 -2
- package/dist/mcp/local/local-backend.d.ts +38 -5
- package/dist/mcp/local/local-backend.js +804 -63
- package/dist/mcp/resources.js +2 -0
- package/dist/mcp/tools.js +73 -4
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- package/dist/storage/repo-manager.d.ts +3 -0
- package/package.json +25 -16
- package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
- package/dist/core/ingestion/named-binding-extraction.js +0 -363
- package/dist/core/ingestion/resolvers/index.d.ts +0 -18
- package/dist/core/ingestion/resolvers/index.js +0 -13
- package/dist/core/ingestion/resolvers/jvm.js +0 -87
- package/dist/core/ingestion/resolvers/php.d.ts +0 -15
- package/dist/core/ingestion/resolvers/php.js +0 -35
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
- package/dist/core/ingestion/type-extractors/index.js +0 -31
- package/dist/core/ingestion/utils.d.ts +0 -138
- package/dist/core/ingestion/utils.js +0 -1290
- package/scripts/patch-tree-sitter-swift.cjs +0 -74
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES
|
|
2
|
-
import {
|
|
1
|
+
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from './utils/ast-helpers.js';
|
|
2
|
+
import { CALL_EXPRESSION_TYPES } from './utils/call-analysis.js';
|
|
3
|
+
import { TYPED_PARAMETER_TYPES } from './type-extractors/shared.js';
|
|
4
|
+
import { getProvider } from './languages/index.js';
|
|
3
5
|
import { extractSimpleTypeName, extractVarName, stripNullable, extractReturnTypeName } from './type-extractors/shared.js';
|
|
4
6
|
/** File-level scope key */
|
|
5
7
|
const FILE_SCOPE = '';
|
|
8
|
+
/** Shared empty map for files with no file-scope bindings. */
|
|
9
|
+
const EMPTY_FILE_SCOPE = new Map();
|
|
6
10
|
/** Fallback for languages where class names aren't in a 'name' field (e.g. Kotlin uses type_identifier). */
|
|
7
11
|
const findTypeIdentifierChild = (node) => {
|
|
8
12
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -12,16 +16,21 @@ const findTypeIdentifierChild = (node) => {
|
|
|
12
16
|
}
|
|
13
17
|
return null;
|
|
14
18
|
};
|
|
15
|
-
/** AST node types that represent mutually exclusive branch containers for pattern bindings.
|
|
16
|
-
|
|
19
|
+
/** AST node types that represent mutually exclusive branch containers for pattern bindings.
|
|
20
|
+
* Includes both multi-arm pattern-match branches AND if-statement bodies for null-check narrowing. */
|
|
21
|
+
const NARROWING_BRANCH_TYPES = new Set([
|
|
17
22
|
'when_entry', // Kotlin when
|
|
18
23
|
'switch_block_label', // Java switch (enhanced)
|
|
24
|
+
'if_statement', // TS/JS, Java, C/C++
|
|
25
|
+
'if_expression', // Kotlin (if is an expression)
|
|
26
|
+
'statement_block', // TS/JS: { ... } body of if
|
|
27
|
+
'control_structure_body', // Kotlin: body of if
|
|
19
28
|
]);
|
|
20
29
|
/** Walk up the AST from a pattern node to find the enclosing branch container. */
|
|
21
|
-
const
|
|
30
|
+
const findNarrowingBranchScope = (node) => {
|
|
22
31
|
let current = node.parent;
|
|
23
32
|
while (current) {
|
|
24
|
-
if (
|
|
33
|
+
if (NARROWING_BRANCH_TYPES.has(current.type))
|
|
25
34
|
return current;
|
|
26
35
|
if (FUNCTION_NODE_TYPES.has(current.type))
|
|
27
36
|
return undefined;
|
|
@@ -44,7 +53,7 @@ const fastStripNullable = (typeName) => {
|
|
|
44
53
|
: stripNullable(typeName);
|
|
45
54
|
};
|
|
46
55
|
/** Implementation of the lookup logic — shared between TypeEnvironment and the legacy export. */
|
|
47
|
-
const lookupInEnv = (env, varName, callNode, patternOverrides) => {
|
|
56
|
+
const lookupInEnv = (env, varName, callNode, patternOverrides, enclosingFunctionFinder) => {
|
|
48
57
|
// Self/this receiver: resolve to enclosing class name via AST walk
|
|
49
58
|
if (varName === 'self' || varName === 'this' || varName === '$this') {
|
|
50
59
|
return findEnclosingClassName(callNode);
|
|
@@ -55,7 +64,7 @@ const lookupInEnv = (env, varName, callNode, patternOverrides) => {
|
|
|
55
64
|
return findEnclosingParentClassName(callNode);
|
|
56
65
|
}
|
|
57
66
|
// Determine the enclosing function scope for the call
|
|
58
|
-
const scopeKey = findEnclosingScopeKey(callNode);
|
|
67
|
+
const scopeKey = findEnclosingScopeKey(callNode, enclosingFunctionFinder);
|
|
59
68
|
// Check position-indexed pattern overrides first (e.g., Kotlin when/is smart casts).
|
|
60
69
|
// These take priority over flat scopeEnv because they represent per-branch narrowing.
|
|
61
70
|
if (scopeKey && patternOverrides) {
|
|
@@ -83,23 +92,51 @@ const lookupInEnv = (env, varName, callNode, patternOverrides) => {
|
|
|
83
92
|
const raw = fileEnv?.get(varName);
|
|
84
93
|
return raw ? fastStripNullable(raw) : undefined;
|
|
85
94
|
};
|
|
95
|
+
/** Per-file memoization caches for expensive parent-walk functions.
|
|
96
|
+
* Cleared at the start of each buildTypeEnv call (one call per file). */
|
|
97
|
+
const enclosingClassNameCache = new Map();
|
|
98
|
+
const enclosingParentClassNameCache = new Map();
|
|
86
99
|
/**
|
|
87
100
|
* Walk up the AST from a node to find the enclosing class/module name.
|
|
88
101
|
* Used to resolve `self`/`this` receivers to their containing type.
|
|
102
|
+
* Memoized per-file: cache is cleared at buildTypeEnv entry.
|
|
89
103
|
*/
|
|
90
104
|
const findEnclosingClassName = (node) => {
|
|
105
|
+
if (enclosingClassNameCache.has(node))
|
|
106
|
+
return enclosingClassNameCache.get(node);
|
|
91
107
|
let current = node.parent;
|
|
92
108
|
while (current) {
|
|
93
109
|
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
|
94
110
|
const nameNode = current.childForFieldName('name')
|
|
95
111
|
?? findTypeIdentifierChild(current);
|
|
96
|
-
if (nameNode)
|
|
112
|
+
if (nameNode) {
|
|
113
|
+
enclosingClassNameCache.set(node, nameNode.text);
|
|
97
114
|
return nameNode.text;
|
|
115
|
+
}
|
|
98
116
|
}
|
|
99
117
|
current = current.parent;
|
|
100
118
|
}
|
|
119
|
+
enclosingClassNameCache.set(node, undefined);
|
|
101
120
|
return undefined;
|
|
102
121
|
};
|
|
122
|
+
/** Keywords that refer to the current instance across languages. */
|
|
123
|
+
const THIS_RECEIVERS = new Set(['this', 'self', '$this', 'Me']);
|
|
124
|
+
/**
|
|
125
|
+
* If a pending assignment's receiver is this/self/$this/Me, substitute the
|
|
126
|
+
* enclosing class name. Returns the item unchanged for non-receiver kinds
|
|
127
|
+
* or when the receiver is not a this-keyword. Properties are readonly in the
|
|
128
|
+
* discriminated union, so a new object is returned when substitution occurs.
|
|
129
|
+
*/
|
|
130
|
+
const substituteThisReceiver = (item, node) => {
|
|
131
|
+
if (item.kind !== 'fieldAccess' && item.kind !== 'methodCallResult')
|
|
132
|
+
return item;
|
|
133
|
+
if (!THIS_RECEIVERS.has(item.receiver))
|
|
134
|
+
return item;
|
|
135
|
+
const className = findEnclosingClassName(node);
|
|
136
|
+
if (!className)
|
|
137
|
+
return item;
|
|
138
|
+
return { ...item, receiver: className };
|
|
139
|
+
};
|
|
103
140
|
/**
|
|
104
141
|
* Walk up the AST to find the enclosing class, then extract its parent class name
|
|
105
142
|
* from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
|
|
@@ -115,13 +152,18 @@ const findEnclosingClassName = (node) => {
|
|
|
115
152
|
* - Swift: unnamed `inheritance_specifier` child → user_type → type_identifier
|
|
116
153
|
*/
|
|
117
154
|
const findEnclosingParentClassName = (node) => {
|
|
155
|
+
if (enclosingParentClassNameCache.has(node))
|
|
156
|
+
return enclosingParentClassNameCache.get(node);
|
|
118
157
|
let current = node.parent;
|
|
119
158
|
while (current) {
|
|
120
159
|
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
|
121
|
-
|
|
160
|
+
const result = extractParentClassFromNode(current);
|
|
161
|
+
enclosingParentClassNameCache.set(node, result);
|
|
162
|
+
return result;
|
|
122
163
|
}
|
|
123
164
|
current = current.parent;
|
|
124
165
|
}
|
|
166
|
+
enclosingParentClassNameCache.set(node, undefined);
|
|
125
167
|
return undefined;
|
|
126
168
|
};
|
|
127
169
|
/** Extract the parent/superclass name from a class declaration AST node. */
|
|
@@ -228,8 +270,12 @@ const extractParentClassFromNode = (classNode) => {
|
|
|
228
270
|
}
|
|
229
271
|
return undefined;
|
|
230
272
|
};
|
|
231
|
-
/** Find the enclosing function name for scope lookup.
|
|
232
|
-
|
|
273
|
+
/** Find the enclosing function name for scope lookup.
|
|
274
|
+
* When an `enclosingFunctionFinder` hook is provided (from the language provider),
|
|
275
|
+
* it is consulted for each ancestor before the default FUNCTION_NODE_TYPES check.
|
|
276
|
+
* This handles languages like Dart where the function body is a sibling of the
|
|
277
|
+
* signature instead of a child. */
|
|
278
|
+
const findEnclosingScopeKey = (node, enclosingFunctionFinder) => {
|
|
233
279
|
let current = node.parent;
|
|
234
280
|
while (current) {
|
|
235
281
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
@@ -237,6 +283,15 @@ const findEnclosingScopeKey = (node) => {
|
|
|
237
283
|
if (funcName)
|
|
238
284
|
return `${funcName}@${current.startIndex}`;
|
|
239
285
|
}
|
|
286
|
+
// Language-specific hook (e.g., Dart function_body → sibling function_signature)
|
|
287
|
+
if (enclosingFunctionFinder) {
|
|
288
|
+
const result = enclosingFunctionFinder(current);
|
|
289
|
+
if (result) {
|
|
290
|
+
const sigNode = current.previousSibling;
|
|
291
|
+
const startIdx = sigNode?.startIndex ?? current.startIndex;
|
|
292
|
+
return `${result.funcName}@${startIdx}`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
240
295
|
current = current.parent;
|
|
241
296
|
}
|
|
242
297
|
return undefined;
|
|
@@ -297,78 +352,321 @@ const SKIP_SUBTREE_TYPES = new Set([
|
|
|
297
352
|
'regex', 'regex_pattern',
|
|
298
353
|
]);
|
|
299
354
|
const CLASS_LIKE_TYPES = new Set(['Class', 'Struct', 'Interface']);
|
|
355
|
+
/** Memoize class definition lookups during fixpoint iteration.
|
|
356
|
+
* SymbolTable is immutable during type resolution, so results never change.
|
|
357
|
+
* Eliminates redundant array allocations + filter scans across iterations. */
|
|
358
|
+
const createClassDefCache = (symbolTable) => {
|
|
359
|
+
const cache = new Map();
|
|
360
|
+
return (typeName) => {
|
|
361
|
+
let result = cache.get(typeName);
|
|
362
|
+
if (result === undefined) {
|
|
363
|
+
result = symbolTable
|
|
364
|
+
? symbolTable.lookupFuzzy(typeName).filter(d => CLASS_LIKE_TYPES.has(d.type))
|
|
365
|
+
: [];
|
|
366
|
+
cache.set(typeName, result);
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
/** AST node types representing constructor expressions across languages.
|
|
372
|
+
* Note: C# also has `implicit_object_creation_expression` (`new()` with type
|
|
373
|
+
* inference) which is NOT captured — the type is inferred, not explicit.
|
|
374
|
+
* Kotlin constructors use `call_expression` (no `new` keyword) — not detected. */
|
|
375
|
+
const CONSTRUCTOR_EXPR_TYPES = new Set([
|
|
376
|
+
'new_expression', // TS/JS/C++: new Dog()
|
|
377
|
+
'object_creation_expression', // Java/C#: new Dog()
|
|
378
|
+
]);
|
|
379
|
+
/** Extract the constructor class name from a declaration node's initializer.
|
|
380
|
+
* Searches for new_expression / object_creation_expression in the node's subtree.
|
|
381
|
+
* Returns the class name or undefined if no constructor is found.
|
|
382
|
+
* Depth-limited to 5 to avoid expensive traversals. */
|
|
383
|
+
const extractConstructorTypeName = (node, depth = 0) => {
|
|
384
|
+
if (depth > 5)
|
|
385
|
+
return undefined;
|
|
386
|
+
if (CONSTRUCTOR_EXPR_TYPES.has(node.type)) {
|
|
387
|
+
// Java/C#: object_creation_expression has 'type' field
|
|
388
|
+
const typeField = node.childForFieldName('type');
|
|
389
|
+
if (typeField)
|
|
390
|
+
return extractSimpleTypeName(typeField);
|
|
391
|
+
// TS/JS: new_expression has 'constructor' field (but tree-sitter often just has identifier child)
|
|
392
|
+
const ctorField = node.childForFieldName('constructor');
|
|
393
|
+
if (ctorField)
|
|
394
|
+
return extractSimpleTypeName(ctorField);
|
|
395
|
+
// Fallback: first named child is often the class identifier
|
|
396
|
+
if (node.firstNamedChild)
|
|
397
|
+
return extractSimpleTypeName(node.firstNamedChild);
|
|
398
|
+
}
|
|
399
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
400
|
+
const child = node.namedChild(i);
|
|
401
|
+
if (!child)
|
|
402
|
+
continue;
|
|
403
|
+
// Don't descend into nested functions/classes or call expressions (prevents
|
|
404
|
+
// finding constructor args inside method calls, e.g. processAll(new Dog()))
|
|
405
|
+
if (FUNCTION_NODE_TYPES.has(child.type) || CLASS_CONTAINER_TYPES.has(child.type)
|
|
406
|
+
|| CALL_EXPRESSION_TYPES.has(child.type))
|
|
407
|
+
continue;
|
|
408
|
+
const result = extractConstructorTypeName(child, depth + 1);
|
|
409
|
+
if (result)
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
};
|
|
414
|
+
/** Max depth for MRO parent chain walking. Real-world inheritance rarely exceeds 3-4 levels. */
|
|
415
|
+
const MAX_MRO_DEPTH = 5;
|
|
416
|
+
/** Check if `child` is a subclass of `parent` using the parentMap.
|
|
417
|
+
* BFS up from child, depth-limited (5), cycle-safe. */
|
|
418
|
+
export const isSubclassOf = (child, parent, parentMap) => {
|
|
419
|
+
if (!parentMap || child === parent)
|
|
420
|
+
return false;
|
|
421
|
+
const visited = new Set([child]);
|
|
422
|
+
let current = [child];
|
|
423
|
+
for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
|
|
424
|
+
const next = [];
|
|
425
|
+
for (const cls of current) {
|
|
426
|
+
const parents = parentMap.get(cls);
|
|
427
|
+
if (!parents)
|
|
428
|
+
continue;
|
|
429
|
+
for (const p of parents) {
|
|
430
|
+
if (p === parent)
|
|
431
|
+
return true;
|
|
432
|
+
if (!visited.has(p)) {
|
|
433
|
+
visited.add(p);
|
|
434
|
+
next.push(p);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
current = next;
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
};
|
|
442
|
+
/** Walk up the parent class chain to find a field or method on an ancestor.
|
|
443
|
+
* BFS-like traversal with depth limit and cycle detection. First match wins.
|
|
444
|
+
* Used by resolveFieldType and resolveMethodReturnType when direct lookup fails. */
|
|
445
|
+
const walkParentChain = (typeName, parentMap, getClassDefs, lookupOnClass) => {
|
|
446
|
+
if (!parentMap)
|
|
447
|
+
return undefined;
|
|
448
|
+
const visited = new Set([typeName]);
|
|
449
|
+
let current = [typeName];
|
|
450
|
+
for (let depth = 0; depth < MAX_MRO_DEPTH && current.length > 0; depth++) {
|
|
451
|
+
const next = [];
|
|
452
|
+
for (const cls of current) {
|
|
453
|
+
const parents = parentMap.get(cls);
|
|
454
|
+
if (!parents)
|
|
455
|
+
continue;
|
|
456
|
+
for (const parent of parents) {
|
|
457
|
+
if (visited.has(parent))
|
|
458
|
+
continue;
|
|
459
|
+
visited.add(parent);
|
|
460
|
+
const parentDefs = getClassDefs(parent);
|
|
461
|
+
if (parentDefs.length === 1) {
|
|
462
|
+
const result = lookupOnClass(parentDefs[0].nodeId);
|
|
463
|
+
if (result !== undefined)
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
next.push(parent);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
current = next;
|
|
470
|
+
}
|
|
471
|
+
return undefined;
|
|
472
|
+
};
|
|
300
473
|
/** Resolve a field's declared type given a receiver variable and field name.
|
|
301
474
|
* Uses SymbolTable to find the class nodeId for the receiver's type, then
|
|
302
|
-
* looks up the field via the eagerly-populated fieldByOwner index.
|
|
303
|
-
|
|
475
|
+
* looks up the field via the eagerly-populated fieldByOwner index.
|
|
476
|
+
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
|
477
|
+
const resolveFieldType = (receiver, field, scopeEnv, symbolTable, getClassDefs, parentMap) => {
|
|
304
478
|
if (!symbolTable)
|
|
305
479
|
return undefined;
|
|
306
480
|
const receiverType = scopeEnv.get(receiver);
|
|
307
481
|
if (!receiverType)
|
|
308
482
|
return undefined;
|
|
309
|
-
const
|
|
310
|
-
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
|
483
|
+
const lookup = getClassDefs
|
|
484
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
485
|
+
const classDefs = lookup(receiverType);
|
|
311
486
|
if (classDefs.length !== 1)
|
|
312
487
|
return undefined;
|
|
488
|
+
// Direct lookup first
|
|
313
489
|
const fieldDef = symbolTable.lookupFieldByOwner(classDefs[0].nodeId, field);
|
|
314
|
-
if (
|
|
315
|
-
return
|
|
316
|
-
|
|
490
|
+
if (fieldDef?.declaredType)
|
|
491
|
+
return extractReturnTypeName(fieldDef.declaredType);
|
|
492
|
+
// MRO parent chain walking on miss
|
|
493
|
+
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
|
494
|
+
const f = symbolTable.lookupFieldByOwner(nodeId, field);
|
|
495
|
+
return f?.declaredType ? extractReturnTypeName(f.declaredType) : undefined;
|
|
496
|
+
});
|
|
497
|
+
return inherited;
|
|
317
498
|
};
|
|
318
499
|
/** Resolve a method's return type given a receiver variable and method name.
|
|
319
500
|
* Uses SymbolTable to find class nodeIds for the receiver's type, then
|
|
320
|
-
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
|
321
|
-
|
|
501
|
+
* looks up the method via lookupFuzzyCallable filtered by ownerId.
|
|
502
|
+
* Falls back to MRO parent chain walking if direct lookup fails (Phase 11A). */
|
|
503
|
+
const resolveMethodReturnType = (receiver, method, scopeEnv, symbolTable, getClassDefs, parentMap) => {
|
|
322
504
|
if (!symbolTable)
|
|
323
505
|
return undefined;
|
|
324
506
|
const receiverType = scopeEnv.get(receiver);
|
|
325
507
|
if (!receiverType)
|
|
326
508
|
return undefined;
|
|
327
|
-
const
|
|
328
|
-
.filter(d => CLASS_LIKE_TYPES.has(d.type));
|
|
509
|
+
const lookup = getClassDefs
|
|
510
|
+
?? ((name) => symbolTable.lookupFuzzy(name).filter(d => CLASS_LIKE_TYPES.has(d.type)));
|
|
511
|
+
const classDefs = lookup(receiverType);
|
|
329
512
|
if (classDefs.length === 0)
|
|
330
513
|
return undefined;
|
|
514
|
+
// Direct lookup first
|
|
331
515
|
const classNodeIds = new Set(classDefs.map(d => d.nodeId));
|
|
332
516
|
const methods = symbolTable.lookupFuzzyCallable(method)
|
|
333
517
|
.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
334
|
-
if (methods.length
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
518
|
+
if (methods.length === 1 && methods[0].returnType) {
|
|
519
|
+
return extractReturnTypeName(methods[0].returnType);
|
|
520
|
+
}
|
|
521
|
+
// MRO parent chain walking on miss
|
|
522
|
+
if (methods.length === 0) {
|
|
523
|
+
const inherited = walkParentChain(receiverType, parentMap, lookup, (nodeId) => {
|
|
524
|
+
const parentMethods = symbolTable.lookupFuzzyCallable(method)
|
|
525
|
+
.filter(d => d.ownerId === nodeId);
|
|
526
|
+
if (parentMethods.length !== 1 || !parentMethods[0].returnType)
|
|
527
|
+
return undefined;
|
|
528
|
+
return extractReturnTypeName(parentMethods[0].returnType);
|
|
529
|
+
});
|
|
530
|
+
return inherited;
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
339
533
|
};
|
|
340
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Unified fixpoint propagation: iterate over ALL pending items (copy, callResult,
|
|
536
|
+
* fieldAccess, methodCallResult) until no new bindings are produced.
|
|
537
|
+
* Handles arbitrary-depth mixed chains:
|
|
538
|
+
* const user = getUser(); // callResult → User
|
|
539
|
+
* const addr = user.address; // fieldAccess → Address (depends on user)
|
|
540
|
+
* const city = addr.getCity(); // methodCallResult → City (depends on addr)
|
|
541
|
+
* const alias = city; // copy → City (depends on city)
|
|
542
|
+
* Data flow: SymbolTable (immutable) + scopeEnv → resolve → scopeEnv.
|
|
543
|
+
* Termination: finite entries, each bound at most once (first-writer-wins), max 10 iterations.
|
|
544
|
+
*/
|
|
545
|
+
const MAX_FIXPOINT_ITERATIONS = 10;
|
|
546
|
+
const resolveFixpointBindings = (pendingItems, env, returnTypeLookup, symbolTable, parentMap) => {
|
|
547
|
+
if (pendingItems.length === 0)
|
|
548
|
+
return;
|
|
549
|
+
const getClassDefs = createClassDefCache(symbolTable);
|
|
550
|
+
const resolved = new Set();
|
|
551
|
+
for (let iter = 0; iter < MAX_FIXPOINT_ITERATIONS; iter++) {
|
|
552
|
+
let changed = false;
|
|
553
|
+
for (let i = 0; i < pendingItems.length; i++) {
|
|
554
|
+
if (resolved.has(i))
|
|
555
|
+
continue;
|
|
556
|
+
const item = pendingItems[i];
|
|
557
|
+
const scopeEnv = env.get(item.scope);
|
|
558
|
+
if (!scopeEnv || scopeEnv.has(item.lhs)) {
|
|
559
|
+
resolved.add(i);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
let typeName;
|
|
563
|
+
switch (item.kind) {
|
|
564
|
+
case 'callResult':
|
|
565
|
+
// Phase 9: Prefer FQN lookup when available for higher precision
|
|
566
|
+
typeName = item.calleeFqn
|
|
567
|
+
? returnTypeLookup.lookupReturnType(item.calleeFqn)
|
|
568
|
+
: returnTypeLookup.lookupReturnType(item.callee);
|
|
569
|
+
break;
|
|
570
|
+
case 'copy':
|
|
571
|
+
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
|
572
|
+
break;
|
|
573
|
+
case 'fieldAccess':
|
|
574
|
+
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable, getClassDefs, parentMap);
|
|
575
|
+
break;
|
|
576
|
+
case 'methodCallResult':
|
|
577
|
+
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable, getClassDefs, parentMap);
|
|
578
|
+
break;
|
|
579
|
+
default: {
|
|
580
|
+
// Exhaustive check: TypeScript will error here if a new PendingAssignment
|
|
581
|
+
// kind is added without handling it in the switch.
|
|
582
|
+
const _exhaustive = item;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (typeName) {
|
|
587
|
+
scopeEnv.set(item.lhs, typeName);
|
|
588
|
+
resolved.add(i);
|
|
589
|
+
changed = true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!changed)
|
|
593
|
+
break;
|
|
594
|
+
if (iter === MAX_FIXPOINT_ITERATIONS - 1 && process.env.GITNEXUS_DEBUG) {
|
|
595
|
+
const unresolved = pendingItems.length - resolved.size;
|
|
596
|
+
if (unresolved > 0) {
|
|
597
|
+
console.warn(`[type-env] fixpoint hit iteration cap (${MAX_FIXPOINT_ITERATIONS}), ${unresolved} items unresolved`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
/** Seed cross-file type bindings into the file scope.
|
|
603
|
+
* MUST be called AFTER walk() completes so that local declarations
|
|
604
|
+
* (Tier 0/1) always take precedence over imported bindings (first-writer-wins). */
|
|
605
|
+
function seedImportedBindings(env, importedBindings) {
|
|
606
|
+
let fileEnv = env.get(FILE_SCOPE);
|
|
607
|
+
if (!fileEnv) {
|
|
608
|
+
fileEnv = new Map();
|
|
609
|
+
env.set(FILE_SCOPE, fileEnv);
|
|
610
|
+
}
|
|
611
|
+
for (const [name, type] of importedBindings) {
|
|
612
|
+
if (!fileEnv.has(name)) {
|
|
613
|
+
fileEnv.set(name, type);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
export const buildTypeEnv = (tree, language, options) => {
|
|
618
|
+
// Clear per-file memoization caches from the previous file.
|
|
619
|
+
enclosingClassNameCache.clear();
|
|
620
|
+
enclosingParentClassNameCache.clear();
|
|
621
|
+
const symbolTable = options?.symbolTable;
|
|
622
|
+
const parentMap = options?.parentMap;
|
|
341
623
|
const env = new Map();
|
|
342
624
|
const patternOverrides = new Map();
|
|
625
|
+
// Phase P: maps `scope\0varName` → constructor type when a declaration has BOTH
|
|
626
|
+
// a base type annotation AND a more specific constructor initializer.
|
|
627
|
+
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('func@42\0a', 'Dog')
|
|
628
|
+
const constructorTypeMap = new Map();
|
|
343
629
|
const localClassNames = new Set();
|
|
344
630
|
const classNames = createClassNameLookup(localClassNames, symbolTable);
|
|
345
|
-
const
|
|
631
|
+
const provider = getProvider(language);
|
|
632
|
+
const config = provider.typeConfig;
|
|
346
633
|
const bindings = [];
|
|
347
|
-
// Build ReturnTypeLookup
|
|
348
|
-
//
|
|
634
|
+
// Build ReturnTypeLookup: SymbolTable is authoritative when it has an unambiguous match.
|
|
635
|
+
// Cross-file importedReturnTypes are consulted ONLY when SymbolTable has 0 matches.
|
|
636
|
+
// Ambiguous (2+) → undefined, no cross-file fallback (conservative, local-first principle).
|
|
349
637
|
const returnTypeLookup = {
|
|
350
638
|
lookupReturnType(callee) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
639
|
+
// SymbolTable is authoritative when it has an unambiguous match
|
|
640
|
+
if (symbolTable) {
|
|
641
|
+
if (provider.isBuiltInName(callee))
|
|
642
|
+
return undefined;
|
|
643
|
+
const callables = symbolTable.lookupFuzzyCallable(callee);
|
|
644
|
+
if (callables.length === 1) {
|
|
645
|
+
const rawReturn = callables[0].returnType;
|
|
646
|
+
if (rawReturn)
|
|
647
|
+
return extractReturnTypeName(rawReturn);
|
|
648
|
+
}
|
|
649
|
+
// Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
|
|
650
|
+
if (callables.length > 1)
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
// No match (0 results or no symbolTable) → fall back to cross-file
|
|
654
|
+
return options?.importedReturnTypes?.get(callee);
|
|
362
655
|
},
|
|
363
656
|
lookupRawReturnType(callee) {
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return undefined
|
|
371
|
-
|
|
657
|
+
if (symbolTable) {
|
|
658
|
+
if (provider.isBuiltInName(callee))
|
|
659
|
+
return undefined;
|
|
660
|
+
const callables = symbolTable.lookupFuzzyCallable(callee);
|
|
661
|
+
if (callables.length === 1)
|
|
662
|
+
return callables[0].returnType;
|
|
663
|
+
// Ambiguous (2+) → return undefined (conservative, no cross-file fallback)
|
|
664
|
+
if (callables.length > 1)
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
// Cross-file fallback uses importedRawReturnTypes (raw declared types, e.g., 'User[]')
|
|
668
|
+
// NOT importedReturnTypes (which contains processed/simple types via extractReturnTypeName)
|
|
669
|
+
return options?.importedRawReturnTypes?.get(callee);
|
|
372
670
|
}
|
|
373
671
|
};
|
|
374
672
|
// Pre-compute combined set of node types that need extractTypeBinding.
|
|
@@ -381,6 +679,9 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
381
679
|
// methodCallResult items during walk(), then iterates until no new bindings are produced.
|
|
382
680
|
// Handles arbitrary-depth mixed chains: callResult → fieldAccess → methodCallResult → copy.
|
|
383
681
|
const pendingItems = [];
|
|
682
|
+
// For-loop nodes whose iterable was unresolved at walk-time. Replayed after the fixpoint
|
|
683
|
+
// resolves the iterable's type, bridging the walk-time/fixpoint gap (Phase 10 / ex-9B).
|
|
684
|
+
const pendingForLoops = [];
|
|
384
685
|
// Maps `scope\0varName` → the type annotation AST node from the original declaration.
|
|
385
686
|
// Allows pattern extractors to navigate back to the declaration's generic type arguments
|
|
386
687
|
// (e.g., to extract T from Result<T, E> for `if let Ok(x) = res`).
|
|
@@ -432,7 +733,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
432
733
|
fallbackName = child;
|
|
433
734
|
}
|
|
434
735
|
if (!fallbackType && (child.type === 'user_type' || child.type === 'type_identifier'
|
|
435
|
-
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
736
|
+
|| child.type === 'generic_type' || child.type === 'parameterized_type'
|
|
737
|
+
|| child.type === 'nullable_type')) {
|
|
436
738
|
fallbackType = child;
|
|
437
739
|
}
|
|
438
740
|
}
|
|
@@ -450,8 +752,14 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
450
752
|
// Checked before declarationNodeTypes — loop variables are not declarations.
|
|
451
753
|
if (config.forLoopNodeTypes?.has(node.type)) {
|
|
452
754
|
if (config.extractForLoopBinding) {
|
|
755
|
+
const sizeBefore = scopeEnv.size;
|
|
453
756
|
const forLoopCtx = { scopeEnv, declarationTypeNodes, scope, returnTypeLookup };
|
|
454
757
|
config.extractForLoopBinding(node, forLoopCtx);
|
|
758
|
+
// If no new binding was produced, the iterable's type may not yet be resolved.
|
|
759
|
+
// Store for post-fixpoint replay (Phase 10 / ex-9B loop-fixpoint bridge).
|
|
760
|
+
if (scopeEnv.size === sizeBefore) {
|
|
761
|
+
pendingForLoops.push({ node, scope });
|
|
762
|
+
}
|
|
455
763
|
}
|
|
456
764
|
return;
|
|
457
765
|
}
|
|
@@ -477,8 +785,32 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
477
785
|
}
|
|
478
786
|
}
|
|
479
787
|
}
|
|
480
|
-
if (wrapped)
|
|
788
|
+
if (wrapped) {
|
|
481
789
|
typeNode = wrapped.childForFieldName('type');
|
|
790
|
+
// Kotlin: variable_declaration stores the type as user_type / nullable_type
|
|
791
|
+
// child rather than a named 'type' field.
|
|
792
|
+
if (!typeNode) {
|
|
793
|
+
for (let i = 0; i < wrapped.namedChildCount; i++) {
|
|
794
|
+
const c = wrapped.namedChild(i);
|
|
795
|
+
if (c && (c.type === 'user_type' || c.type === 'nullable_type')) {
|
|
796
|
+
typeNode = c;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Swift: property_declaration has type_annotation as a direct child (not a 'type' field).
|
|
803
|
+
// Extract the inner type node (array_type, user_type, etc.) for declarationTypeNodes.
|
|
804
|
+
if (!typeNode) {
|
|
805
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
806
|
+
const c = node.namedChild(i);
|
|
807
|
+
if (c?.type === 'type_annotation') {
|
|
808
|
+
// Use the inner type (array_type, user_type) rather than the annotation wrapper
|
|
809
|
+
typeNode = c.firstNamedChild ?? c;
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
482
814
|
}
|
|
483
815
|
if (typeNode) {
|
|
484
816
|
const nameNode = node.childForFieldName('name')
|
|
@@ -492,13 +824,20 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
492
824
|
}
|
|
493
825
|
}
|
|
494
826
|
// Run the language-specific declaration extractor (may or may not add to scopeEnv).
|
|
495
|
-
const
|
|
827
|
+
const sizeBefore = typeNode ? scopeEnv.size : -1;
|
|
496
828
|
config.extractDeclaration(node, scopeEnv);
|
|
497
829
|
// Fallback: for multi-declarator languages (TS, C#, Java) where the type field
|
|
498
|
-
// is on variable_declarator children, capture
|
|
499
|
-
|
|
830
|
+
// is on variable_declarator children, capture newly-added keys.
|
|
831
|
+
// Map preserves insertion order, so new keys are always at the end —
|
|
832
|
+
// skip the first sizeBefore entries to find only newly-added variables.
|
|
833
|
+
if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
|
|
834
|
+
let skip = sizeBefore;
|
|
500
835
|
for (const varName of scopeEnv.keys()) {
|
|
501
|
-
if (
|
|
836
|
+
if (skip > 0) {
|
|
837
|
+
skip--;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (!declarationTypeNodes.has(`${scope}\0${varName}`)) {
|
|
502
841
|
declarationTypeNodes.set(`${scope}\0${varName}`, typeNode);
|
|
503
842
|
}
|
|
504
843
|
}
|
|
@@ -510,6 +849,35 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
510
849
|
if (config.extractInitializer) {
|
|
511
850
|
config.extractInitializer(node, scopeEnv, classNames);
|
|
512
851
|
}
|
|
852
|
+
// Phase P: detect constructor-visible virtual dispatch.
|
|
853
|
+
// When a declaration has BOTH a type annotation AND a constructor initializer,
|
|
854
|
+
// record the constructor type for receiver override at call resolution time.
|
|
855
|
+
// e.g., `Animal a = new Dog()` → constructorTypeMap.set('scope\0a', 'Dog')
|
|
856
|
+
if (sizeBefore >= 0 && scopeEnv.size > sizeBefore) {
|
|
857
|
+
let ctorSkip = sizeBefore;
|
|
858
|
+
for (const varName of scopeEnv.keys()) {
|
|
859
|
+
if (ctorSkip > 0) {
|
|
860
|
+
ctorSkip--;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const declaredType = scopeEnv.get(varName);
|
|
864
|
+
if (!declaredType)
|
|
865
|
+
continue;
|
|
866
|
+
const ctorType = extractConstructorTypeName(node)
|
|
867
|
+
?? config.detectConstructorType?.(node, classNames);
|
|
868
|
+
if (!ctorType || ctorType === declaredType)
|
|
869
|
+
continue;
|
|
870
|
+
// Unwrap wrapper types (e.g., C++ shared_ptr<Animal> → Animal) for an
|
|
871
|
+
// accurate isSubclassOf comparison. Language-specific via config hook.
|
|
872
|
+
const declTypeNode = declarationTypeNodes.get(`${scope}\0${varName}`);
|
|
873
|
+
const effectiveDeclaredType = (declTypeNode && config.unwrapDeclaredType)
|
|
874
|
+
? (config.unwrapDeclaredType(declaredType, declTypeNode) ?? declaredType)
|
|
875
|
+
: declaredType;
|
|
876
|
+
if (ctorType !== effectiveDeclaredType) {
|
|
877
|
+
constructorTypeMap.set(`${scope}\0${varName}`, ctorType);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
513
881
|
}
|
|
514
882
|
};
|
|
515
883
|
const walk = (node, currentScope) => {
|
|
@@ -542,7 +910,8 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
542
910
|
extractTypeBinding(node, scopeEnv, scope);
|
|
543
911
|
}
|
|
544
912
|
// Pattern binding extraction: handles constructs that introduce NEW typed variables
|
|
545
|
-
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
|
|
913
|
+
// via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`)
|
|
914
|
+
// or narrow existing variables within a branch (null-check narrowing).
|
|
546
915
|
// Runs after Tier 0/1 so scopeEnv already contains the source variable's type.
|
|
547
916
|
// Conservative: extractor returns undefined when source type is unknown.
|
|
548
917
|
if (config.extractPatternBinding && (!config.patternBindingNodeTypes || config.patternBindingNodeTypes.has(node.type))) {
|
|
@@ -552,11 +921,25 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
552
921
|
const scopeEnv = env.get(scope);
|
|
553
922
|
const patternBinding = config.extractPatternBinding(node, scopeEnv, declarationTypeNodes, scope);
|
|
554
923
|
if (patternBinding) {
|
|
555
|
-
if (
|
|
924
|
+
if (patternBinding.narrowingRange) {
|
|
925
|
+
// Explicit narrowing range (null-check narrowing): always store in patternOverrides
|
|
926
|
+
// using the extractor-provided range (typically the if-body block).
|
|
927
|
+
if (!patternOverrides.has(scope))
|
|
928
|
+
patternOverrides.set(scope, new Map());
|
|
929
|
+
const varMap = patternOverrides.get(scope);
|
|
930
|
+
if (!varMap.has(patternBinding.varName))
|
|
931
|
+
varMap.set(patternBinding.varName, []);
|
|
932
|
+
varMap.get(patternBinding.varName).push({
|
|
933
|
+
rangeStart: patternBinding.narrowingRange.startIndex,
|
|
934
|
+
rangeEnd: patternBinding.narrowingRange.endIndex,
|
|
935
|
+
typeName: patternBinding.typeName,
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
else if (config.allowPatternBindingOverwrite) {
|
|
556
939
|
// Position-indexed: store per-branch binding for smart-cast narrowing.
|
|
557
940
|
// Each when arm / switch case gets its own type for the variable,
|
|
558
941
|
// preventing cross-arm contamination (e.g., Kotlin when/is).
|
|
559
|
-
const branchNode =
|
|
942
|
+
const branchNode = findNarrowingBranchScope(node);
|
|
560
943
|
if (branchNode) {
|
|
561
944
|
if (!patternOverrides.has(scope))
|
|
562
945
|
patternOverrides.set(scope, new Map());
|
|
@@ -583,6 +966,7 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
583
966
|
// Delegates to per-language extractPendingAssignment — AST shapes differ widely
|
|
584
967
|
// (JS uses variable_declarator/name/value, Rust uses let_declaration/pattern/value,
|
|
585
968
|
// Python uses assignment/left/right, Go uses short_var_declaration/expression_list).
|
|
969
|
+
// May return a single item or an array (for destructuring: N fieldAccess items).
|
|
586
970
|
if (config.extractPendingAssignment && config.declarationNodeTypes.has(node.type)) {
|
|
587
971
|
// scopeEnv is guaranteed to exist here because declarationNodeTypes is a subset
|
|
588
972
|
// of interestingNodeTypes, so extractTypeBinding already created the scope map above.
|
|
@@ -590,7 +974,12 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
590
974
|
if (scopeEnv) {
|
|
591
975
|
const pending = config.extractPendingAssignment(node, scopeEnv);
|
|
592
976
|
if (pending) {
|
|
593
|
-
|
|
977
|
+
const items = Array.isArray(pending) ? pending : [pending];
|
|
978
|
+
for (const item of items) {
|
|
979
|
+
// Substitute this/self/$this/Me receivers with enclosing class name
|
|
980
|
+
const resolved = substituteThisReceiver(item, node);
|
|
981
|
+
pendingItems.push({ scope, ...resolved });
|
|
982
|
+
}
|
|
594
983
|
}
|
|
595
984
|
}
|
|
596
985
|
}
|
|
@@ -613,55 +1002,41 @@ export const buildTypeEnv = (tree, language, symbolTable) => {
|
|
|
613
1002
|
}
|
|
614
1003
|
};
|
|
615
1004
|
walk(tree.rootNode, FILE_SCOPE);
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1005
|
+
// Phase 14: Seed cross-file bindings from upstream files AFTER walk
|
|
1006
|
+
// (local declarations from walk() take precedence — first-writer-wins)
|
|
1007
|
+
if (options?.importedBindings && options.importedBindings.size > 0) {
|
|
1008
|
+
seedImportedBindings(env, options.importedBindings);
|
|
1009
|
+
}
|
|
1010
|
+
resolveFixpointBindings(pendingItems, env, returnTypeLookup, symbolTable, parentMap);
|
|
1011
|
+
// Post-fixpoint for-loop replay (Phase 10 / ex-9B loop-fixpoint bridge):
|
|
1012
|
+
// For-loop nodes whose iterables were unresolved at walk-time may now be
|
|
1013
|
+
// resolvable because the fixpoint bound the iterable's type.
|
|
1014
|
+
// Example: `const users = getUsers(); for (const u of users) { u.save(); }`
|
|
1015
|
+
// - walk-time: users untyped → u unresolved
|
|
1016
|
+
// - fixpoint: users → User[]
|
|
1017
|
+
// - replay: users now typed → u → User
|
|
1018
|
+
if (pendingForLoops.length > 0 && config.extractForLoopBinding) {
|
|
1019
|
+
for (const { node, scope } of pendingForLoops) {
|
|
1020
|
+
if (!env.has(scope))
|
|
1021
|
+
env.set(scope, new Map());
|
|
1022
|
+
const scopeEnv = env.get(scope);
|
|
1023
|
+
config.extractForLoopBinding(node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup });
|
|
1024
|
+
}
|
|
1025
|
+
// Re-run the main fixpoint to resolve items that depended on loop variables.
|
|
1026
|
+
// Only needed if replay actually produced new bindings.
|
|
1027
|
+
const unresolvedBefore = pendingItems.filter((item) => {
|
|
633
1028
|
const scopeEnv = env.get(item.scope);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
let typeName;
|
|
639
|
-
switch (item.kind) {
|
|
640
|
-
case 'callResult':
|
|
641
|
-
typeName = returnTypeLookup.lookupReturnType(item.callee);
|
|
642
|
-
break;
|
|
643
|
-
case 'copy':
|
|
644
|
-
typeName = scopeEnv.get(item.rhs) ?? env.get(FILE_SCOPE)?.get(item.rhs);
|
|
645
|
-
break;
|
|
646
|
-
case 'fieldAccess':
|
|
647
|
-
typeName = resolveFieldType(item.receiver, item.field, scopeEnv, symbolTable);
|
|
648
|
-
break;
|
|
649
|
-
case 'methodCallResult':
|
|
650
|
-
typeName = resolveMethodReturnType(item.receiver, item.method, scopeEnv, symbolTable);
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
if (typeName) {
|
|
654
|
-
scopeEnv.set(item.lhs, typeName);
|
|
655
|
-
resolved.add(i);
|
|
656
|
-
changed = true;
|
|
657
|
-
}
|
|
1029
|
+
return scopeEnv && !scopeEnv.has(item.lhs);
|
|
1030
|
+
});
|
|
1031
|
+
if (unresolvedBefore.length > 0) {
|
|
1032
|
+
resolveFixpointBindings(unresolvedBefore, env, returnTypeLookup, symbolTable);
|
|
658
1033
|
}
|
|
659
|
-
if (!changed)
|
|
660
|
-
break;
|
|
661
1034
|
}
|
|
662
1035
|
return {
|
|
663
|
-
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides),
|
|
1036
|
+
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode, patternOverrides, options?.enclosingFunctionFinder),
|
|
664
1037
|
constructorBindings: bindings,
|
|
665
|
-
env,
|
|
1038
|
+
fileScope: () => env.get(FILE_SCOPE) ?? EMPTY_FILE_SCOPE,
|
|
1039
|
+
allScopes: () => env,
|
|
1040
|
+
constructorTypeMap,
|
|
666
1041
|
};
|
|
667
1042
|
};
|