ucn 4.2.2 → 5.0.1

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.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +445 -300
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -131
  13. package/core/cache.js +533 -11
  14. package/core/callers.js +5533 -494
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +421 -20
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +204 -42
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +216 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -177
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +371 -116
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +428 -16
  65. package/languages/javascript.js +452 -49
  66. package/languages/python.js +1041 -32
  67. package/languages/rust.js +1415 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +41 -24
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
package/languages/java.js CHANGED
@@ -64,7 +64,10 @@ function extractModifiers(node) {
64
64
  }
65
65
  continue;
66
66
  }
67
- modifiers.push(mod.text);
67
+ // Comments are named children of a Java modifiers node in
68
+ // tree-sitter. Keyword modifiers are recovered from the bounded
69
+ // declaration prefix below; never turn arbitrary comment text
70
+ // into a semantic modifier/annotation.
68
71
  }
69
72
  }
70
73
 
@@ -328,6 +331,15 @@ function findFunctions(code, parser) {
328
331
  * Returns true if node was matched, false otherwise
329
332
  */
330
333
  function _processClass(node, classes, processedRanges, lines, code) {
334
+ const enclosingTypeName = current => {
335
+ for (let p = current.parent; p; p = p.parent) {
336
+ if (p.type === 'class_declaration' || p.type === 'interface_declaration' ||
337
+ p.type === 'enum_declaration' || p.type === 'record_declaration') {
338
+ return p.childForFieldName('name')?.text || null;
339
+ }
340
+ }
341
+ return null;
342
+ };
331
343
  // Class declarations
332
344
  if (node.type === 'class_declaration') {
333
345
  const rangeKey = `${node.startIndex}-${node.endIndex}`;
@@ -347,8 +359,9 @@ function _processClass(node, classes, processedRanges, lines, code) {
347
359
  const implementsInfo = extractImplements(node);
348
360
 
349
361
  // Check if this is a nested/inner class
350
- let parentNode = node.parent;
362
+ const parentNode = node.parent;
351
363
  const isNested = parentNode && parentNode.type === 'class_body';
364
+ const enclosingType = enclosingTypeName(node);
352
365
 
353
366
  classes.push({
354
367
  name: nameNode.text,
@@ -358,6 +371,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
358
371
  members,
359
372
  modifiers,
360
373
  ...(isNested && { isNested: true }),
374
+ ...(enclosingType && { enclosingType }),
361
375
  ...(docstring && { docstring }),
362
376
  ...(generics && { generics }),
363
377
  ...(annotations.length > 0 && { annotations }),
@@ -392,6 +406,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
392
406
  type: 'interface',
393
407
  members: extractClassMembers(node, lines),
394
408
  modifiers,
409
+ ...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
395
410
  ...(docstring && { docstring }),
396
411
  ...(generics && { generics }),
397
412
  ...(annotations.length > 0 && { annotations }),
@@ -423,6 +438,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
423
438
  type: 'enum',
424
439
  members: extractEnumConstants(node, lines),
425
440
  modifiers,
441
+ ...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
426
442
  ...(docstring && { docstring }),
427
443
  ...(annotations.length > 0 && { annotations }),
428
444
  ...(annotationsWithArgs.length > 0 && { annotationsWithArgs })
@@ -478,6 +494,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
478
494
  type: 'record',
479
495
  members,
480
496
  modifiers,
497
+ ...(enclosingTypeName(node) && { enclosingType: enclosingTypeName(node) }),
481
498
  ...(docstring && { docstring }),
482
499
  ...(generics && { generics }),
483
500
  ...(annotations.length > 0 && { annotations }),
@@ -828,6 +845,14 @@ function parse(code, parser) {
828
845
  const stateObjects = [];
829
846
  const processedFn = new Set();
830
847
  const processedCls = new Set();
848
+ let namespace = null;
849
+ for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
850
+ const child = tree.rootNode.namedChild(i);
851
+ if (child.type === 'package_declaration') {
852
+ namespace = child.namedChild(0)?.text || null;
853
+ break;
854
+ }
855
+ }
831
856
 
832
857
  traverseTreeCached(tree.rootNode, (node) => {
833
858
  _processFunction(node, functions, processedFn, lines, code);
@@ -839,6 +864,11 @@ function parse(code, parser) {
839
864
  functions.sort((a, b) => a.startLine - b.startLine);
840
865
  classes.sort((a, b) => a.startLine - b.startLine);
841
866
  stateObjects.sort((a, b) => a.startLine - b.startLine);
867
+ if (namespace) {
868
+ for (const item of [...functions, ...classes, ...stateObjects]) {
869
+ if (!item.namespace) item.namespace = namespace;
870
+ }
871
+ }
842
872
 
843
873
  return {
844
874
  language: 'java',
@@ -864,6 +894,35 @@ function findCallsInCode(code, parser) {
864
894
  const functionStack = []; // Stack of { name, startLine, endLine }
865
895
  // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
866
896
  const scopeTypes = new Map();
897
+ const scopeRawTypes = new Map();
898
+ const scopeTypeQualifiers = new Map();
899
+ // Preserve import ownership in nested-call argument kinds. A simple
900
+ // `Method.getReturnType()` marker is not enough to distinguish
901
+ // java.lang.reflect.Method from a project class with the same name.
902
+ const importedTypeNames = new Map();
903
+ const localMethodReturnTypes = new Map();
904
+ traverseTreeCached(tree.rootNode, (node) => {
905
+ if (node.type === 'import_declaration') {
906
+ const spec = String(node.text || '')
907
+ .replace(/^import\s+/, '')
908
+ .replace(/^static\s+/, '')
909
+ .replace(/;\s*$/, '')
910
+ .trim();
911
+ if (!spec || spec.endsWith('.*')) return true;
912
+ const simple = spec.split('.').pop();
913
+ if (simple) importedTypeNames.set(simple, spec);
914
+ } else if (node.type === 'method_declaration') {
915
+ const name = node.childForFieldName('name')?.text;
916
+ const result = node.childForFieldName('type')?.text;
917
+ if (name && result) {
918
+ if (!localMethodReturnTypes.has(name)) {
919
+ localMethodReturnTypes.set(name, new Set());
920
+ }
921
+ localMethodReturnTypes.get(name).add(result);
922
+ }
923
+ }
924
+ return true;
925
+ });
867
926
 
868
927
  // Helper: extract first string-arg literal from a method_invocation node.
869
928
  // Used by route extraction to capture path arg of webClient.uri("/users") etc.
@@ -883,6 +942,10 @@ function findCallsInCode(code, parser) {
883
942
  const isFunctionNode = (node) => {
884
943
  return ['method_declaration', 'constructor_declaration', 'lambda_expression'].includes(node.type);
885
944
  };
945
+ const JAVA_TYPE_DECLARATIONS = new Set([
946
+ 'class_declaration', 'interface_declaration',
947
+ 'enum_declaration', 'record_declaration',
948
+ ]);
886
949
 
887
950
  // Extract type name from a Java type node (strips generics, qualified names)
888
951
  const extractTypeName = (typeNode) => {
@@ -906,10 +969,69 @@ function findCallsInCode(code, parser) {
906
969
  }
907
970
  return null;
908
971
  };
972
+ // A bare nested type is resolved through its lexical owner:
973
+ // `JavaFile(Builder b)` inside JavaFile means JavaFile.Builder, and
974
+ // `Builder copy(Builder b)` inside JavaFile.Builder means the same nested
975
+ // type. Preserve that owner just as an explicit `JavaFile.Builder`
976
+ // annotation does; flattening all of them to `Builder` conflates every
977
+ // builder class in a package.
978
+ const lexicalNestedTypeOwner = (node, typeName) => {
979
+ if (!node || !typeName) return null;
980
+ const enclosingTypes = [];
981
+ for (let p = node.parent; p; p = p.parent) {
982
+ if (JAVA_TYPE_DECLARATIONS.has(p.type)) enclosingTypes.push(p);
983
+ }
984
+ for (let i = 0; i < enclosingTypes.length; i++) {
985
+ const owner = enclosingTypes[i];
986
+ const ownerName = owner.childForFieldName('name')?.text;
987
+ if (!ownerName) continue;
988
+ if (ownerName === typeName) {
989
+ return enclosingTypes[i + 1]?.childForFieldName('name')?.text || null;
990
+ }
991
+ const body = owner.childForFieldName('body');
992
+ if (!body) continue;
993
+ for (let j = 0; j < body.namedChildCount; j++) {
994
+ const child = body.namedChild(j);
995
+ if (JAVA_TYPE_DECLARATIONS.has(child.type) &&
996
+ child.childForFieldName('name')?.text === typeName) {
997
+ return ownerName;
998
+ }
999
+ }
1000
+ }
1001
+ return null;
1002
+ };
1003
+ const extractTypeQualifier = (typeNode) => {
1004
+ if (!typeNode) return null;
1005
+ const text = String(typeNode.text || '').replace(/<.*$/, '');
1006
+ const parts = text.split('.');
1007
+ if (parts.length >= 2) {
1008
+ const qualifier = parts[parts.length - 2];
1009
+ // A capitalized penultimate segment is a nested owner
1010
+ // (Outer.Builder). Lowercase segments are packages and do not
1011
+ // distinguish same-named top-level types by themselves.
1012
+ if (/^[A-Z]/.test(qualifier)) return qualifier;
1013
+ }
1014
+ return lexicalNestedTypeOwner(typeNode, extractTypeName(typeNode));
1015
+ };
1016
+ const qualifyTypeName = (text) => {
1017
+ const raw = String(text || '').trim()
1018
+ .replace(/^\?\s+extends\s+/, '')
1019
+ .replace(/^\?\s+super\s+/, '');
1020
+ const base = raw.replace(/<.*$/s, '').replace(/\[\]$/, '').trim();
1021
+ if (!base) return null;
1022
+ if (base.includes('.')) return base;
1023
+ if (importedTypeNames.has(base)) return importedTypeNames.get(base);
1024
+ if (['Class', 'Enum', 'Object', 'String', 'Throwable'].includes(base)) {
1025
+ return `java.lang.${base}`;
1026
+ }
1027
+ return base;
1028
+ };
909
1029
 
910
1030
  // Build type map from method/constructor parameters
911
1031
  const buildScopeTypeMap = (node) => {
912
1032
  const typeMap = new Map();
1033
+ const rawTypeMap = new Map();
1034
+ const qualifierMap = new Map();
913
1035
  const paramsNode = node.childForFieldName('parameters');
914
1036
  if (paramsNode) {
915
1037
  for (let i = 0; i < paramsNode.namedChildCount; i++) {
@@ -920,11 +1042,14 @@ function findCallsInCode(code, parser) {
920
1042
  const typeName = extractTypeName(typeNode);
921
1043
  if (nameNode && typeName) {
922
1044
  typeMap.set(nameNode.text, typeName);
1045
+ rawTypeMap.set(nameNode.text, typeNode.text);
1046
+ const qualifier = extractTypeQualifier(typeNode);
1047
+ if (qualifier) qualifierMap.set(nameNode.text, qualifier);
923
1048
  }
924
1049
  }
925
1050
  }
926
1051
  }
927
- return typeMap;
1052
+ return { typeMap, rawTypeMap, qualifierMap };
928
1053
  };
929
1054
 
930
1055
  // Helper to extract function name from a function node
@@ -958,6 +1083,20 @@ function findCallsInCode(code, parser) {
958
1083
  }
959
1084
  return undefined;
960
1085
  };
1086
+ const getReceiverRawType = (varName) => {
1087
+ for (let i = functionStack.length - 1; i >= 0; i--) {
1088
+ const typeMap = scopeRawTypes.get(functionStack[i].startLine);
1089
+ if (typeMap?.has(varName)) return typeMap.get(varName);
1090
+ }
1091
+ return undefined;
1092
+ };
1093
+ const getReceiverTypeQualifier = (varName) => {
1094
+ for (let i = functionStack.length - 1; i >= 0; i--) {
1095
+ const qualifierMap = scopeTypeQualifiers.get(functionStack[i].startLine);
1096
+ if (qualifierMap?.has(varName)) return qualifierMap.get(varName);
1097
+ }
1098
+ return undefined;
1099
+ };
961
1100
 
962
1101
  // Variable receiving this call's result (fix #207 return-type flow):
963
1102
  // `var x = find();` / `x = find();` → 'x'. Declared-type locals are
@@ -1032,6 +1171,45 @@ function findCallsInCode(code, parser) {
1032
1171
  }
1033
1172
  return undefined;
1034
1173
  };
1174
+ const hasEnclosingField = (n, name) => {
1175
+ for (let p = n.parent; p; p = p.parent) {
1176
+ if (p.type !== 'class_declaration' && p.type !== 'interface_declaration' &&
1177
+ p.type !== 'enum_declaration' && p.type !== 'record_declaration') continue;
1178
+ const body = p.childForFieldName('body');
1179
+ if (!body) return false;
1180
+ for (let i = 0; i < body.namedChildCount; i++) {
1181
+ const child = body.namedChild(i);
1182
+ if (child.type !== 'field_declaration') continue;
1183
+ for (let j = 0; j < child.namedChildCount; j++) {
1184
+ const decl = child.namedChild(j);
1185
+ if (decl.type === 'variable_declarator' &&
1186
+ decl.childForFieldName('name')?.text === name) return true;
1187
+ }
1188
+ }
1189
+ return false;
1190
+ }
1191
+ return false;
1192
+ };
1193
+ const enclosingFieldTypeNode = (n, name) => {
1194
+ for (let p = n.parent; p; p = p.parent) {
1195
+ if (!JAVA_TYPE_DECLARATIONS.has(p.type)) continue;
1196
+ const body = p.childForFieldName('body');
1197
+ if (!body) return null;
1198
+ for (let i = 0; i < body.namedChildCount; i++) {
1199
+ const child = body.namedChild(i);
1200
+ if (child.type !== 'field_declaration') continue;
1201
+ for (let j = 0; j < child.namedChildCount; j++) {
1202
+ const decl = child.namedChild(j);
1203
+ if (decl.type === 'variable_declarator' &&
1204
+ decl.childForFieldName('name')?.text === name) {
1205
+ return child.childForFieldName('type') || null;
1206
+ }
1207
+ }
1208
+ }
1209
+ return null;
1210
+ }
1211
+ return null;
1212
+ };
1035
1213
 
1036
1214
  // Call-site argument shape: count + per-arg static kind. Kinds feed the
1037
1215
  // overload discipline in findCallers (Java is the only supported language
@@ -1046,6 +1224,39 @@ function findCallsInCode(code, parser) {
1046
1224
  if (d >= 0) t = t.substring(d + 1);
1047
1225
  return t.trim();
1048
1226
  };
1227
+ const methodCallPathOf = (node) => {
1228
+ if (node?.type !== 'method_invocation') return null;
1229
+ const method = node.childForFieldName('name')?.text;
1230
+ if (!method) return null;
1231
+ let ownerNode = node.childForFieldName('object');
1232
+ if (!ownerNode) {
1233
+ const owner = findEnclosingClassName(node);
1234
+ return owner ? { owner, methods: [method] } : null;
1235
+ }
1236
+ while (ownerNode?.type === 'parenthesized_expression' &&
1237
+ ownerNode.namedChildCount === 1) {
1238
+ ownerNode = ownerNode.namedChild(0);
1239
+ }
1240
+ if (ownerNode?.type === 'method_invocation') {
1241
+ const parent = methodCallPathOf(ownerNode);
1242
+ return parent
1243
+ ? { owner: parent.owner, methods: [...parent.methods, method] }
1244
+ : null;
1245
+ }
1246
+ let ownerType = null;
1247
+ if (ownerNode?.type === 'identifier') {
1248
+ ownerType = getReceiverRawType(ownerNode.text) ||
1249
+ getReceiverType(ownerNode.text) ||
1250
+ (/^[A-Z]/.test(ownerNode.text) ? ownerNode.text : null);
1251
+ } else if (ownerNode?.type === 'cast_expression') {
1252
+ ownerType = ownerNode.childForFieldName('type')?.text;
1253
+ } else if (ownerNode?.type === 'object_creation_expression') {
1254
+ ownerType = ownerNode.childForFieldName('type')?.text;
1255
+ }
1256
+ return ownerType
1257
+ ? { owner: qualifyTypeName(ownerType), methods: [method] }
1258
+ : null;
1259
+ };
1049
1260
  const argKindOf = (arg) => {
1050
1261
  switch (arg.type) {
1051
1262
  case 'string_literal': return 'string';
@@ -1063,33 +1274,151 @@ function findCallsInCode(code, parser) {
1063
1274
  case 'null_literal': return 'null';
1064
1275
  case 'object_creation_expression': {
1065
1276
  const tn = arg.childForFieldName('type');
1066
- return tn ? `new:${bareTypeName(tn.text)}` : 'expr';
1277
+ return tn ? `new:${qualifyTypeName(tn.text)}` : 'expr';
1067
1278
  }
1068
1279
  case 'cast_expression': {
1069
1280
  const tn = arg.childForFieldName('type');
1070
- return tn ? `cast:${bareTypeName(tn.text)}` : 'expr';
1281
+ return tn ? `cast:${qualifyTypeName(tn.text)}` : 'expr';
1071
1282
  }
1072
1283
  case 'class_literal': {
1073
1284
  const tn = arg.namedChild(0);
1074
1285
  return tn ? `class:${bareTypeName(tn.text)}` : 'class:Object';
1075
1286
  }
1076
1287
  case 'identifier': {
1077
- const typeName = getReceiverType(arg.text);
1078
- return typeName ? `type:${bareTypeName(typeName)}` : 'expr';
1288
+ const fieldType = !isDeclaredLocal(arg.text)
1289
+ ? enclosingFieldTypeNode(arg, arg.text) : null;
1290
+ const typeName = getReceiverType(arg.text) ||
1291
+ extractTypeName(fieldType);
1292
+ const rawType = getReceiverRawType(arg.text) ||
1293
+ fieldType?.text;
1294
+ return typeName
1295
+ ? `type:${qualifyTypeName(rawType || typeName)}`
1296
+ : 'expr';
1297
+ }
1298
+ case 'field_access': {
1299
+ // A class-qualified static field has a compiler-visible
1300
+ // declared type (`TypeName.DOUBLE` is TypeName). Preserve
1301
+ // owner + field identity in the call IR and resolve the
1302
+ // declaration from the project index at query time. Do not
1303
+ // flatten it to the owner's type: the field may hold any
1304
+ // declared value type.
1305
+ const ownerNode = arg.childForFieldName('object');
1306
+ const fieldNode = arg.childForFieldName('field');
1307
+ if (!ownerNode || !fieldNode) return 'expr';
1308
+ let ownerType = null;
1309
+ if (ownerNode.type === 'identifier') {
1310
+ ownerType = getReceiverRawType(ownerNode.text) ||
1311
+ getReceiverType(ownerNode.text) ||
1312
+ (/^[A-Z]/.test(ownerNode.text) ? ownerNode.text : null);
1313
+ } else if (ownerNode.type === 'field_access' ||
1314
+ ownerNode.type === 'scoped_identifier') {
1315
+ ownerType = ownerNode.text;
1316
+ }
1317
+ const qualifiedOwner = ownerType && qualifyTypeName(ownerType);
1318
+ return qualifiedOwner
1319
+ ? `field:${qualifiedOwner}:${fieldNode.text}`
1320
+ : 'expr';
1079
1321
  }
1080
1322
  case 'method_invocation': {
1081
1323
  // Preserve compiler-visible producer ownership for nested
1082
- // static factories: build(CodeBlock.of(...)) can select a
1083
- // CodeBlock overload without guessing from the method name.
1324
+ // factories and helpers. Imported owners stay qualified so
1325
+ // platform contracts cannot be confused with project types
1326
+ // that share a simple name.
1084
1327
  const ownerNode = arg.childForFieldName('object');
1085
1328
  const methodNode = arg.childForFieldName('name');
1086
- if (ownerNode?.type === 'identifier' && methodNode) {
1087
- const ownerType = getReceiverType(ownerNode.text) ||
1088
- (/^[A-Z]/.test(ownerNode.text) ? ownerNode.text : null);
1089
- if (ownerType) return `call:${bareTypeName(ownerType)}:${methodNode.text}`;
1329
+ if (!methodNode) return 'expr';
1330
+ if (!ownerNode) {
1331
+ const owner = findEnclosingClassName(arg);
1332
+ return owner ? `call:${owner}:${methodNode.text}` : 'expr';
1333
+ }
1334
+ let normalizedOwner = ownerNode;
1335
+ while (normalizedOwner?.type === 'parenthesized_expression' &&
1336
+ normalizedOwner.namedChildCount === 1) {
1337
+ normalizedOwner = normalizedOwner.namedChild(0);
1338
+ }
1339
+ let ownerType = null;
1340
+ let ownerRawType = null;
1341
+ if (normalizedOwner?.type === 'identifier') {
1342
+ ownerType = getReceiverType(normalizedOwner.text) ||
1343
+ (/^[A-Z]/.test(normalizedOwner.text) ? normalizedOwner.text : null);
1344
+ ownerRawType = getReceiverRawType(normalizedOwner.text);
1345
+ } else if (normalizedOwner?.type === 'cast_expression') {
1346
+ const typeNode = normalizedOwner.childForFieldName('type');
1347
+ ownerType = extractTypeName(typeNode);
1348
+ ownerRawType = typeNode?.text;
1349
+ } else if (normalizedOwner?.type === 'object_creation_expression') {
1350
+ const typeNode = normalizedOwner.childForFieldName('type');
1351
+ ownerType = extractTypeName(typeNode);
1352
+ ownerRawType = typeNode?.text;
1353
+ } else if (normalizedOwner?.type === 'method_invocation') {
1354
+ const producerName = normalizedOwner.childForFieldName('name')?.text;
1355
+ const producerObject = normalizedOwner.childForFieldName('object');
1356
+ if (producerName && !producerObject) {
1357
+ const returns = localMethodReturnTypes.get(producerName);
1358
+ if (returns?.size === 1) {
1359
+ ownerRawType = [...returns][0];
1360
+ ownerType = bareTypeName(ownerRawType);
1361
+ }
1362
+ }
1363
+ if (!ownerType) {
1364
+ const producerKind = argKindOf(normalizedOwner);
1365
+ if (producerKind.startsWith('type:')) {
1366
+ ownerType = producerKind.slice('type:'.length);
1367
+ ownerRawType = ownerType;
1368
+ }
1369
+ }
1370
+ }
1371
+ // Generic container access has a source-declared result type:
1372
+ // List<T>.get(i) -> T, Map<K,V>.get(k) -> V. Wildcard
1373
+ // `? extends T` is safe in value position; `? super T` is not.
1374
+ if (methodNode.text === 'get' && ownerRawType?.includes('<')) {
1375
+ const generic = ownerRawType.slice(
1376
+ ownerRawType.indexOf('<') + 1,
1377
+ ownerRawType.lastIndexOf('>'));
1378
+ const args = [];
1379
+ let start = 0;
1380
+ let depth = 0;
1381
+ for (let i = 0; i <= generic.length; i++) {
1382
+ const ch = generic[i];
1383
+ if (ch === '<') depth++;
1384
+ else if (ch === '>') depth--;
1385
+ else if ((ch === ',' || i === generic.length) && depth === 0) {
1386
+ args.push(generic.slice(start, i).trim());
1387
+ start = i + 1;
1388
+ }
1389
+ }
1390
+ const base = bareTypeName(ownerRawType);
1391
+ const picked = base === 'Map' ? args[1] : args[0];
1392
+ if (picked && !/^\?\s+super\b/.test(picked)) {
1393
+ const valueType = picked.replace(/^\?\s+extends\s+/, '').trim();
1394
+ if (valueType && valueType !== '?') {
1395
+ return `type:${qualifyTypeName(valueType)}`;
1396
+ }
1397
+ }
1398
+ }
1399
+ if (ownerType) {
1400
+ return `call:${qualifyTypeName(ownerRawType || ownerType)}:${methodNode.text}`;
1401
+ }
1402
+ const path = methodCallPathOf(arg);
1403
+ if (path?.methods.length > 1) {
1404
+ return `chain:${path.owner}#${path.methods.join('#')}`;
1090
1405
  }
1091
1406
  return 'expr';
1092
1407
  }
1408
+ case 'array_access': {
1409
+ const arrayNode = arg.childForFieldName('array') || arg.namedChild(0);
1410
+ if (!arrayNode) return 'expr';
1411
+ if (arrayNode.type === 'identifier') {
1412
+ const rawType = getReceiverRawType(arrayNode.text);
1413
+ if (rawType?.endsWith('[]')) {
1414
+ return `type:${qualifyTypeName(rawType.slice(0, -2))}`;
1415
+ }
1416
+ }
1417
+ const containerKind = argKindOf(arrayNode);
1418
+ return containerKind !== 'expr' ? `element:${containerKind}` : 'expr';
1419
+ }
1420
+ case 'parenthesized_expression':
1421
+ return arg.namedChildCount === 1 ? argKindOf(arg.namedChild(0)) : 'expr';
1093
1422
  case 'lambda_expression':
1094
1423
  case 'method_reference': return 'lambda';
1095
1424
  case 'unary_expression':
@@ -1119,7 +1448,10 @@ function findCallsInCode(code, parser) {
1119
1448
  endLine: node.endPosition.row + 1
1120
1449
  };
1121
1450
  functionStack.push(entry);
1122
- scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
1451
+ const builtScope = buildScopeTypeMap(node);
1452
+ scopeTypes.set(entry.startLine, builtScope.typeMap);
1453
+ scopeRawTypes.set(entry.startLine, builtScope.rawTypeMap);
1454
+ scopeTypeQualifiers.set(entry.startLine, builtScope.qualifierMap);
1123
1455
  scopeDeclared.set(entry.startLine, collectDeclaredNames(node));
1124
1456
  }
1125
1457
 
@@ -1130,16 +1462,25 @@ function findCallsInCode(code, parser) {
1130
1462
  const typeNode = node.childForFieldName('type');
1131
1463
  const nameNode = node.childForFieldName('name');
1132
1464
  const typeName = extractTypeName(typeNode);
1465
+ const typeQualifier = extractTypeQualifier(typeNode);
1133
1466
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1134
1467
  const typeMap = scopeTypes.get(scopeKey);
1135
1468
  if (nameNode && typeName && typeMap) {
1469
+ const rawTypeMap = scopeRawTypes.get(scopeKey);
1136
1470
  scopedTypeRestores.set(node.id, {
1137
1471
  typeMap,
1472
+ rawTypeMap,
1138
1473
  name: nameNode.text,
1139
1474
  had: typeMap.has(nameNode.text),
1140
1475
  previous: typeMap.get(nameNode.text),
1476
+ rawHad: rawTypeMap?.has(nameNode.text),
1477
+ rawPrevious: rawTypeMap?.get(nameNode.text),
1141
1478
  });
1142
1479
  typeMap.set(nameNode.text, typeName);
1480
+ rawTypeMap?.set(nameNode.text, typeNode.text);
1481
+ const qualifierMap = scopeTypeQualifiers.get(scopeKey);
1482
+ if (typeQualifier) qualifierMap?.set(nameNode.text, typeQualifier);
1483
+ else qualifierMap?.delete(nameNode.text);
1143
1484
  }
1144
1485
  }
1145
1486
 
@@ -1150,13 +1491,33 @@ function findCallsInCode(code, parser) {
1150
1491
 
1151
1492
  if (nameNode) {
1152
1493
  const enclosingFunction = getCurrentEnclosingFunction();
1153
- const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
1154
- const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
1494
+ let receiverNode = objNode;
1495
+ while (receiverNode?.type === 'parenthesized_expression' && receiverNode.namedChildCount === 1) {
1496
+ receiverNode = receiverNode.namedChild(0);
1497
+ }
1498
+ let castReceiverType;
1499
+ let castReceiverName;
1500
+ if (receiverNode?.type === 'cast_expression') {
1501
+ castReceiverType = extractTypeName(receiverNode.childForFieldName('type'));
1502
+ const valueNode = receiverNode.childForFieldName('value');
1503
+ if (valueNode?.type === 'identifier') castReceiverName = valueNode.text;
1504
+ }
1505
+ const receiver = castReceiverName ||
1506
+ ((receiverNode?.type === 'identifier' || receiverNode?.type === 'this')
1507
+ ? receiverNode.text : undefined);
1508
+ const receiverType = castReceiverType ||
1509
+ ((receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined);
1510
+ const receiverTypeQualifier = !castReceiverType && receiver
1511
+ ? getReceiverTypeQualifier(receiver) : undefined;
1512
+ const receiverIsTypeQualified = !!(receiverNode?.type === 'identifier' &&
1513
+ receiver && /^[A-Z]/.test(receiver) && !receiverType &&
1514
+ !isDeclaredLocal(receiver) && !hasEnclosingField(node, receiver));
1155
1515
  // fix #202: one-hop declared-field receivers —
1156
1516
  // this.service.execute(), svc.client.run(), and bare
1157
1517
  // service.execute() where service is a class field (only when
1158
1518
  // no same-named local is declared anywhere in the method).
1159
1519
  let receiverRoot, receiverFieldName, receiverRootType;
1520
+ let receiverRootNamespace;
1160
1521
  if (objNode && !receiverType) {
1161
1522
  if (objNode.type === 'field_access') {
1162
1523
  const rootNode = objNode.childForFieldName('object');
@@ -1177,6 +1538,8 @@ function findCallsInCode(code, parser) {
1177
1538
  receiverRoot = rootNode.text;
1178
1539
  receiverFieldName = fldNode.text;
1179
1540
  receiverRootType = rootType;
1541
+ receiverRootNamespace =
1542
+ getReceiverTypeQualifier(rootNode.text);
1180
1543
  }
1181
1544
  }
1182
1545
  }
@@ -1206,6 +1569,13 @@ function findCallsInCode(code, parser) {
1206
1569
  const firstArg = getFirstStringArg(node);
1207
1570
  const callArgs = getCallArgs(node);
1208
1571
  const assignedTo = assignmentTargetOf(node);
1572
+ const receiverPath = objNode?.type === 'method_invocation'
1573
+ ? methodCallPathOf(objNode) : null;
1574
+ const receiverCallTypePath = receiverPath
1575
+ ? (receiverPath.methods.length === 1
1576
+ ? `call:${receiverPath.owner}:${receiverPath.methods[0]}`
1577
+ : `chain:${receiverPath.owner}#${receiverPath.methods.join('#')}`)
1578
+ : null;
1209
1579
  calls.push({
1210
1580
  name: nameNode.text,
1211
1581
  // Multi-line chains (builder.x()\n.y()) must report each
@@ -1215,11 +1585,18 @@ function findCallsInCode(code, parser) {
1215
1585
  isMethod: !!objNode,
1216
1586
  receiver,
1217
1587
  ...(receiverType && { receiverType }),
1588
+ ...(receiverTypeQualifier && { receiverTypeQualifier }),
1589
+ ...(receiverIsTypeQualified && { receiverIsTypeQualified: true }),
1590
+ ...(castReceiverType && { receiverTypeCast: true }),
1218
1591
  ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
1219
1592
  ...(receiverFieldName && receiverRootType && { receiverRootType }),
1593
+ ...(receiverFieldName && receiverRootNamespace && {
1594
+ receiverRootNamespace,
1595
+ }),
1220
1596
  ...(receiverCall && { receiverCall }),
1221
1597
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1222
1598
  ...(receiverCallLine && { receiverCallLine }),
1599
+ ...(receiverCallTypePath && { receiverCallTypePath }),
1223
1600
  argCount: callArgs.argCount,
1224
1601
  ...(callArgs.argKinds && { argKinds: callArgs.argKinds }),
1225
1602
  ...(assignedTo && { assignedTo }),
@@ -1371,6 +1748,8 @@ function findCallsInCode(code, parser) {
1371
1748
  const declTypeNode = node.childForFieldName('type');
1372
1749
  const declaredType = declTypeNode && declTypeNode.text !== 'var'
1373
1750
  ? extractTypeName(declTypeNode) : null;
1751
+ const declaredTypeQualifier = declTypeNode && declTypeNode.text !== 'var'
1752
+ ? extractTypeQualifier(declTypeNode) : null;
1374
1753
  for (let i = 0; i < node.namedChildCount; i++) {
1375
1754
  const child = node.namedChild(i);
1376
1755
  if (child.type === 'variable_declarator') {
@@ -1381,11 +1760,23 @@ function findCallsInCode(code, parser) {
1381
1760
  let typeName = valueNode?.type === 'object_creation_expression'
1382
1761
  ? extractTypeName(valueNode.childForFieldName('type'))
1383
1762
  : null;
1763
+ let typeQualifier = valueNode?.type === 'object_creation_expression'
1764
+ ? extractTypeQualifier(valueNode.childForFieldName('type'))
1765
+ : null;
1384
1766
  if (!typeName) typeName = declaredType;
1767
+ if (!typeQualifier) typeQualifier = declaredTypeQualifier;
1385
1768
  if (nameNode && typeName) {
1386
1769
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1387
1770
  const typeMap = scopeTypes.get(scopeKey);
1388
1771
  if (typeMap) typeMap.set(nameNode.text, typeName);
1772
+ const rawTypeMap = scopeRawTypes.get(scopeKey);
1773
+ const rawType = valueNode?.type === 'object_creation_expression'
1774
+ ? valueNode.childForFieldName('type')?.text
1775
+ : declTypeNode?.text;
1776
+ if (rawType) rawTypeMap?.set(nameNode.text, rawType);
1777
+ const qualifierMap = scopeTypeQualifiers.get(scopeKey);
1778
+ if (typeQualifier) qualifierMap?.set(nameNode.text, typeQualifier);
1779
+ else qualifierMap?.delete(nameNode.text);
1389
1780
  }
1390
1781
  }
1391
1782
  }
@@ -1401,13 +1792,25 @@ function findCallsInCode(code, parser) {
1401
1792
  let typeName = resValueNode?.type === 'object_creation_expression'
1402
1793
  ? extractTypeName(resValueNode.childForFieldName('type'))
1403
1794
  : null;
1795
+ let typeQualifier = resValueNode?.type === 'object_creation_expression'
1796
+ ? extractTypeQualifier(resValueNode.childForFieldName('type'))
1797
+ : null;
1404
1798
  if (!typeName && resTypeNode && resTypeNode.text !== 'var') {
1405
1799
  typeName = extractTypeName(resTypeNode);
1800
+ typeQualifier = extractTypeQualifier(resTypeNode);
1406
1801
  }
1407
1802
  if (resNameNode && typeName) {
1408
1803
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1409
1804
  const typeMap = scopeTypes.get(scopeKey);
1410
1805
  if (typeMap) typeMap.set(resNameNode.text, typeName);
1806
+ const rawTypeMap = scopeRawTypes.get(scopeKey);
1807
+ const rawType = resValueNode?.type === 'object_creation_expression'
1808
+ ? resValueNode.childForFieldName('type')?.text
1809
+ : resTypeNode?.text;
1810
+ if (rawType) rawTypeMap?.set(resNameNode.text, rawType);
1811
+ const qualifierMap = scopeTypeQualifiers.get(scopeKey);
1812
+ if (typeQualifier) qualifierMap?.set(resNameNode.text, typeQualifier);
1813
+ else qualifierMap?.delete(resNameNode.text);
1411
1814
  }
1412
1815
  }
1413
1816
 
@@ -1418,12 +1821,21 @@ function findCallsInCode(code, parser) {
1418
1821
  if (restore) {
1419
1822
  if (restore.had) restore.typeMap.set(restore.name, restore.previous);
1420
1823
  else restore.typeMap.delete(restore.name);
1824
+ if (restore.rawTypeMap) {
1825
+ if (restore.rawHad) {
1826
+ restore.rawTypeMap.set(restore.name, restore.rawPrevious);
1827
+ } else {
1828
+ restore.rawTypeMap.delete(restore.name);
1829
+ }
1830
+ }
1421
1831
  scopedTypeRestores.delete(node.id);
1422
1832
  }
1423
1833
  if (isFunctionNode(node)) {
1424
1834
  const leaving = functionStack.pop();
1425
1835
  if (leaving) {
1426
1836
  scopeTypes.delete(leaving.startLine);
1837
+ scopeRawTypes.delete(leaving.startLine);
1838
+ scopeTypeQualifiers.delete(leaving.startLine);
1427
1839
  }
1428
1840
  }
1429
1841
  }