gitnexus 1.4.0 → 1.4.5
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 +19 -18
- package/dist/cli/analyze.js +37 -28
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +1 -1
- package/dist/cli/eval-server.js +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/setup.js +25 -13
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +1 -1
- package/dist/cli/tool.js +2 -2
- package/dist/cli/wiki.js +2 -2
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -1
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/ingestion/call-processor.d.ts +6 -7
- package/dist/core/ingestion/call-processor.js +490 -127
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/entry-point-scoring.js +13 -2
- package/dist/core/ingestion/export-detection.js +1 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.js +9 -0
- package/dist/core/ingestion/heritage-processor.d.ts +3 -4
- package/dist/core/ingestion/heritage-processor.js +40 -50
- package/dist/core/ingestion/import-processor.d.ts +3 -5
- package/dist/core/ingestion/import-processor.js +41 -10
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +41 -4
- package/dist/core/ingestion/pipeline.d.ts +5 -1
- package/dist/core/ingestion/pipeline.js +174 -121
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/index.d.ts +2 -0
- package/dist/core/ingestion/resolvers/index.js +2 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/standard.js +0 -22
- package/dist/core/ingestion/resolvers/utils.js +2 -0
- package/dist/core/ingestion/symbol-table.d.ts +3 -0
- package/dist/core/ingestion/symbol-table.js +1 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +3 -2
- package/dist/core/ingestion/tree-sitter-queries.js +53 -1
- package/dist/core/ingestion/type-env.d.ts +32 -10
- package/dist/core/ingestion/type-env.js +520 -47
- package/dist/core/ingestion/type-extractors/c-cpp.js +326 -1
- package/dist/core/ingestion/type-extractors/csharp.js +282 -2
- package/dist/core/ingestion/type-extractors/go.js +333 -2
- package/dist/core/ingestion/type-extractors/index.d.ts +3 -2
- package/dist/core/ingestion/type-extractors/index.js +3 -1
- package/dist/core/ingestion/type-extractors/jvm.js +537 -4
- package/dist/core/ingestion/type-extractors/php.js +387 -7
- package/dist/core/ingestion/type-extractors/python.js +356 -5
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.js +399 -2
- package/dist/core/ingestion/type-extractors/shared.d.ts +116 -1
- package/dist/core/ingestion/type-extractors/shared.js +488 -14
- package/dist/core/ingestion/type-extractors/swift.js +95 -1
- package/dist/core/ingestion/type-extractors/types.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/typescript.js +436 -2
- package/dist/core/ingestion/utils.d.ts +33 -2
- package/dist/core/ingestion/utils.js +399 -27
- package/dist/core/ingestion/workers/parse-worker.d.ts +18 -1
- package/dist/core/ingestion/workers/parse-worker.js +169 -19
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/csv-generator.js +1 -1
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +70 -65
- package/dist/core/{kuzu → lbug}/schema.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/schema.js +1 -1
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +10 -10
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.js +9 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -7
- package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +72 -43
- package/dist/mcp/local/local-backend.d.ts +6 -6
- package/dist/mcp/local/local-backend.js +25 -18
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +55 -1
- package/dist/types/pipeline.d.ts +1 -1
- package/package.json +5 -3
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, findChildByType, hasTypeAnnotation } from './shared.js';
|
|
2
2
|
const DECLARATION_NODE_TYPES = new Set([
|
|
3
3
|
'property_declaration',
|
|
4
4
|
]);
|
|
@@ -36,8 +36,102 @@ const extractParameter = (node, env) => {
|
|
|
36
36
|
if (varName && typeName)
|
|
37
37
|
env.set(varName, typeName);
|
|
38
38
|
};
|
|
39
|
+
/** Swift: let user = User(name: "alice") — infer type from call when callee is a known class.
|
|
40
|
+
* Swift initializers are syntactically identical to function calls, so we verify
|
|
41
|
+
* against classNames (which may include cross-file SymbolTable lookups). */
|
|
42
|
+
const extractInitializer = (node, env, classNames) => {
|
|
43
|
+
if (node.type !== 'property_declaration')
|
|
44
|
+
return;
|
|
45
|
+
// Skip if has type annotation — extractDeclaration handled it
|
|
46
|
+
if (node.childForFieldName('type') || findChildByType(node, 'type_annotation'))
|
|
47
|
+
return;
|
|
48
|
+
// Find pattern (variable name)
|
|
49
|
+
const pattern = node.childForFieldName('pattern') ?? findChildByType(node, 'pattern');
|
|
50
|
+
if (!pattern)
|
|
51
|
+
return;
|
|
52
|
+
const varName = extractVarName(pattern) ?? pattern.text;
|
|
53
|
+
if (!varName || env.has(varName))
|
|
54
|
+
return;
|
|
55
|
+
// Find call_expression in the value
|
|
56
|
+
const callExpr = findChildByType(node, 'call_expression');
|
|
57
|
+
if (!callExpr)
|
|
58
|
+
return;
|
|
59
|
+
const callee = callExpr.firstNamedChild;
|
|
60
|
+
if (!callee)
|
|
61
|
+
return;
|
|
62
|
+
// Direct call: User(name: "alice")
|
|
63
|
+
if (callee.type === 'simple_identifier') {
|
|
64
|
+
const calleeName = callee.text;
|
|
65
|
+
if (calleeName && classNames.has(calleeName)) {
|
|
66
|
+
env.set(varName, calleeName);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Explicit init: User.init(name: "alice") — navigation_expression with .init suffix
|
|
71
|
+
if (callee.type === 'navigation_expression') {
|
|
72
|
+
const receiver = callee.firstNamedChild;
|
|
73
|
+
const suffix = callee.lastNamedChild;
|
|
74
|
+
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
|
|
75
|
+
const calleeName = receiver.text;
|
|
76
|
+
if (calleeName && classNames.has(calleeName)) {
|
|
77
|
+
env.set(varName, calleeName);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** Swift: let user = User(name: "alice") — scan property_declaration for constructor binding */
|
|
83
|
+
const scanConstructorBinding = (node) => {
|
|
84
|
+
if (node.type !== 'property_declaration')
|
|
85
|
+
return undefined;
|
|
86
|
+
if (hasTypeAnnotation(node))
|
|
87
|
+
return undefined;
|
|
88
|
+
const pattern = node.childForFieldName('pattern');
|
|
89
|
+
if (!pattern)
|
|
90
|
+
return undefined;
|
|
91
|
+
const varName = pattern.text;
|
|
92
|
+
if (!varName)
|
|
93
|
+
return undefined;
|
|
94
|
+
let callExpr = null;
|
|
95
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
96
|
+
const child = node.namedChild(i);
|
|
97
|
+
if (child?.type === 'call_expression') {
|
|
98
|
+
callExpr = child;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!callExpr)
|
|
103
|
+
return undefined;
|
|
104
|
+
const callee = callExpr.firstNamedChild;
|
|
105
|
+
if (!callee)
|
|
106
|
+
return undefined;
|
|
107
|
+
if (callee.type === 'simple_identifier') {
|
|
108
|
+
return { varName, calleeName: callee.text };
|
|
109
|
+
}
|
|
110
|
+
if (callee.type === 'navigation_expression') {
|
|
111
|
+
const receiver = callee.firstNamedChild;
|
|
112
|
+
const suffix = callee.lastNamedChild;
|
|
113
|
+
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
|
|
114
|
+
return { varName, calleeName: receiver.text };
|
|
115
|
+
}
|
|
116
|
+
// General qualified call: service.getUser() → extract method name.
|
|
117
|
+
// tree-sitter-swift may wrap the identifier in navigation_suffix, so
|
|
118
|
+
// check both direct simple_identifier and navigation_suffix > simple_identifier.
|
|
119
|
+
if (suffix?.type === 'simple_identifier') {
|
|
120
|
+
return { varName, calleeName: suffix.text };
|
|
121
|
+
}
|
|
122
|
+
if (suffix?.type === 'navigation_suffix') {
|
|
123
|
+
const inner = suffix.lastNamedChild;
|
|
124
|
+
if (inner?.type === 'simple_identifier') {
|
|
125
|
+
return { varName, calleeName: inner.text };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
};
|
|
39
131
|
export const typeConfig = {
|
|
40
132
|
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
|
41
133
|
extractDeclaration,
|
|
42
134
|
extractParameter,
|
|
135
|
+
extractInitializer,
|
|
136
|
+
scanConstructorBinding,
|
|
43
137
|
};
|
|
@@ -3,12 +3,93 @@ import type { SyntaxNode } from '../utils.js';
|
|
|
3
3
|
export type TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>) => void;
|
|
4
4
|
/** Extracts type bindings from a parameter node into the env map */
|
|
5
5
|
export type ParameterExtractor = (node: SyntaxNode, env: Map<string, string>) => void;
|
|
6
|
+
/** Minimal interface for checking whether a name is a known class/struct.
|
|
7
|
+
* Narrower than ReadonlySet — only `.has()` is used by extractors. */
|
|
8
|
+
export type ClassNameLookup = {
|
|
9
|
+
has(name: string): boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Extracts type bindings from a constructor-call initializer, with access to known class names */
|
|
12
|
+
export type InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup) => void;
|
|
13
|
+
/** Scans an AST node for untyped `var = callee()` patterns for return-type inference.
|
|
14
|
+
* Returns { varName, calleeName } if the node matches, undefined otherwise.
|
|
15
|
+
* `receiverClassName` — optional hint for method calls on known receivers
|
|
16
|
+
* (e.g. $this->getUser() in PHP provides the enclosing class name). */
|
|
17
|
+
export type ConstructorBindingScanner = (node: SyntaxNode) => {
|
|
18
|
+
varName: string;
|
|
19
|
+
calleeName: string;
|
|
20
|
+
receiverClassName?: string;
|
|
21
|
+
} | undefined;
|
|
22
|
+
/** Extracts a return type string from a method/function definition node.
|
|
23
|
+
* Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
|
|
24
|
+
* rather than in AST fields. Returns undefined if no return type can be determined. */
|
|
25
|
+
export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
|
|
26
|
+
/** Extracts loop variable type binding from a for-each statement.
|
|
27
|
+
* All parameters are required (aligned with PatternBindingExtractor convention)
|
|
28
|
+
* to prevent new extractors from silently ignoring declarationTypeNodes/scope. */
|
|
29
|
+
export type ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>, declarationTypeNodes: ReadonlyMap<string, SyntaxNode>, scope: string) => void;
|
|
30
|
+
/** Extracts a plain-identifier assignment for Tier 2 propagation.
|
|
31
|
+
* For `const b = a`, returns { lhs: 'b', rhs: 'a' } when the LHS has no resolved type.
|
|
32
|
+
* Returns undefined if the node is not a plain identifier assignment. */
|
|
33
|
+
export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => {
|
|
34
|
+
lhs: string;
|
|
35
|
+
rhs: string;
|
|
36
|
+
} | undefined;
|
|
37
|
+
/** Extracts a typed variable binding from a pattern-matching construct.
|
|
38
|
+
* Returns { varName, typeName } for patterns that introduce NEW variables.
|
|
39
|
+
* Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java).
|
|
40
|
+
* Conservative: returns undefined when the source variable's type is unknown.
|
|
41
|
+
*
|
|
42
|
+
* @param scopeEnv Read-only view of already-resolved type bindings in the current scope.
|
|
43
|
+
* @param declarationTypeNodes Maps `scope\0varName` to the original declaration's type
|
|
44
|
+
* annotation AST node. Allows extracting generic type arguments (e.g., T from Result<T,E>)
|
|
45
|
+
* that are stripped during normal TypeEnv extraction.
|
|
46
|
+
* @param scope Current scope key (e.g. `"process@42"`) for declarationTypeNodes lookups. */
|
|
47
|
+
export type PatternBindingExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>, declarationTypeNodes: ReadonlyMap<string, SyntaxNode>, scope: string) => {
|
|
48
|
+
varName: string;
|
|
49
|
+
typeName: string;
|
|
50
|
+
} | undefined;
|
|
6
51
|
/** Per-language type extraction configuration */
|
|
7
52
|
export interface LanguageTypeConfig {
|
|
53
|
+
/** Allow pattern binding to overwrite existing scopeEnv entries.
|
|
54
|
+
* WARNING: Enables function-scope type pollution. Only for languages with
|
|
55
|
+
* smart-cast semantics (e.g., Kotlin `when/is`) where the subject variable
|
|
56
|
+
* already exists in scopeEnv from its declaration. */
|
|
57
|
+
readonly allowPatternBindingOverwrite?: boolean;
|
|
8
58
|
/** Node types that represent typed declarations for this language */
|
|
9
59
|
declarationNodeTypes: ReadonlySet<string>;
|
|
60
|
+
/** AST node types for for-each/for-in statements with explicit element types. */
|
|
61
|
+
forLoopNodeTypes?: ReadonlySet<string>;
|
|
62
|
+
/** Optional allowlist of AST node types on which extractPatternBinding should run.
|
|
63
|
+
* When present, extractPatternBinding is only invoked for nodes whose type is in this set,
|
|
64
|
+
* short-circuiting the call for all other node types. When absent, every node is passed to
|
|
65
|
+
* extractPatternBinding (legacy behaviour). */
|
|
66
|
+
patternBindingNodeTypes?: ReadonlySet<string>;
|
|
10
67
|
/** Extract a (varName → typeName) binding from a declaration node */
|
|
11
68
|
extractDeclaration: TypeBindingExtractor;
|
|
12
69
|
/** Extract a (varName → typeName) binding from a parameter node */
|
|
13
70
|
extractParameter: ParameterExtractor;
|
|
71
|
+
/** Extract a (varName → typeName) binding from a constructor-call initializer.
|
|
72
|
+
* Called as fallback when extractDeclaration produces no binding for a declaration node.
|
|
73
|
+
* Only for languages with syntactic constructor markers (new, composite_literal, ::new).
|
|
74
|
+
* Receives classNames — the set of class/struct names visible in the current file's AST. */
|
|
75
|
+
extractInitializer?: InitializerExtractor;
|
|
76
|
+
/** Scan for untyped `var = callee()` assignments for return-type inference.
|
|
77
|
+
* Called on every AST node during buildTypeEnv walk; returns undefined for non-matches.
|
|
78
|
+
* The callee binding is unverified — the caller must confirm against the SymbolTable. */
|
|
79
|
+
scanConstructorBinding?: ConstructorBindingScanner;
|
|
80
|
+
/** Extract return type from comment-based annotations (e.g. YARD @return [Type]).
|
|
81
|
+
* Called as fallback when extractMethodSignature finds no AST-based return type. */
|
|
82
|
+
extractReturnType?: ReturnTypeExtractor;
|
|
83
|
+
/** Extract loop variable → type binding from a for-each AST node. */
|
|
84
|
+
extractForLoopBinding?: ForLoopExtractor;
|
|
85
|
+
/** Extract plain-identifier assignment (e.g. `const b = a`) for Tier 2 chain propagation.
|
|
86
|
+
* Called on declaration/assignment nodes; returns {lhs, rhs} when the RHS is a bare identifier
|
|
87
|
+
* and the LHS has no resolved type yet. Language-specific because AST shapes differ widely. */
|
|
88
|
+
extractPendingAssignment?: PendingAssignmentExtractor;
|
|
89
|
+
/** Extract a typed variable binding from a pattern-matching construct.
|
|
90
|
+
* Called on every AST node; returns { varName, typeName } when the node introduces a new
|
|
91
|
+
* typed variable via pattern matching (e.g. `if let Some(x) = opt`, `x instanceof T t`).
|
|
92
|
+
* The extractor receives the current scope's resolved bindings (read-only) to look up the
|
|
93
|
+
* source variable's type. Returns undefined for non-matching nodes or unknown source types. */
|
|
94
|
+
extractPatternBinding?: PatternBindingExtractor;
|
|
14
95
|
}
|
|
@@ -1,10 +1,97 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractCalleeName, extractElementTypeFromString, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition } from './shared.js';
|
|
2
2
|
const DECLARATION_NODE_TYPES = new Set([
|
|
3
3
|
'lexical_declaration',
|
|
4
4
|
'variable_declaration',
|
|
5
|
+
'function_declaration', // JSDoc @param on function declarations
|
|
6
|
+
'method_definition', // JSDoc @param on class methods
|
|
7
|
+
'public_field_definition', // class field: private users: User[]
|
|
5
8
|
]);
|
|
6
|
-
|
|
9
|
+
const normalizeJsDocType = (raw) => {
|
|
10
|
+
let type = raw.trim();
|
|
11
|
+
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
|
12
|
+
if (type.startsWith('?') || type.startsWith('!'))
|
|
13
|
+
type = type.slice(1);
|
|
14
|
+
// Strip union with null/undefined/void: User|null → User
|
|
15
|
+
const parts = type.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void');
|
|
16
|
+
if (parts.length !== 1)
|
|
17
|
+
return undefined; // ambiguous union
|
|
18
|
+
type = parts[0];
|
|
19
|
+
// Strip module: prefix — module:models.User → models.User
|
|
20
|
+
if (type.startsWith('module:'))
|
|
21
|
+
type = type.slice(7);
|
|
22
|
+
// Take last segment of dotted path: models.User → User
|
|
23
|
+
const segments = type.split('.');
|
|
24
|
+
type = segments[segments.length - 1];
|
|
25
|
+
// Strip generic wrapper: Promise<User> → Promise (base type, not inner)
|
|
26
|
+
const genericMatch = type.match(/^(\w+)\s*</);
|
|
27
|
+
if (genericMatch)
|
|
28
|
+
type = genericMatch[1];
|
|
29
|
+
// Simple identifier check
|
|
30
|
+
if (/^\w+$/.test(type))
|
|
31
|
+
return type;
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
/** Regex to extract JSDoc @param annotations: `@param {Type} name` */
|
|
35
|
+
const JSDOC_PARAM_RE = /@param\s*\{([^}]+)\}\s+\[?(\w+)[\]=]?[^\s]*/g;
|
|
36
|
+
/**
|
|
37
|
+
* Collect JSDoc @param type bindings from comment nodes preceding a function/method.
|
|
38
|
+
* Returns a map of paramName → typeName.
|
|
39
|
+
*/
|
|
40
|
+
const collectJsDocParams = (funcNode) => {
|
|
41
|
+
const commentTexts = [];
|
|
42
|
+
let sibling = funcNode.previousSibling;
|
|
43
|
+
while (sibling) {
|
|
44
|
+
if (sibling.type === 'comment') {
|
|
45
|
+
commentTexts.unshift(sibling.text);
|
|
46
|
+
}
|
|
47
|
+
else if (sibling.isNamed && sibling.type !== 'decorator') {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
sibling = sibling.previousSibling;
|
|
51
|
+
}
|
|
52
|
+
if (commentTexts.length === 0)
|
|
53
|
+
return new Map();
|
|
54
|
+
const params = new Map();
|
|
55
|
+
const commentBlock = commentTexts.join('\n');
|
|
56
|
+
JSDOC_PARAM_RE.lastIndex = 0;
|
|
57
|
+
let match;
|
|
58
|
+
while ((match = JSDOC_PARAM_RE.exec(commentBlock)) !== null) {
|
|
59
|
+
const typeName = normalizeJsDocType(match[1]);
|
|
60
|
+
const paramName = match[2];
|
|
61
|
+
if (typeName) {
|
|
62
|
+
params.set(paramName, typeName);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return params;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* TypeScript: const x: Foo = ..., let x: Foo
|
|
69
|
+
* Also: JSDoc @param annotations on function/method definitions (for .js files).
|
|
70
|
+
*/
|
|
7
71
|
const extractDeclaration = (node, env) => {
|
|
72
|
+
// JSDoc @param on functions/methods — pre-populate env with param types
|
|
73
|
+
if (node.type === 'function_declaration' || node.type === 'method_definition') {
|
|
74
|
+
const jsDocParams = collectJsDocParams(node);
|
|
75
|
+
for (const [paramName, typeName] of jsDocParams) {
|
|
76
|
+
if (!env.has(paramName))
|
|
77
|
+
env.set(paramName, typeName);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Class field: `private users: User[]` — public_field_definition has name + type fields directly.
|
|
82
|
+
if (node.type === 'public_field_definition') {
|
|
83
|
+
const nameNode = node.childForFieldName('name');
|
|
84
|
+
const typeAnnotation = node.childForFieldName('type');
|
|
85
|
+
if (!nameNode || !typeAnnotation)
|
|
86
|
+
return;
|
|
87
|
+
const varName = nameNode.text;
|
|
88
|
+
if (!varName)
|
|
89
|
+
return;
|
|
90
|
+
const typeName = extractSimpleTypeName(typeAnnotation);
|
|
91
|
+
if (typeName)
|
|
92
|
+
env.set(varName, typeName);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
8
95
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
9
96
|
const declarator = node.namedChild(i);
|
|
10
97
|
if (declarator?.type !== 'variable_declarator')
|
|
@@ -39,8 +126,355 @@ const extractParameter = (node, env) => {
|
|
|
39
126
|
if (varName && typeName)
|
|
40
127
|
env.set(varName, typeName);
|
|
41
128
|
};
|
|
129
|
+
/** TypeScript: const x = new User() — infer type from new_expression */
|
|
130
|
+
const extractInitializer = (node, env, _classNames) => {
|
|
131
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
132
|
+
const declarator = node.namedChild(i);
|
|
133
|
+
if (declarator?.type !== 'variable_declarator')
|
|
134
|
+
continue;
|
|
135
|
+
// Only activate when there is no explicit type annotation — extractDeclaration already
|
|
136
|
+
// handles the annotated case and this function is called as a fallback.
|
|
137
|
+
if (declarator.childForFieldName('type') !== null)
|
|
138
|
+
continue;
|
|
139
|
+
let valueNode = declarator.childForFieldName('value');
|
|
140
|
+
// Unwrap `new User() as T`, `new User()!`, and double-cast `new User() as unknown as T`
|
|
141
|
+
while (valueNode?.type === 'as_expression' || valueNode?.type === 'non_null_expression') {
|
|
142
|
+
valueNode = valueNode.firstNamedChild;
|
|
143
|
+
}
|
|
144
|
+
if (valueNode?.type !== 'new_expression')
|
|
145
|
+
continue;
|
|
146
|
+
const constructorNode = valueNode.childForFieldName('constructor');
|
|
147
|
+
if (!constructorNode)
|
|
148
|
+
continue;
|
|
149
|
+
const nameNode = declarator.childForFieldName('name');
|
|
150
|
+
if (!nameNode)
|
|
151
|
+
continue;
|
|
152
|
+
const varName = extractVarName(nameNode);
|
|
153
|
+
const typeName = extractSimpleTypeName(constructorNode);
|
|
154
|
+
if (varName && typeName)
|
|
155
|
+
env.set(varName, typeName);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* TypeScript/JavaScript: const user = getUser() — variable_declarator with call_expression value.
|
|
160
|
+
* Only matches unannotated declarators; annotated ones are handled by extractDeclaration.
|
|
161
|
+
* await is unwrapped: const user = await fetchUser() → callee = 'fetchUser'.
|
|
162
|
+
*/
|
|
163
|
+
const scanConstructorBinding = (node) => {
|
|
164
|
+
if (node.type !== 'variable_declarator')
|
|
165
|
+
return undefined;
|
|
166
|
+
if (hasTypeAnnotation(node))
|
|
167
|
+
return undefined;
|
|
168
|
+
const nameNode = node.childForFieldName('name');
|
|
169
|
+
if (!nameNode || nameNode.type !== 'identifier')
|
|
170
|
+
return undefined;
|
|
171
|
+
const value = unwrapAwait(node.childForFieldName('value'));
|
|
172
|
+
if (!value || value.type !== 'call_expression')
|
|
173
|
+
return undefined;
|
|
174
|
+
const calleeName = extractCalleeName(value);
|
|
175
|
+
if (!calleeName)
|
|
176
|
+
return undefined;
|
|
177
|
+
return { varName: nameNode.text, calleeName };
|
|
178
|
+
};
|
|
179
|
+
/** Regex to extract @returns or @return from JSDoc comments: `@returns {Type}` */
|
|
180
|
+
const JSDOC_RETURN_RE = /@returns?\s*\{([^}]+)\}/;
|
|
181
|
+
/**
|
|
182
|
+
* Minimal sanitization for JSDoc return types — preserves generic wrappers
|
|
183
|
+
* (e.g. `Promise<User>`) so that extractReturnTypeName in call-processor
|
|
184
|
+
* can apply WRAPPER_GENERICS unwrapping. Unlike normalizeJsDocType (which
|
|
185
|
+
* strips generics), this only strips JSDoc-specific syntax markers.
|
|
186
|
+
*/
|
|
187
|
+
const sanitizeReturnType = (raw) => {
|
|
188
|
+
let type = raw.trim();
|
|
189
|
+
// Strip JSDoc nullable/non-nullable prefixes: ?User → User, !User → User
|
|
190
|
+
if (type.startsWith('?') || type.startsWith('!'))
|
|
191
|
+
type = type.slice(1);
|
|
192
|
+
// Strip module: prefix — module:models.User → models.User
|
|
193
|
+
if (type.startsWith('module:'))
|
|
194
|
+
type = type.slice(7);
|
|
195
|
+
// Reject unions (ambiguous)
|
|
196
|
+
if (type.includes('|'))
|
|
197
|
+
return undefined;
|
|
198
|
+
if (!type)
|
|
199
|
+
return undefined;
|
|
200
|
+
return type;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Extract return type from JSDoc `@returns {Type}` or `@return {Type}` annotation
|
|
204
|
+
* preceding a function/method definition. Walks backwards through preceding siblings
|
|
205
|
+
* looking for comment nodes containing the annotation.
|
|
206
|
+
*/
|
|
207
|
+
const extractReturnType = (node) => {
|
|
208
|
+
let sibling = node.previousSibling;
|
|
209
|
+
while (sibling) {
|
|
210
|
+
if (sibling.type === 'comment') {
|
|
211
|
+
const match = JSDOC_RETURN_RE.exec(sibling.text);
|
|
212
|
+
if (match)
|
|
213
|
+
return sanitizeReturnType(match[1]);
|
|
214
|
+
}
|
|
215
|
+
else if (sibling.isNamed && sibling.type !== 'decorator')
|
|
216
|
+
break;
|
|
217
|
+
sibling = sibling.previousSibling;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
};
|
|
221
|
+
const FOR_LOOP_NODE_TYPES = new Set([
|
|
222
|
+
'for_in_statement',
|
|
223
|
+
]);
|
|
224
|
+
/** TS function/method node types that carry a parameters list. */
|
|
225
|
+
const TS_FUNCTION_NODE_TYPES = new Set([
|
|
226
|
+
'function_declaration', 'function_expression', 'arrow_function',
|
|
227
|
+
'method_definition', 'generator_function', 'generator_function_declaration',
|
|
228
|
+
]);
|
|
229
|
+
/**
|
|
230
|
+
* Extract element type from a TypeScript type annotation AST node.
|
|
231
|
+
* Handles:
|
|
232
|
+
* type_annotation ": User[]" → array_type → type_identifier "User"
|
|
233
|
+
* type_annotation ": Array<User>" → generic_type → extractGenericTypeArgs → "User"
|
|
234
|
+
* Falls back to text-based extraction via extractElementTypeFromString.
|
|
235
|
+
*/
|
|
236
|
+
const extractTsElementTypeFromAnnotation = (typeAnnotation, pos = 'last', depth = 0) => {
|
|
237
|
+
if (depth > 50)
|
|
238
|
+
return undefined;
|
|
239
|
+
// Unwrap type_annotation (the node text includes ': ' prefix)
|
|
240
|
+
const inner = typeAnnotation.type === 'type_annotation'
|
|
241
|
+
? (typeAnnotation.firstNamedChild ?? typeAnnotation)
|
|
242
|
+
: typeAnnotation;
|
|
243
|
+
// readonly User[] — readonly_type wraps array_type: unwrap and recurse
|
|
244
|
+
if (inner.type === 'readonly_type') {
|
|
245
|
+
const wrapped = inner.firstNamedChild;
|
|
246
|
+
if (wrapped)
|
|
247
|
+
return extractTsElementTypeFromAnnotation(wrapped, pos, depth + 1);
|
|
248
|
+
}
|
|
249
|
+
// User[] — array_type: first named child is the element type
|
|
250
|
+
if (inner.type === 'array_type') {
|
|
251
|
+
const elem = inner.firstNamedChild;
|
|
252
|
+
if (elem)
|
|
253
|
+
return extractSimpleTypeName(elem);
|
|
254
|
+
}
|
|
255
|
+
// Array<User>, Map<string, User> — generic_type
|
|
256
|
+
// pos determines which type arg: 'first' for keys, 'last' for values
|
|
257
|
+
if (inner.type === 'generic_type') {
|
|
258
|
+
const args = extractGenericTypeArgs(inner);
|
|
259
|
+
if (args.length >= 1)
|
|
260
|
+
return pos === 'first' ? args[0] : args[args.length - 1];
|
|
261
|
+
}
|
|
262
|
+
// Fallback: strip ': ' prefix from type_annotation text and use string extraction
|
|
263
|
+
const rawText = inner.text;
|
|
264
|
+
return extractElementTypeFromString(rawText, pos);
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Search a statement_block (function body) for a variable_declarator named `iterableName`
|
|
268
|
+
* that has a type annotation, preceding the given `beforeNode`.
|
|
269
|
+
* Returns the element type from the type annotation, or undefined.
|
|
270
|
+
*/
|
|
271
|
+
const findTsLocalDeclElementType = (iterableName, blockNode, beforeNode, pos = 'last') => {
|
|
272
|
+
for (let i = 0; i < blockNode.namedChildCount; i++) {
|
|
273
|
+
const stmt = blockNode.namedChild(i);
|
|
274
|
+
if (!stmt)
|
|
275
|
+
continue;
|
|
276
|
+
// Stop when we reach the for-loop itself
|
|
277
|
+
if (stmt === beforeNode || stmt.startIndex >= beforeNode.startIndex)
|
|
278
|
+
break;
|
|
279
|
+
// Look for lexical_declaration or variable_declaration
|
|
280
|
+
if (stmt.type !== 'lexical_declaration' && stmt.type !== 'variable_declaration')
|
|
281
|
+
continue;
|
|
282
|
+
for (let j = 0; j < stmt.namedChildCount; j++) {
|
|
283
|
+
const decl = stmt.namedChild(j);
|
|
284
|
+
if (decl?.type !== 'variable_declarator')
|
|
285
|
+
continue;
|
|
286
|
+
const nameNode = decl.childForFieldName('name');
|
|
287
|
+
if (nameNode?.text !== iterableName)
|
|
288
|
+
continue;
|
|
289
|
+
const typeAnnotation = decl.childForFieldName('type');
|
|
290
|
+
if (typeAnnotation)
|
|
291
|
+
return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Walk up the AST from a for-loop node to find the enclosing function scope,
|
|
298
|
+
* then search (1) its parameter list and (2) local declarations in the body
|
|
299
|
+
* for a variable named `iterableName` with a container type annotation.
|
|
300
|
+
* Returns the element type extracted from the annotation, or undefined.
|
|
301
|
+
*/
|
|
302
|
+
const findTsIterableElementType = (iterableName, startNode, pos = 'last') => {
|
|
303
|
+
let current = startNode.parent;
|
|
304
|
+
// Capture the immediate statement_block parent to search local declarations
|
|
305
|
+
const blockNode = current?.type === 'statement_block' ? current : null;
|
|
306
|
+
while (current) {
|
|
307
|
+
if (TS_FUNCTION_NODE_TYPES.has(current.type)) {
|
|
308
|
+
// Search function parameters
|
|
309
|
+
const paramsNode = current.childForFieldName('parameters')
|
|
310
|
+
?? current.childForFieldName('formal_parameters');
|
|
311
|
+
if (paramsNode) {
|
|
312
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
313
|
+
const param = paramsNode.namedChild(i);
|
|
314
|
+
if (!param)
|
|
315
|
+
continue;
|
|
316
|
+
const patternNode = param.childForFieldName('pattern') ?? param.childForFieldName('name');
|
|
317
|
+
if (patternNode?.text === iterableName) {
|
|
318
|
+
const typeAnnotation = param.childForFieldName('type');
|
|
319
|
+
if (typeAnnotation)
|
|
320
|
+
return extractTsElementTypeFromAnnotation(typeAnnotation, pos);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Search local declarations in the function body (statement_block)
|
|
325
|
+
if (blockNode) {
|
|
326
|
+
const result = findTsLocalDeclElementType(iterableName, blockNode, startNode, pos);
|
|
327
|
+
if (result)
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
break; // stop at the nearest function boundary
|
|
331
|
+
}
|
|
332
|
+
current = current.parent;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* TypeScript/JavaScript: for (const user of users) where users has a known array type.
|
|
338
|
+
*
|
|
339
|
+
* Both `for...of` and `for...in` use the same `for_in_statement` AST node in tree-sitter.
|
|
340
|
+
* We differentiate by checking for the `of` keyword among the unnamed children.
|
|
341
|
+
*
|
|
342
|
+
* Tier 1c: resolves the element type via three strategies in priority order:
|
|
343
|
+
* 1. declarationTypeNodes — raw type annotation AST node (covers Array<User> from declarations)
|
|
344
|
+
* 2. scopeEnv string — extractElementTypeFromString on the stored type (covers locally annotated vars)
|
|
345
|
+
* 3. AST walk — walks up to the enclosing function's parameters to read User[] annotations directly
|
|
346
|
+
* Only handles `for...of`; `for...in` produces string keys, not element types.
|
|
347
|
+
*/
|
|
348
|
+
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
349
|
+
if (node.type !== 'for_in_statement')
|
|
350
|
+
return;
|
|
351
|
+
// Confirm this is `for...of`, not `for...in`, by scanning unnamed children for the keyword text.
|
|
352
|
+
let isForOf = false;
|
|
353
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
354
|
+
const child = node.child(i);
|
|
355
|
+
if (child && !child.isNamed && child.text === 'of') {
|
|
356
|
+
isForOf = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!isForOf)
|
|
361
|
+
return;
|
|
362
|
+
// The iterable is the `right` field — may be identifier or call_expression.
|
|
363
|
+
const rightNode = node.childForFieldName('right');
|
|
364
|
+
let iterableName;
|
|
365
|
+
let methodName;
|
|
366
|
+
if (rightNode?.type === 'identifier') {
|
|
367
|
+
iterableName = rightNode.text;
|
|
368
|
+
}
|
|
369
|
+
else if (rightNode?.type === 'member_expression') {
|
|
370
|
+
const prop = rightNode.childForFieldName('property');
|
|
371
|
+
if (prop)
|
|
372
|
+
iterableName = prop.text;
|
|
373
|
+
}
|
|
374
|
+
else if (rightNode?.type === 'call_expression') {
|
|
375
|
+
// entries.values() → call_expression > function: member_expression > object + property
|
|
376
|
+
// this.repos.values() → nested member_expression: extract property from inner member
|
|
377
|
+
const fn = rightNode.childForFieldName('function');
|
|
378
|
+
if (fn?.type === 'member_expression') {
|
|
379
|
+
const obj = fn.childForFieldName('object');
|
|
380
|
+
const prop = fn.childForFieldName('property');
|
|
381
|
+
if (obj?.type === 'identifier') {
|
|
382
|
+
iterableName = obj.text;
|
|
383
|
+
}
|
|
384
|
+
else if (obj?.type === 'member_expression') {
|
|
385
|
+
// this.repos.values() → obj = this.repos → extract 'repos'
|
|
386
|
+
const innerProp = obj.childForFieldName('property');
|
|
387
|
+
if (innerProp)
|
|
388
|
+
iterableName = innerProp.text;
|
|
389
|
+
}
|
|
390
|
+
if (prop?.type === 'property_identifier')
|
|
391
|
+
methodName = prop.text;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!iterableName)
|
|
395
|
+
return;
|
|
396
|
+
// Look up the container's base type name for descriptor-aware resolution
|
|
397
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
398
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
399
|
+
const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
|
|
400
|
+
if (!elementType)
|
|
401
|
+
return;
|
|
402
|
+
// The loop variable is the `left` field.
|
|
403
|
+
const leftNode = node.childForFieldName('left');
|
|
404
|
+
if (!leftNode)
|
|
405
|
+
return;
|
|
406
|
+
// Handle destructured for-of: for (const [k, v] of entries)
|
|
407
|
+
// AST: left = array_pattern directly (no variable_declarator wrapper)
|
|
408
|
+
// Bind the LAST identifier to the element type (value in [key, value] patterns)
|
|
409
|
+
if (leftNode.type === 'array_pattern') {
|
|
410
|
+
const lastChild = leftNode.lastNamedChild;
|
|
411
|
+
if (lastChild?.type === 'identifier') {
|
|
412
|
+
scopeEnv.set(lastChild.text, elementType);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (leftNode.type === 'object_pattern') {
|
|
417
|
+
// Object destructuring (e.g., `for (const { id } of users)`) destructures
|
|
418
|
+
// into fields of the element type. Without field-level resolution, we cannot
|
|
419
|
+
// bind individual properties to their correct types. Skip to avoid false bindings.
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
let loopVarNode = leftNode;
|
|
423
|
+
// `const user` parses as: left → variable_declarator containing an identifier named `user`
|
|
424
|
+
if (loopVarNode.type === 'variable_declarator') {
|
|
425
|
+
loopVarNode = loopVarNode.childForFieldName('name') ?? loopVarNode.firstNamedChild;
|
|
426
|
+
}
|
|
427
|
+
if (!loopVarNode)
|
|
428
|
+
return;
|
|
429
|
+
const loopVarName = extractVarName(loopVarNode);
|
|
430
|
+
if (loopVarName)
|
|
431
|
+
scopeEnv.set(loopVarName, elementType);
|
|
432
|
+
};
|
|
433
|
+
/** TS/JS: const alias = u → variable_declarator with name/value fields */
|
|
434
|
+
const extractPendingAssignment = (node, scopeEnv) => {
|
|
435
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
436
|
+
const child = node.namedChild(i);
|
|
437
|
+
if (!child || child.type !== 'variable_declarator')
|
|
438
|
+
continue;
|
|
439
|
+
const nameNode = child.childForFieldName('name');
|
|
440
|
+
const valueNode = child.childForFieldName('value');
|
|
441
|
+
if (!nameNode || !valueNode)
|
|
442
|
+
continue;
|
|
443
|
+
const lhs = nameNode.text;
|
|
444
|
+
if (scopeEnv.has(lhs))
|
|
445
|
+
continue;
|
|
446
|
+
if (valueNode.type === 'identifier')
|
|
447
|
+
return { lhs, rhs: valueNode.text };
|
|
448
|
+
}
|
|
449
|
+
return undefined;
|
|
450
|
+
};
|
|
451
|
+
/** TS instanceof narrowing: `x instanceof User` → bind x to User.
|
|
452
|
+
* Only works when x has no prior type binding (e.g. x: unknown, untyped params).
|
|
453
|
+
* Typed params (x: Animal) are blocked by the !scopeEnv.has() guard in buildTypeEnv.
|
|
454
|
+
* Uses first-writer-wins, same as Rust match arm bindings. */
|
|
455
|
+
const extractPatternBinding = (node) => {
|
|
456
|
+
if (node.type !== 'binary_expression')
|
|
457
|
+
return undefined;
|
|
458
|
+
const op = node.children.find(c => !c.isNamed && c.text === 'instanceof');
|
|
459
|
+
if (!op)
|
|
460
|
+
return undefined;
|
|
461
|
+
// binary_expression children are positional — no left/right fields
|
|
462
|
+
const left = node.namedChild(0);
|
|
463
|
+
const right = node.namedChild(1);
|
|
464
|
+
if (left?.type !== 'identifier' || right?.type !== 'identifier')
|
|
465
|
+
return undefined;
|
|
466
|
+
return { varName: left.text, typeName: right.text };
|
|
467
|
+
};
|
|
42
468
|
export const typeConfig = {
|
|
43
469
|
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
|
470
|
+
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
|
471
|
+
patternBindingNodeTypes: new Set(['binary_expression']),
|
|
44
472
|
extractDeclaration,
|
|
45
473
|
extractParameter,
|
|
474
|
+
extractInitializer,
|
|
475
|
+
scanConstructorBinding,
|
|
476
|
+
extractReturnType,
|
|
477
|
+
extractForLoopBinding,
|
|
478
|
+
extractPendingAssignment,
|
|
479
|
+
extractPatternBinding,
|
|
46
480
|
};
|