gitnexus 1.4.6 → 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/core/graph/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -1
- package/dist/core/ingestion/call-processor.js +308 -104
- 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 +32 -6
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/symbol-table.d.ts +13 -3
- package/dist/core/ingestion/symbol-table.js +23 -4
- 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 +94 -38
- package/dist/core/ingestion/type-extractors/c-cpp.js +27 -2
- package/dist/core/ingestion/type-extractors/csharp.js +40 -0
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/jvm.js +75 -3
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +37 -3
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/shared.js +110 -3
- package/dist/core/ingestion/type-extractors/types.d.ts +17 -4
- package/dist/core/ingestion/type-extractors/typescript.js +30 -0
- 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 +68 -26
- 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.js +3 -3
- package/dist/mcp/resources.js +11 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +15 -5
- package/package.json +4 -4
|
@@ -219,6 +219,32 @@ const extractJavaPendingAssignment = (node, scopeEnv) => {
|
|
|
219
219
|
continue;
|
|
220
220
|
if (valueNode.type === 'identifier' || valueNode.type === 'simple_identifier')
|
|
221
221
|
return { kind: 'copy', lhs, rhs: valueNode.text };
|
|
222
|
+
// field_access RHS → fieldAccess (a.field)
|
|
223
|
+
if (valueNode.type === 'field_access') {
|
|
224
|
+
const obj = valueNode.childForFieldName('object');
|
|
225
|
+
const field = valueNode.childForFieldName('field');
|
|
226
|
+
if (obj?.type === 'identifier' && field) {
|
|
227
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: field.text };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// method_invocation RHS
|
|
231
|
+
if (valueNode.type === 'method_invocation') {
|
|
232
|
+
const objField = valueNode.childForFieldName('object');
|
|
233
|
+
if (!objField) {
|
|
234
|
+
// No receiver → callResult
|
|
235
|
+
const nameField = valueNode.childForFieldName('name');
|
|
236
|
+
if (nameField?.type === 'identifier') {
|
|
237
|
+
return { kind: 'callResult', lhs, callee: nameField.text };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (objField.type === 'identifier') {
|
|
241
|
+
// With receiver → methodCallResult
|
|
242
|
+
const nameField = valueNode.childForFieldName('name');
|
|
243
|
+
if (nameField?.type === 'identifier') {
|
|
244
|
+
return { kind: 'methodCallResult', lhs, receiver: objField.text, method: nameField.text };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
222
248
|
}
|
|
223
249
|
return undefined;
|
|
224
250
|
};
|
|
@@ -589,7 +615,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
589
615
|
const lhs = nameNode.text;
|
|
590
616
|
if (scopeEnv.has(lhs))
|
|
591
617
|
return undefined;
|
|
592
|
-
// Find the RHS
|
|
618
|
+
// Find the RHS after the "=" token
|
|
593
619
|
let foundEq = false;
|
|
594
620
|
for (let i = 0; i < node.childCount; i++) {
|
|
595
621
|
const child = node.child(i);
|
|
@@ -602,6 +628,31 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
602
628
|
if (foundEq && child.type === 'simple_identifier') {
|
|
603
629
|
return { kind: 'copy', lhs, rhs: child.text };
|
|
604
630
|
}
|
|
631
|
+
// navigation_expression RHS → fieldAccess (a.field)
|
|
632
|
+
if (foundEq && child.type === 'navigation_expression') {
|
|
633
|
+
const recv = child.firstNamedChild;
|
|
634
|
+
const suffix = child.lastNamedChild;
|
|
635
|
+
const fieldNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
|
|
636
|
+
if (recv?.type === 'simple_identifier' && fieldNode?.type === 'simple_identifier') {
|
|
637
|
+
return { kind: 'fieldAccess', lhs, receiver: recv.text, field: fieldNode.text };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// call_expression RHS
|
|
641
|
+
if (foundEq && child.type === 'call_expression') {
|
|
642
|
+
const calleeNode = child.firstNamedChild;
|
|
643
|
+
if (calleeNode?.type === 'simple_identifier') {
|
|
644
|
+
return { kind: 'callResult', lhs, callee: calleeNode.text };
|
|
645
|
+
}
|
|
646
|
+
// navigation_expression callee → methodCallResult (a.method())
|
|
647
|
+
if (calleeNode?.type === 'navigation_expression') {
|
|
648
|
+
const recv = calleeNode.firstNamedChild;
|
|
649
|
+
const suffix = calleeNode.lastNamedChild;
|
|
650
|
+
const methodNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
|
|
651
|
+
if (recv?.type === 'simple_identifier' && methodNode?.type === 'simple_identifier') {
|
|
652
|
+
return { kind: 'methodCallResult', lhs, receiver: recv.text, method: methodNode.text };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
605
656
|
}
|
|
606
657
|
return undefined;
|
|
607
658
|
}
|
|
@@ -613,8 +664,7 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
613
664
|
const lhs = nameNode.text;
|
|
614
665
|
if (scopeEnv.has(lhs))
|
|
615
666
|
return undefined;
|
|
616
|
-
// Look for RHS
|
|
617
|
-
// variable_declaration itself doesn't contain "=" — it's in the parent
|
|
667
|
+
// Look for RHS after "=" in the parent (property_declaration)
|
|
618
668
|
const parent = node.parent;
|
|
619
669
|
if (!parent)
|
|
620
670
|
return undefined;
|
|
@@ -630,6 +680,28 @@ const extractKotlinPendingAssignment = (node, scopeEnv) => {
|
|
|
630
680
|
if (foundEq && child.type === 'simple_identifier') {
|
|
631
681
|
return { kind: 'copy', lhs, rhs: child.text };
|
|
632
682
|
}
|
|
683
|
+
if (foundEq && child.type === 'navigation_expression') {
|
|
684
|
+
const recv = child.firstNamedChild;
|
|
685
|
+
const suffix = child.lastNamedChild;
|
|
686
|
+
const fieldNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
|
|
687
|
+
if (recv?.type === 'simple_identifier' && fieldNode?.type === 'simple_identifier') {
|
|
688
|
+
return { kind: 'fieldAccess', lhs, receiver: recv.text, field: fieldNode.text };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (foundEq && child.type === 'call_expression') {
|
|
692
|
+
const calleeNode = child.firstNamedChild;
|
|
693
|
+
if (calleeNode?.type === 'simple_identifier') {
|
|
694
|
+
return { kind: 'callResult', lhs, callee: calleeNode.text };
|
|
695
|
+
}
|
|
696
|
+
if (calleeNode?.type === 'navigation_expression') {
|
|
697
|
+
const recv = calleeNode.firstNamedChild;
|
|
698
|
+
const suffix = calleeNode.lastNamedChild;
|
|
699
|
+
const methodNode = suffix?.type === 'navigation_suffix' ? suffix.lastNamedChild : suffix;
|
|
700
|
+
if (recv?.type === 'simple_identifier' && methodNode?.type === 'simple_identifier') {
|
|
701
|
+
return { kind: 'methodCallResult', lhs, receiver: recv.text, method: methodNode.text };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
633
705
|
}
|
|
634
706
|
return undefined;
|
|
635
707
|
}
|
|
@@ -373,13 +373,40 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
373
373
|
const right = node.childForFieldName('right');
|
|
374
374
|
if (!left || !right)
|
|
375
375
|
return undefined;
|
|
376
|
-
if (left.type !== 'variable_name'
|
|
376
|
+
if (left.type !== 'variable_name')
|
|
377
377
|
return undefined;
|
|
378
378
|
const lhs = left.text;
|
|
379
|
-
|
|
380
|
-
if (!lhs || !rhs || scopeEnv.has(lhs))
|
|
379
|
+
if (!lhs || scopeEnv.has(lhs))
|
|
381
380
|
return undefined;
|
|
382
|
-
|
|
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;
|
|
383
410
|
};
|
|
384
411
|
const FOR_LOOP_NODE_TYPES = new Set([
|
|
385
412
|
'foreach_statement',
|
|
@@ -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,15 +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
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, attribute, or call.
|
|
246
284
|
const rightNode = node.childForFieldName('right');
|
|
247
285
|
let iterableName;
|
|
248
286
|
let methodName;
|
|
249
287
|
let callExprElementType;
|
|
288
|
+
let isEnumerate = false;
|
|
289
|
+
// Extract iterable info from the `right` field — may be identifier, attribute, or call.
|
|
250
290
|
if (rightNode?.type === 'identifier') {
|
|
251
291
|
iterableName = rightNode.text;
|
|
252
292
|
}
|
|
@@ -256,20 +296,28 @@ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, re
|
|
|
256
296
|
iterableName = prop.text;
|
|
257
297
|
}
|
|
258
298
|
else if (rightNode?.type === 'call') {
|
|
259
|
-
// data.items() → call > function: attribute > identifier('data') + identifier('items')
|
|
260
|
-
// get_users() → call > function: identifier (Phase 7.3 — return-type path)
|
|
261
299
|
const fn = rightNode.childForFieldName('function');
|
|
262
|
-
if (fn?.type === '
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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);
|
|
270
318
|
}
|
|
271
319
|
else if (fn?.type === 'identifier') {
|
|
272
|
-
// Direct function call: for user in get_users()
|
|
320
|
+
// Direct function call: for user in get_users() (Phase 7.3 — return-type path)
|
|
273
321
|
const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
|
|
274
322
|
if (rawReturn)
|
|
275
323
|
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
@@ -292,11 +340,12 @@ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, re
|
|
|
292
340
|
const leftNode = node.childForFieldName('left');
|
|
293
341
|
if (!leftNode)
|
|
294
342
|
return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|
|
300
349
|
}
|
|
301
350
|
return;
|
|
302
351
|
}
|
|
@@ -327,6 +376,29 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
327
376
|
return undefined;
|
|
328
377
|
if (right.type === 'identifier')
|
|
329
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
|
+
}
|
|
330
402
|
return undefined;
|
|
331
403
|
};
|
|
332
404
|
/**
|
|
@@ -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,
|
|
@@ -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
|
/**
|
|
@@ -130,4 +130,16 @@ export declare const findChildByType: (node: SyntaxNode, type: string) => Syntax
|
|
|
130
130
|
*/
|
|
131
131
|
export declare function extractElementTypeFromString(typeStr: string, pos?: TypeArgPosition): string | undefined;
|
|
132
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;
|
|
133
145
|
export {};
|
|
@@ -226,9 +226,14 @@ export const extractSimpleTypeName = (typeNode, depth = 0) => {
|
|
|
226
226
|
}
|
|
227
227
|
// Pointer/reference types (C++, Rust): User*, &User, &mut User
|
|
228
228
|
if (typeNode.type === 'pointer_type' || typeNode.type === 'reference_type') {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
// Skip mutable_specifier for Rust &mut references — firstNamedChild would be
|
|
230
|
+
// `mutable_specifier` not the actual type. Walk named children to find the type.
|
|
231
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
232
|
+
const child = typeNode.namedChild(i);
|
|
233
|
+
if (child && child.type !== 'mutable_specifier') {
|
|
234
|
+
return extractSimpleTypeName(child, depth + 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
232
237
|
}
|
|
233
238
|
// Primitive/predefined types: string, int, float, bool, number, unknown, any
|
|
234
239
|
// PHP: primitive_type; TS/JS: predefined_type
|
|
@@ -701,3 +706,105 @@ export const extractReturnTypeName = (raw, depth = 0) => {
|
|
|
701
706
|
return undefined;
|
|
702
707
|
return text;
|
|
703
708
|
};
|
|
709
|
+
// ── Property declared-type extraction ────────────────────────────────────
|
|
710
|
+
// Shared between parse-worker (worker path) and parsing-processor (sequential path).
|
|
711
|
+
/**
|
|
712
|
+
* Extract the declared type of a property/field from its AST definition node.
|
|
713
|
+
* Handles cross-language patterns:
|
|
714
|
+
* - TypeScript: `name: Type` → type_annotation child
|
|
715
|
+
* - Java: `Type name` → type child on field_declaration
|
|
716
|
+
* - C#: `Type Name { get; set; }` → type child on property_declaration
|
|
717
|
+
* - Go: `Name Type` → type child on field_declaration
|
|
718
|
+
* - Kotlin: `var name: Type` → variable_declaration child with type field
|
|
719
|
+
*
|
|
720
|
+
* Returns the normalized type name, or undefined if no type can be extracted.
|
|
721
|
+
*/
|
|
722
|
+
export const extractPropertyDeclaredType = (definitionNode) => {
|
|
723
|
+
if (!definitionNode)
|
|
724
|
+
return undefined;
|
|
725
|
+
// Strategy 1: Look for a `type` or `type_annotation` named field
|
|
726
|
+
const typeNode = definitionNode.childForFieldName?.('type');
|
|
727
|
+
if (typeNode) {
|
|
728
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
729
|
+
if (typeName)
|
|
730
|
+
return typeName;
|
|
731
|
+
// Fallback: use the raw text (for complex types like User[] or List<User>)
|
|
732
|
+
const text = typeNode.text?.trim();
|
|
733
|
+
if (text && text.length < 100)
|
|
734
|
+
return text;
|
|
735
|
+
}
|
|
736
|
+
// Strategy 2: Walk children looking for type_annotation (TypeScript pattern)
|
|
737
|
+
for (let i = 0; i < definitionNode.childCount; i++) {
|
|
738
|
+
const child = definitionNode.child(i);
|
|
739
|
+
if (!child)
|
|
740
|
+
continue;
|
|
741
|
+
if (child.type === 'type_annotation') {
|
|
742
|
+
// Type annotation has the actual type as a child
|
|
743
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
744
|
+
const typeChild = child.child(j);
|
|
745
|
+
if (typeChild && typeChild.type !== ':') {
|
|
746
|
+
const typeName = extractSimpleTypeName(typeChild);
|
|
747
|
+
if (typeName)
|
|
748
|
+
return typeName;
|
|
749
|
+
const text = typeChild.text?.trim();
|
|
750
|
+
if (text && text.length < 100)
|
|
751
|
+
return text;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Strategy 3: For Java field_declaration, the type is a sibling of variable_declarator
|
|
757
|
+
// AST: (field_declaration type: (type_identifier) declarator: (variable_declarator ...))
|
|
758
|
+
const parentDecl = definitionNode.parent;
|
|
759
|
+
if (parentDecl) {
|
|
760
|
+
const parentType = parentDecl.childForFieldName?.('type');
|
|
761
|
+
if (parentType) {
|
|
762
|
+
const typeName = extractSimpleTypeName(parentType);
|
|
763
|
+
if (typeName)
|
|
764
|
+
return typeName;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Strategy 4: Kotlin property_declaration — type is nested inside variable_declaration child
|
|
768
|
+
// AST: (property_declaration (variable_declaration (simple_identifier) ":" (user_type (type_identifier))))
|
|
769
|
+
// Kotlin's variable_declaration has NO named 'type' field — children are all positional.
|
|
770
|
+
for (let i = 0; i < definitionNode.childCount; i++) {
|
|
771
|
+
const child = definitionNode.child(i);
|
|
772
|
+
if (child?.type === 'variable_declaration') {
|
|
773
|
+
// Try named field first (works for other languages sharing this strategy)
|
|
774
|
+
const varType = child.childForFieldName?.('type');
|
|
775
|
+
if (varType) {
|
|
776
|
+
const typeName = extractSimpleTypeName(varType);
|
|
777
|
+
if (typeName)
|
|
778
|
+
return typeName;
|
|
779
|
+
const text = varType.text?.trim();
|
|
780
|
+
if (text && text.length < 100)
|
|
781
|
+
return text;
|
|
782
|
+
}
|
|
783
|
+
// Fallback: walk unnamed children for user_type / type_identifier (Kotlin)
|
|
784
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
785
|
+
const varChild = child.namedChild(j);
|
|
786
|
+
if (varChild && (varChild.type === 'user_type' || varChild.type === 'type_identifier'
|
|
787
|
+
|| varChild.type === 'nullable_type' || varChild.type === 'generic_type')) {
|
|
788
|
+
const typeName = extractSimpleTypeName(varChild);
|
|
789
|
+
if (typeName)
|
|
790
|
+
return typeName;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Strategy 5: PHP @var PHPDoc — look for preceding comment with @var Type
|
|
796
|
+
// Handles pre-PHP-7.4 code: /** @var Address */ public $address;
|
|
797
|
+
const prevSibling = definitionNode.previousNamedSibling ?? definitionNode.parent?.previousNamedSibling;
|
|
798
|
+
if (prevSibling?.type === 'comment') {
|
|
799
|
+
const commentText = prevSibling.text;
|
|
800
|
+
const varMatch = commentText?.match(/@var\s+([A-Z][\w\\]*)/);
|
|
801
|
+
if (varMatch) {
|
|
802
|
+
// Strip namespace prefix: \App\Models\User → User
|
|
803
|
+
const raw = varMatch[1];
|
|
804
|
+
const base = raw.includes('\\') ? raw.split('\\').pop() : raw;
|
|
805
|
+
if (base && /^[A-Z]\w*$/.test(base))
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
};
|
|
@@ -49,8 +49,10 @@ export interface ForLoopExtractorContext {
|
|
|
49
49
|
/** Extracts loop variable type binding from a for-each statement. */
|
|
50
50
|
export type ForLoopExtractor = (node: SyntaxNode, ctx: ForLoopExtractorContext) => void;
|
|
51
51
|
/** Discriminated union for pending Tier-2 propagation items.
|
|
52
|
-
* - `copy`
|
|
53
|
-
* - `callResult`
|
|
52
|
+
* - `copy` — `const b = a` (identifier alias, propagate a's type to b)
|
|
53
|
+
* - `callResult` — `const b = foo()` (bind b to foo's declared return type)
|
|
54
|
+
* - `fieldAccess` — `const b = a.field` (bind b to field's declaredType on a's type)
|
|
55
|
+
* - `methodCallResult` — `const b = a.method()` (bind b to method's returnType on a's type) */
|
|
54
56
|
export type PendingAssignment = {
|
|
55
57
|
kind: 'copy';
|
|
56
58
|
lhs: string;
|
|
@@ -59,10 +61,21 @@ export type PendingAssignment = {
|
|
|
59
61
|
kind: 'callResult';
|
|
60
62
|
lhs: string;
|
|
61
63
|
callee: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'fieldAccess';
|
|
66
|
+
lhs: string;
|
|
67
|
+
receiver: string;
|
|
68
|
+
field: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'methodCallResult';
|
|
71
|
+
lhs: string;
|
|
72
|
+
receiver: string;
|
|
73
|
+
method: string;
|
|
62
74
|
};
|
|
63
75
|
/** Extracts a pending assignment for Tier 2 propagation.
|
|
64
|
-
* Returns a PendingAssignment when the RHS is a bare identifier (`copy`)
|
|
65
|
-
* call expression (`callResult`)
|
|
76
|
+
* Returns a PendingAssignment when the RHS is a bare identifier (`copy`), a
|
|
77
|
+
* call expression (`callResult`), a field access (`fieldAccess`), or a
|
|
78
|
+
* method call with receiver (`methodCallResult`) and the LHS has no resolved type yet.
|
|
66
79
|
* Returns undefined if the node is not a matching assignment. */
|
|
67
80
|
export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => PendingAssignment | undefined;
|
|
68
81
|
/** Extracts a typed variable binding from a pattern-matching construct.
|
|
@@ -459,6 +459,36 @@ const extractPendingAssignment = (node, scopeEnv) => {
|
|
|
459
459
|
continue;
|
|
460
460
|
if (valueNode.type === 'identifier')
|
|
461
461
|
return { kind: 'copy', lhs, rhs: valueNode.text };
|
|
462
|
+
// member_expression RHS → fieldAccess (a.field, this.field)
|
|
463
|
+
if (valueNode.type === 'member_expression') {
|
|
464
|
+
const obj = valueNode.childForFieldName('object');
|
|
465
|
+
const prop = valueNode.childForFieldName('property');
|
|
466
|
+
if (obj && prop?.type === 'property_identifier' &&
|
|
467
|
+
(obj.type === 'identifier' || obj.type === 'this')) {
|
|
468
|
+
return { kind: 'fieldAccess', lhs, receiver: obj.text, field: prop.text };
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Unwrap await: `const user = await fetchUser()` or `await a.getC()`
|
|
473
|
+
const callNode = unwrapAwait(valueNode);
|
|
474
|
+
if (!callNode || callNode.type !== 'call_expression')
|
|
475
|
+
continue;
|
|
476
|
+
const funcNode = callNode.childForFieldName('function');
|
|
477
|
+
if (!funcNode)
|
|
478
|
+
continue;
|
|
479
|
+
// Simple call → callResult: getUser()
|
|
480
|
+
if (funcNode.type === 'identifier') {
|
|
481
|
+
return { kind: 'callResult', lhs, callee: funcNode.text };
|
|
482
|
+
}
|
|
483
|
+
// Method call with receiver → methodCallResult: a.getC()
|
|
484
|
+
if (funcNode.type === 'member_expression') {
|
|
485
|
+
const obj = funcNode.childForFieldName('object');
|
|
486
|
+
const prop = funcNode.childForFieldName('property');
|
|
487
|
+
if (obj && prop?.type === 'property_identifier' &&
|
|
488
|
+
(obj.type === 'identifier' || obj.type === 'this')) {
|
|
489
|
+
return { kind: 'methodCallResult', lhs, receiver: obj.text, method: prop.text };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
462
492
|
}
|
|
463
493
|
return undefined;
|
|
464
494
|
};
|
|
@@ -110,4 +110,29 @@ export declare function extractCallChain(receiverCallNode: SyntaxNode): {
|
|
|
110
110
|
chain: string[];
|
|
111
111
|
baseReceiverName: string | undefined;
|
|
112
112
|
} | undefined;
|
|
113
|
+
/** One step in a mixed receiver chain. */
|
|
114
|
+
export type MixedChainStep = {
|
|
115
|
+
kind: 'field' | 'call';
|
|
116
|
+
name: string;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Walk a receiver AST node that may interleave field accesses and method calls,
|
|
120
|
+
* building a unified chain of steps up to MAX_CHAIN_DEPTH.
|
|
121
|
+
*
|
|
122
|
+
* For `svc.getUser().address.save()`, called with the receiver of `save`
|
|
123
|
+
* (`svc.getUser().address`, a field access node):
|
|
124
|
+
* returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
|
|
125
|
+
* baseReceiverName: 'svc' }
|
|
126
|
+
*
|
|
127
|
+
* For `user.getAddress().city.getName()`, called with receiver of `getName`
|
|
128
|
+
* (`user.getAddress().city`):
|
|
129
|
+
* returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
|
|
130
|
+
* baseReceiverName: 'user' }
|
|
131
|
+
*
|
|
132
|
+
* Pure field chains and pure call chains are special cases (all steps same kind).
|
|
133
|
+
*/
|
|
134
|
+
export declare function extractMixedChain(receiverNode: SyntaxNode): {
|
|
135
|
+
chain: MixedChainStep[];
|
|
136
|
+
baseReceiverName: string | undefined;
|
|
137
|
+
} | undefined;
|
|
113
138
|
export {};
|