gitnexus 1.4.5 → 1.4.7
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/dist/cli/eval-server.js +13 -5
- package/dist/cli/index.js +0 -0
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +48 -13
- package/dist/core/graph/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -2
- package/dist/core/ingestion/call-processor.js +308 -235
- package/dist/core/ingestion/call-routing.d.ts +17 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +37 -8
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/symbol-table.d.ts +19 -3
- package/dist/core/ingestion/symbol-table.js +41 -2
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +200 -0
- package/dist/core/ingestion/type-env.js +126 -18
- package/dist/core/ingestion/type-extractors/c-cpp.js +28 -3
- package/dist/core/ingestion/type-extractors/csharp.js +61 -7
- package/dist/core/ingestion/type-extractors/go.js +86 -10
- package/dist/core/ingestion/type-extractors/jvm.js +122 -23
- package/dist/core/ingestion/type-extractors/php.js +172 -7
- package/dist/core/ingestion/type-extractors/python.js +107 -21
- package/dist/core/ingestion/type-extractors/ruby.js +18 -3
- package/dist/core/ingestion/type-extractors/rust.js +61 -14
- package/dist/core/ingestion/type-extractors/shared.d.ts +13 -0
- package/dist/core/ingestion/type-extractors/shared.js +243 -4
- package/dist/core/ingestion/type-extractors/types.d.ts +57 -12
- package/dist/core/ingestion/type-extractors/typescript.js +52 -8
- package/dist/core/ingestion/utils.d.ts +25 -0
- package/dist/core/ingestion/utils.js +160 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
- package/dist/core/ingestion/workers/parse-worker.js +73 -28
- package/dist/core/lbug/lbug-adapter.d.ts +2 -0
- package/dist/core/lbug/lbug-adapter.js +2 -0
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +167 -23
- package/dist/mcp/local/local-backend.d.ts +1 -0
- package/dist/mcp/local/local-backend.js +25 -3
- package/dist/mcp/resources.js +11 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +15 -5
- package/hooks/claude/gitnexus-hook.cjs +0 -0
- package/hooks/claude/pre-tool-use.sh +0 -0
- package/hooks/claude/session-start.sh +0 -0
- package/package.json +6 -5
- package/scripts/patch-tree-sitter-swift.cjs +0 -0
|
@@ -81,6 +81,72 @@ const SKIP_NODE_TYPES = new Set(['attribute_list', 'attribute']);
|
|
|
81
81
|
const PHPDOC_PARAM_RE = /@param\s+(\S+)\s+\$(\w+)/g;
|
|
82
82
|
/** Alternate PHPDoc order: `@param $name Type` (name first) */
|
|
83
83
|
const PHPDOC_PARAM_ALT_RE = /@param\s+\$(\w+)\s+(\S+)/g;
|
|
84
|
+
/** Regex to extract PHPDoc @var annotations: `@var Type` */
|
|
85
|
+
const PHPDOC_VAR_RE = /@var\s+(\S+)/;
|
|
86
|
+
/**
|
|
87
|
+
* Extract the element type for a class property from its PHPDoc @var annotation or
|
|
88
|
+
* PHP 7.4+ native type. Walks backward from the property_declaration node to find
|
|
89
|
+
* an immediately preceding comment containing @var.
|
|
90
|
+
*
|
|
91
|
+
* Returns the normalized element type (e.g. User[] → User, Collection<User> → User).
|
|
92
|
+
* Returns undefined when no usable type annotation is found.
|
|
93
|
+
*/
|
|
94
|
+
const extractClassPropertyElementType = (propDecl) => {
|
|
95
|
+
// Strategy 1: PHPDoc @var annotation on a preceding comment sibling
|
|
96
|
+
let sibling = propDecl.previousSibling;
|
|
97
|
+
while (sibling) {
|
|
98
|
+
if (sibling.type === 'comment') {
|
|
99
|
+
const match = PHPDOC_VAR_RE.exec(sibling.text);
|
|
100
|
+
if (match)
|
|
101
|
+
return normalizePhpType(match[1]);
|
|
102
|
+
}
|
|
103
|
+
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
sibling = sibling.previousSibling;
|
|
107
|
+
}
|
|
108
|
+
// Strategy 2: PHP 7.4+ native type field — skip generic 'array' since element type is unknown
|
|
109
|
+
const typeNode = propDecl.childForFieldName('type');
|
|
110
|
+
if (!typeNode)
|
|
111
|
+
return undefined;
|
|
112
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
113
|
+
if (!typeName || typeName === 'array')
|
|
114
|
+
return undefined;
|
|
115
|
+
return typeName;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Scan a class body for a property_declaration matching the given property name,
|
|
119
|
+
* and extract its element type. The class body is the `declaration_list` child of
|
|
120
|
+
* a `class_declaration` node.
|
|
121
|
+
*
|
|
122
|
+
* Used as Strategy C in extractForLoopBinding for `$this->property` iterables
|
|
123
|
+
* where Strategy A (resolveIterableElementType) and Strategy B (scopeEnv lookup)
|
|
124
|
+
* both fail to find the type.
|
|
125
|
+
*/
|
|
126
|
+
const findClassPropertyElementType = (propName, classNode) => {
|
|
127
|
+
const declList = classNode.childForFieldName('body')
|
|
128
|
+
?? (classNode.namedChild(classNode.namedChildCount - 1)?.type === 'declaration_list'
|
|
129
|
+
? classNode.namedChild(classNode.namedChildCount - 1)
|
|
130
|
+
: null); // fallback: last named child, only if it's a declaration_list
|
|
131
|
+
if (!declList)
|
|
132
|
+
return undefined;
|
|
133
|
+
for (let i = 0; i < declList.namedChildCount; i++) {
|
|
134
|
+
const child = declList.namedChild(i);
|
|
135
|
+
if (child?.type !== 'property_declaration')
|
|
136
|
+
continue;
|
|
137
|
+
// Check if any property_element has a variable_name matching '$propName'
|
|
138
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
139
|
+
const elem = child.namedChild(j);
|
|
140
|
+
if (elem?.type !== 'property_element')
|
|
141
|
+
continue;
|
|
142
|
+
const varNameNode = elem.firstNamedChild; // variable_name node
|
|
143
|
+
if (varNameNode?.text === '$' + propName) {
|
|
144
|
+
return extractClassPropertyElementType(child);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
84
150
|
/**
|
|
85
151
|
* Collect PHPDoc @param type bindings from comment nodes preceding a method/function.
|
|
86
152
|
* Returns a map of paramName → typeName (without $ prefix).
|
|
@@ -253,9 +319,36 @@ const scanConstructorBinding = (node) => {
|
|
|
253
319
|
};
|
|
254
320
|
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
|
255
321
|
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
|
322
|
+
/**
|
|
323
|
+
* Normalize a PHPDoc return type for storage in the SymbolTable.
|
|
324
|
+
* Unlike normalizePhpType (which strips User[] → User for scopeEnv), this preserves
|
|
325
|
+
* array notation so lookupRawReturnType can extract element types for for-loop resolution.
|
|
326
|
+
* \App\Models\User[] → User[]
|
|
327
|
+
* ?User → User
|
|
328
|
+
* Collection<User> → Collection<User> (preserved for extractElementTypeFromString)
|
|
329
|
+
*/
|
|
330
|
+
const normalizePhpReturnType = (raw) => {
|
|
331
|
+
// Strip nullable prefix: ?User[] → User[]
|
|
332
|
+
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
|
333
|
+
// Strip union with null/false/void: User[]|null → User[]
|
|
334
|
+
const parts = type.split('|').filter(p => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
|
335
|
+
if (parts.length !== 1)
|
|
336
|
+
return undefined;
|
|
337
|
+
type = parts[0];
|
|
338
|
+
// Strip namespace: \App\Models\User[] → User[]
|
|
339
|
+
const segments = type.split('\\');
|
|
340
|
+
type = segments[segments.length - 1];
|
|
341
|
+
// Skip uninformative types
|
|
342
|
+
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object' || type === 'array')
|
|
343
|
+
return undefined;
|
|
344
|
+
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type))
|
|
345
|
+
return type;
|
|
346
|
+
return undefined;
|
|
347
|
+
};
|
|
256
348
|
/**
|
|
257
349
|
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
|
|
258
350
|
* Walks backwards through preceding siblings looking for comment nodes.
|
|
351
|
+
* Preserves array notation (e.g., User[]) for for-loop element type extraction.
|
|
259
352
|
*/
|
|
260
353
|
const extractReturnType = (node) => {
|
|
261
354
|
let sibling = node.previousSibling;
|
|
@@ -263,7 +356,7 @@ const extractReturnType = (node) => {
|
|
|
263
356
|
if (sibling.type === 'comment') {
|
|
264
357
|
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
|
265
358
|
if (match)
|
|
266
|
-
return
|
|
359
|
+
return normalizePhpReturnType(match[1]);
|
|
267
360
|
}
|
|
268
361
|
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type))
|
|
269
362
|
break;
|
|
@@ -280,13 +373,40 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
280
373
|
const right = node.childForFieldName('right');
|
|
281
374
|
if (!left || !right)
|
|
282
375
|
return undefined;
|
|
283
|
-
if (left.type !== 'variable_name'
|
|
376
|
+
if (left.type !== 'variable_name')
|
|
284
377
|
return undefined;
|
|
285
378
|
const lhs = left.text;
|
|
286
|
-
|
|
287
|
-
if (!lhs || !rhs || scopeEnv.has(lhs))
|
|
379
|
+
if (!lhs || scopeEnv.has(lhs))
|
|
288
380
|
return undefined;
|
|
289
|
-
|
|
381
|
+
if (right.type === 'variable_name') {
|
|
382
|
+
const rhs = right.text;
|
|
383
|
+
if (rhs)
|
|
384
|
+
return { kind: 'copy', lhs, rhs };
|
|
385
|
+
}
|
|
386
|
+
// member_access_expression RHS → fieldAccess ($a->field)
|
|
387
|
+
if (right.type === 'member_access_expression') {
|
|
388
|
+
const obj = right.childForFieldName('object');
|
|
389
|
+
const name = right.childForFieldName('name');
|
|
390
|
+
if (obj?.type === 'variable_name' && name) {
|
|
391
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: name.text };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// function_call_expression RHS → callResult (bare function calls only)
|
|
395
|
+
if (right.type === 'function_call_expression') {
|
|
396
|
+
const funcNode = right.childForFieldName('function');
|
|
397
|
+
if (funcNode?.type === 'name') {
|
|
398
|
+
return { kind: 'callResult', lhs, callee: funcNode.text };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// member_call_expression RHS → methodCallResult ($a->method())
|
|
402
|
+
if (right.type === 'member_call_expression') {
|
|
403
|
+
const obj = right.childForFieldName('object');
|
|
404
|
+
const name = right.childForFieldName('name');
|
|
405
|
+
if (obj?.type === 'variable_name' && name) {
|
|
406
|
+
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: name.text };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
290
410
|
};
|
|
291
411
|
const FOR_LOOP_NODE_TYPES = new Set([
|
|
292
412
|
'foreach_statement',
|
|
@@ -339,7 +459,7 @@ const findPhpParamElementType = (iterableName, startNode) => {
|
|
|
339
459
|
* constructor-binding cases that retain container types), then fall back to direct
|
|
340
460
|
* scopeEnv lookup (for PHPDoc-normalized types).
|
|
341
461
|
*/
|
|
342
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
462
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
343
463
|
if (node.type !== 'foreach_statement')
|
|
344
464
|
return;
|
|
345
465
|
// Collect non-body named children: first is the iterable, second is value or pair
|
|
@@ -373,6 +493,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
373
493
|
return;
|
|
374
494
|
// Get iterable variable name (PHP vars include $ prefix)
|
|
375
495
|
let iterableName;
|
|
496
|
+
let callExprElementType;
|
|
376
497
|
if (iterableNode.type === 'variable_name') {
|
|
377
498
|
iterableName = iterableNode.text;
|
|
378
499
|
}
|
|
@@ -383,8 +504,31 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
383
504
|
if (name)
|
|
384
505
|
iterableName = '$' + name.text;
|
|
385
506
|
}
|
|
386
|
-
if (
|
|
507
|
+
else if (iterableNode?.type === 'function_call_expression') {
|
|
508
|
+
// foreach (getUsers() as $user) — resolve via return type lookup
|
|
509
|
+
const calleeName = extractCalleeName(iterableNode);
|
|
510
|
+
if (calleeName) {
|
|
511
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(calleeName);
|
|
512
|
+
if (rawReturn)
|
|
513
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else if (iterableNode?.type === 'member_call_expression') {
|
|
517
|
+
// foreach ($this->getUsers() as $user) — resolve via return type lookup
|
|
518
|
+
const methodName = iterableNode.childForFieldName('name');
|
|
519
|
+
if (methodName) {
|
|
520
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(methodName.text);
|
|
521
|
+
if (rawReturn)
|
|
522
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!iterableName && !callExprElementType)
|
|
526
|
+
return;
|
|
527
|
+
// If we resolved the element type from a call expression, bind and return early
|
|
528
|
+
if (callExprElementType) {
|
|
529
|
+
scopeEnv.set(varName, callExprElementType);
|
|
387
530
|
return;
|
|
531
|
+
}
|
|
388
532
|
// Strategy A: try resolveIterableElementType (handles constructor-binding container types)
|
|
389
533
|
const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPhpElementTypeFromTypeNode, findPhpParamElementType, undefined);
|
|
390
534
|
if (elementType) {
|
|
@@ -396,6 +540,27 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
396
540
|
const iterableType = scopeEnv.get(iterableName);
|
|
397
541
|
if (iterableType) {
|
|
398
542
|
scopeEnv.set(varName, iterableType);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Strategy C: $this->property — scan the enclosing class body for the property
|
|
546
|
+
// declaration and extract its element type from @var PHPDoc or native type.
|
|
547
|
+
// This handles the common PHP pattern where the property type is declared on the
|
|
548
|
+
// class body (/** @var User[] */ private $users) but the foreach is in a method
|
|
549
|
+
// whose scopeEnv does not contain the property type.
|
|
550
|
+
if (iterableNode?.type === 'member_access_expression') {
|
|
551
|
+
const obj = iterableNode.childForFieldName('object');
|
|
552
|
+
if (obj?.text === '$this') {
|
|
553
|
+
const nameNode = iterableNode.childForFieldName('name');
|
|
554
|
+
const propName = nameNode?.text;
|
|
555
|
+
if (propName) {
|
|
556
|
+
const classNode = findEnclosingClass(iterableNode);
|
|
557
|
+
if (classNode) {
|
|
558
|
+
const elementType = findClassPropertyElementType(propName, classNode);
|
|
559
|
+
if (elementType)
|
|
560
|
+
scopeEnv.set(varName, elementType);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
399
564
|
}
|
|
400
565
|
};
|
|
401
566
|
export const typeConfig = {
|
|
@@ -61,6 +61,10 @@ const extractParameter = (node, env) => {
|
|
|
61
61
|
else {
|
|
62
62
|
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
|
63
63
|
typeNode = node.childForFieldName('type');
|
|
64
|
+
// Python typed_parameter: name is a positional child (identifier), not a named field
|
|
65
|
+
if (!nameNode && node.type === 'typed_parameter') {
|
|
66
|
+
nameNode = node.firstNamedChild?.type === 'identifier' ? node.firstNamedChild : null;
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
if (!nameNode || !typeNode)
|
|
66
70
|
return;
|
|
@@ -229,6 +233,38 @@ const findPyParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
229
233
|
}
|
|
230
234
|
return undefined;
|
|
231
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* Extracts iterableName and methodName from a call expression like `data.items()`.
|
|
238
|
+
* Returns undefined if the call doesn't match the expected pattern.
|
|
239
|
+
*/
|
|
240
|
+
const extractMethodCall = (callNode) => {
|
|
241
|
+
const fn = callNode.childForFieldName('function');
|
|
242
|
+
if (fn?.type !== 'attribute')
|
|
243
|
+
return undefined;
|
|
244
|
+
const obj = fn.firstNamedChild;
|
|
245
|
+
if (obj?.type !== 'identifier')
|
|
246
|
+
return undefined;
|
|
247
|
+
const method = fn.lastNamedChild;
|
|
248
|
+
const methodName = (method?.type === 'identifier' && method !== obj) ? method.text : undefined;
|
|
249
|
+
return { iterableName: obj.text, methodName };
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Collects all identifier nodes from a pattern, descending into nested tuple_patterns.
|
|
253
|
+
* For `i, (k, v)` returns [i, k, v]. For `key, value` returns [key, value].
|
|
254
|
+
*/
|
|
255
|
+
const collectPatternIdentifiers = (pattern) => {
|
|
256
|
+
const vars = [];
|
|
257
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
258
|
+
const child = pattern.namedChild(i);
|
|
259
|
+
if (child?.type === 'identifier') {
|
|
260
|
+
vars.push(child);
|
|
261
|
+
}
|
|
262
|
+
else if (child?.type === 'tuple_pattern') {
|
|
263
|
+
vars.push(...collectPatternIdentifiers(child));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return vars;
|
|
267
|
+
};
|
|
232
268
|
/**
|
|
233
269
|
* Python: for user in users: where users has a known container type annotation.
|
|
234
270
|
*
|
|
@@ -238,14 +274,19 @@ const findPyParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
238
274
|
* 1. declarationTypeNodes — raw type annotation AST node (covers stored container types)
|
|
239
275
|
* 2. scopeEnv string — extractElementTypeFromString on the stored type
|
|
240
276
|
* 3. AST walk — walks up to the enclosing function's parameters to read List[User] directly
|
|
277
|
+
*
|
|
278
|
+
* Also handles `enumerate(iterable)` — unwraps the outer call and skips the integer
|
|
279
|
+
* index variable so the value variable still resolves to the element type.
|
|
241
280
|
*/
|
|
242
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
281
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
243
282
|
if (node.type !== 'for_statement')
|
|
244
283
|
return;
|
|
245
|
-
// The iterable is the `right` field — may be identifier or call (data.items()/keys()/values()).
|
|
246
284
|
const rightNode = node.childForFieldName('right');
|
|
247
285
|
let iterableName;
|
|
248
286
|
let methodName;
|
|
287
|
+
let callExprElementType;
|
|
288
|
+
let isEnumerate = false;
|
|
289
|
+
// Extract iterable info from the `right` field — may be identifier, attribute, or call.
|
|
249
290
|
if (rightNode?.type === 'identifier') {
|
|
250
291
|
iterableName = rightNode.text;
|
|
251
292
|
}
|
|
@@ -255,34 +296,56 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
255
296
|
iterableName = prop.text;
|
|
256
297
|
}
|
|
257
298
|
else if (rightNode?.type === 'call') {
|
|
258
|
-
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
|
259
299
|
const fn = rightNode.childForFieldName('function');
|
|
260
|
-
if (fn?.type === '
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
300
|
+
if (fn?.type === 'identifier' && fn.text === 'enumerate') {
|
|
301
|
+
// enumerate(iterable) or enumerate(d.items()) — unwrap to inner iterable.
|
|
302
|
+
isEnumerate = true;
|
|
303
|
+
const innerArg = rightNode.childForFieldName('arguments')?.firstNamedChild;
|
|
304
|
+
if (innerArg?.type === 'identifier') {
|
|
305
|
+
iterableName = innerArg.text;
|
|
306
|
+
}
|
|
307
|
+
else if (innerArg?.type === 'call') {
|
|
308
|
+
const extracted = extractMethodCall(innerArg);
|
|
309
|
+
if (extracted)
|
|
310
|
+
({ iterableName, methodName } = extracted);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (fn?.type === 'attribute') {
|
|
314
|
+
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
|
315
|
+
const extracted = extractMethodCall(rightNode);
|
|
316
|
+
if (extracted)
|
|
317
|
+
({ iterableName, methodName } = extracted);
|
|
318
|
+
}
|
|
319
|
+
else if (fn?.type === 'identifier') {
|
|
320
|
+
// Direct function call: for user in get_users() (Phase 7.3 — return-type path)
|
|
321
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
|
|
322
|
+
if (rawReturn)
|
|
323
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
268
324
|
}
|
|
269
325
|
}
|
|
270
|
-
if (!iterableName)
|
|
326
|
+
if (!iterableName && !callExprElementType)
|
|
271
327
|
return;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
328
|
+
let elementType;
|
|
329
|
+
if (callExprElementType) {
|
|
330
|
+
elementType = callExprElementType;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
334
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
335
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPyElementTypeFromAnnotation, findPyParamElementType, typeArgPos);
|
|
336
|
+
}
|
|
275
337
|
if (!elementType)
|
|
276
338
|
return;
|
|
277
339
|
// The loop variable is the `left` field — identifier or pattern_list.
|
|
278
340
|
const leftNode = node.childForFieldName('left');
|
|
279
341
|
if (!leftNode)
|
|
280
342
|
return;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
343
|
+
if (leftNode.type === 'pattern_list' || leftNode.type === 'tuple_pattern') {
|
|
344
|
+
// Tuple unpacking: `key, value` or `i, (k, v)` or `(k, v)` — bind the last identifier to element type.
|
|
345
|
+
// With enumerate, skip binding if there's only one var (just the index, no value to bind).
|
|
346
|
+
const vars = collectPatternIdentifiers(leftNode);
|
|
347
|
+
if (vars.length > 0 && (!isEnumerate || vars.length > 1)) {
|
|
348
|
+
scopeEnv.set(vars[vars.length - 1].text, elementType);
|
|
286
349
|
}
|
|
287
350
|
return;
|
|
288
351
|
}
|
|
@@ -312,7 +375,30 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
312
375
|
if (!lhs || scopeEnv.has(lhs))
|
|
313
376
|
return undefined;
|
|
314
377
|
if (right.type === 'identifier')
|
|
315
|
-
return { lhs, rhs: right.text };
|
|
378
|
+
return { kind: 'copy', lhs, rhs: right.text };
|
|
379
|
+
// attribute RHS → fieldAccess (a.field)
|
|
380
|
+
if (right.type === 'attribute') {
|
|
381
|
+
const obj = right.firstNamedChild;
|
|
382
|
+
const field = right.lastNamedChild;
|
|
383
|
+
if (obj?.type === 'identifier' && field?.type === 'identifier' && obj !== field) {
|
|
384
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// call RHS
|
|
388
|
+
if (right.type === 'call') {
|
|
389
|
+
const funcNode = right.childForFieldName('function');
|
|
390
|
+
if (funcNode?.type === 'identifier') {
|
|
391
|
+
return { kind: 'callResult', lhs, callee: funcNode.text };
|
|
392
|
+
}
|
|
393
|
+
// method call with receiver: call → function: attribute
|
|
394
|
+
if (funcNode?.type === 'attribute') {
|
|
395
|
+
const obj = funcNode.firstNamedChild;
|
|
396
|
+
const method = funcNode.lastNamedChild;
|
|
397
|
+
if (obj?.type === 'identifier' && method?.type === 'identifier' && obj !== method) {
|
|
398
|
+
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: method.text };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
316
402
|
return undefined;
|
|
317
403
|
};
|
|
318
404
|
/**
|
|
@@ -324,7 +324,7 @@ const findRubyParamElementType = (iterableName, startNode) => {
|
|
|
324
324
|
* Ruby has no static types on loop variables, so this mainly works when the
|
|
325
325
|
* iterable has a YARD-annotated container type (e.g., `@param users [Array<User>]`).
|
|
326
326
|
*/
|
|
327
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
327
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope }) => {
|
|
328
328
|
if (node.type !== 'for')
|
|
329
329
|
return;
|
|
330
330
|
// The loop variable is the `pattern` field (identifier).
|
|
@@ -372,9 +372,24 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
372
372
|
if (scopeEnv.has(varName))
|
|
373
373
|
return undefined;
|
|
374
374
|
const rhsNode = node.childForFieldName('right');
|
|
375
|
-
if (!rhsNode
|
|
375
|
+
if (!rhsNode)
|
|
376
376
|
return undefined;
|
|
377
|
-
|
|
377
|
+
if (rhsNode.type === 'identifier')
|
|
378
|
+
return { kind: 'copy', lhs: varName, rhs: rhsNode.text };
|
|
379
|
+
// call/method_call RHS — Ruby uses method calls for both field access and method calls
|
|
380
|
+
if (rhsNode.type === 'call' || rhsNode.type === 'method_call') {
|
|
381
|
+
const methodNode = rhsNode.childForFieldName('method');
|
|
382
|
+
const receiverNode = rhsNode.childForFieldName('receiver');
|
|
383
|
+
if (!receiverNode && methodNode?.type === 'identifier') {
|
|
384
|
+
// No receiver → callResult (bare function call)
|
|
385
|
+
return { kind: 'callResult', lhs: varName, callee: methodNode.text };
|
|
386
|
+
}
|
|
387
|
+
if (receiverNode?.type === 'identifier' && methodNode?.type === 'identifier') {
|
|
388
|
+
// With receiver → methodCallResult (a.method)
|
|
389
|
+
return { kind: 'methodCallResult', lhs: varName, receiver: receiverNode.text, method: methodNode.text };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
378
393
|
};
|
|
379
394
|
export const typeConfig = {
|
|
380
395
|
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition } from './shared.js';
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, hasTypeAnnotation, unwrapAwait, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString } from './shared.js';
|
|
2
2
|
const DECLARATION_NODE_TYPES = new Set([
|
|
3
3
|
'let_declaration',
|
|
4
4
|
'let_condition',
|
|
@@ -95,7 +95,7 @@ const extractDeclaration = (node, env) => {
|
|
|
95
95
|
env.set(varName, typeName);
|
|
96
96
|
};
|
|
97
97
|
/** Rust: let x = User::new(), let x = User::default(), or let x = User { ... } */
|
|
98
|
-
const extractInitializer = (node, env,
|
|
98
|
+
const extractInitializer = (node, env, classNames) => {
|
|
99
99
|
// Skip if there's an explicit type annotation — Tier 0 already handled it
|
|
100
100
|
if (node.childForFieldName('type') !== null)
|
|
101
101
|
return;
|
|
@@ -119,6 +119,13 @@ const extractInitializer = (node, env, _classNames) => {
|
|
|
119
119
|
env.set(varName, typeName);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
+
// Unit struct instantiation: let svc = UserService; (bare identifier, no braces or call)
|
|
123
|
+
if (value.type === 'identifier' && classNames.has(value.text)) {
|
|
124
|
+
const varName = extractVarName(pattern);
|
|
125
|
+
if (varName)
|
|
126
|
+
env.set(varName, value.text);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
122
129
|
if (value.type !== 'call_expression')
|
|
123
130
|
return;
|
|
124
131
|
const func = value.childForFieldName('function');
|
|
@@ -209,8 +216,35 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
209
216
|
const lhs = extractVarName(pattern);
|
|
210
217
|
if (!lhs || scopeEnv.has(lhs))
|
|
211
218
|
return undefined;
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
// Unwrap Rust .await: `let user = get_user().await` → call_expression
|
|
220
|
+
const unwrapped = unwrapAwait(value) ?? value;
|
|
221
|
+
if (unwrapped.type === 'identifier')
|
|
222
|
+
return { kind: 'copy', lhs, rhs: unwrapped.text };
|
|
223
|
+
// field_expression RHS → fieldAccess (a.field)
|
|
224
|
+
if (unwrapped.type === 'field_expression') {
|
|
225
|
+
const obj = unwrapped.firstNamedChild;
|
|
226
|
+
const field = unwrapped.lastNamedChild;
|
|
227
|
+
if (obj?.type === 'identifier' && field?.type === 'field_identifier') {
|
|
228
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// call_expression RHS → callResult (simple calls only)
|
|
232
|
+
if (unwrapped.type === 'call_expression') {
|
|
233
|
+
const funcNode = unwrapped.childForFieldName('function');
|
|
234
|
+
if (funcNode?.type === 'identifier') {
|
|
235
|
+
return { kind: 'callResult', lhs, callee: funcNode.text };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// method_call_expression RHS → methodCallResult (receiver.method())
|
|
239
|
+
if (unwrapped.type === 'method_call_expression') {
|
|
240
|
+
const obj = unwrapped.firstNamedChild;
|
|
241
|
+
if (obj?.type === 'identifier') {
|
|
242
|
+
const methodNode = unwrapped.childForFieldName('name') ?? unwrapped.namedChild(1);
|
|
243
|
+
if (methodNode?.type === 'field_identifier') {
|
|
244
|
+
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: methodNode.text };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
214
248
|
return undefined;
|
|
215
249
|
};
|
|
216
250
|
/**
|
|
@@ -375,7 +409,7 @@ const findRustParamElementType = (iterableName, startNode, pos = 'last') => {
|
|
|
375
409
|
};
|
|
376
410
|
/** Rust: for user in &users where users has a known container type.
|
|
377
411
|
* Unwraps reference_expression (&users, &mut users) to get the iterable name. */
|
|
378
|
-
const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
412
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
379
413
|
if (node.type !== 'for_expression')
|
|
380
414
|
return;
|
|
381
415
|
const patternNode = node.childForFieldName('pattern');
|
|
@@ -385,6 +419,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
385
419
|
// Extract iterable name + method — may be &users, users, or users.iter()/keys()/values()
|
|
386
420
|
let iterableName;
|
|
387
421
|
let methodName;
|
|
422
|
+
let callExprElementType;
|
|
388
423
|
if (valueNode.type === 'reference_expression') {
|
|
389
424
|
const inner = valueNode.lastNamedChild;
|
|
390
425
|
if (inner?.type === 'identifier')
|
|
@@ -399,23 +434,35 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
|
|
|
399
434
|
iterableName = prop.text;
|
|
400
435
|
}
|
|
401
436
|
else if (valueNode.type === 'call_expression') {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const obj =
|
|
437
|
+
const funcExpr = valueNode.childForFieldName('function');
|
|
438
|
+
if (funcExpr?.type === 'field_expression') {
|
|
439
|
+
// users.iter() → field_expression > identifier + field_identifier
|
|
440
|
+
const obj = funcExpr.firstNamedChild;
|
|
406
441
|
if (obj?.type === 'identifier')
|
|
407
442
|
iterableName = obj.text;
|
|
408
443
|
// Extract method name: iter, keys, values, into_iter, etc.
|
|
409
|
-
const field =
|
|
444
|
+
const field = funcExpr.lastNamedChild;
|
|
410
445
|
if (field?.type === 'field_identifier')
|
|
411
446
|
methodName = field.text;
|
|
412
447
|
}
|
|
448
|
+
else if (funcExpr?.type === 'identifier') {
|
|
449
|
+
// Direct function call: for user in get_users()
|
|
450
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(funcExpr.text);
|
|
451
|
+
if (rawReturn)
|
|
452
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
453
|
+
}
|
|
413
454
|
}
|
|
414
|
-
if (!iterableName)
|
|
455
|
+
if (!iterableName && !callExprElementType)
|
|
415
456
|
return;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
let elementType;
|
|
458
|
+
if (callExprElementType) {
|
|
459
|
+
elementType = callExprElementType;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
const containerTypeName = scopeEnv.get(iterableName);
|
|
463
|
+
const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
|
|
464
|
+
elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractRustElementTypeFromTypeNode, findRustParamElementType, typeArgPos);
|
|
465
|
+
}
|
|
419
466
|
if (!elementType)
|
|
420
467
|
return;
|
|
421
468
|
const loopVarName = extractVarName(patternNode);
|
|
@@ -129,4 +129,17 @@ export declare const findChildByType: (node: SyntaxNode, type: string) => Syntax
|
|
|
129
129
|
* Returns undefined when the extracted type is not a simple word.
|
|
130
130
|
*/
|
|
131
131
|
export declare function extractElementTypeFromString(typeStr: string, pos?: TypeArgPosition): string | undefined;
|
|
132
|
+
export declare const extractReturnTypeName: (raw: string, depth?: number) => string | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Extract the declared type of a property/field from its AST definition node.
|
|
135
|
+
* Handles cross-language patterns:
|
|
136
|
+
* - TypeScript: `name: Type` → type_annotation child
|
|
137
|
+
* - Java: `Type name` → type child on field_declaration
|
|
138
|
+
* - C#: `Type Name { get; set; }` → type child on property_declaration
|
|
139
|
+
* - Go: `Name Type` → type child on field_declaration
|
|
140
|
+
* - Kotlin: `var name: Type` → variable_declaration child with type field
|
|
141
|
+
*
|
|
142
|
+
* Returns the normalized type name, or undefined if no type can be extracted.
|
|
143
|
+
*/
|
|
144
|
+
export declare const extractPropertyDeclaredType: (definitionNode: SyntaxNode | null) => string | undefined;
|
|
132
145
|
export {};
|