ucn 4.2.3 → 5.0.2

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 +438 -305
  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 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  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 +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  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 +212 -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 -187
  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 +317 -185
  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 +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  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
 
@@ -842,6 +845,14 @@ function parse(code, parser) {
842
845
  const stateObjects = [];
843
846
  const processedFn = new Set();
844
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
+ }
845
856
 
846
857
  traverseTreeCached(tree.rootNode, (node) => {
847
858
  _processFunction(node, functions, processedFn, lines, code);
@@ -853,6 +864,11 @@ function parse(code, parser) {
853
864
  functions.sort((a, b) => a.startLine - b.startLine);
854
865
  classes.sort((a, b) => a.startLine - b.startLine);
855
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
+ }
856
872
 
857
873
  return {
858
874
  language: 'java',
@@ -878,6 +894,35 @@ function findCallsInCode(code, parser) {
878
894
  const functionStack = []; // Stack of { name, startLine, endLine }
879
895
  // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
880
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
+ });
881
926
 
882
927
  // Helper: extract first string-arg literal from a method_invocation node.
883
928
  // Used by route extraction to capture path arg of webClient.uri("/users") etc.
@@ -897,6 +942,10 @@ function findCallsInCode(code, parser) {
897
942
  const isFunctionNode = (node) => {
898
943
  return ['method_declaration', 'constructor_declaration', 'lambda_expression'].includes(node.type);
899
944
  };
945
+ const JAVA_TYPE_DECLARATIONS = new Set([
946
+ 'class_declaration', 'interface_declaration',
947
+ 'enum_declaration', 'record_declaration',
948
+ ]);
900
949
 
901
950
  // Extract type name from a Java type node (strips generics, qualified names)
902
951
  const extractTypeName = (typeNode) => {
@@ -920,10 +969,69 @@ function findCallsInCode(code, parser) {
920
969
  }
921
970
  return null;
922
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
+ };
923
1029
 
924
1030
  // Build type map from method/constructor parameters
925
1031
  const buildScopeTypeMap = (node) => {
926
1032
  const typeMap = new Map();
1033
+ const rawTypeMap = new Map();
1034
+ const qualifierMap = new Map();
927
1035
  const paramsNode = node.childForFieldName('parameters');
928
1036
  if (paramsNode) {
929
1037
  for (let i = 0; i < paramsNode.namedChildCount; i++) {
@@ -934,11 +1042,14 @@ function findCallsInCode(code, parser) {
934
1042
  const typeName = extractTypeName(typeNode);
935
1043
  if (nameNode && typeName) {
936
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);
937
1048
  }
938
1049
  }
939
1050
  }
940
1051
  }
941
- return typeMap;
1052
+ return { typeMap, rawTypeMap, qualifierMap };
942
1053
  };
943
1054
 
944
1055
  // Helper to extract function name from a function node
@@ -972,6 +1083,20 @@ function findCallsInCode(code, parser) {
972
1083
  }
973
1084
  return undefined;
974
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
+ };
975
1100
 
976
1101
  // Variable receiving this call's result (fix #207 return-type flow):
977
1102
  // `var x = find();` / `x = find();` → 'x'. Declared-type locals are
@@ -1046,6 +1171,45 @@ function findCallsInCode(code, parser) {
1046
1171
  }
1047
1172
  return undefined;
1048
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
+ };
1049
1213
 
1050
1214
  // Call-site argument shape: count + per-arg static kind. Kinds feed the
1051
1215
  // overload discipline in findCallers (Java is the only supported language
@@ -1060,6 +1224,39 @@ function findCallsInCode(code, parser) {
1060
1224
  if (d >= 0) t = t.substring(d + 1);
1061
1225
  return t.trim();
1062
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
+ };
1063
1260
  const argKindOf = (arg) => {
1064
1261
  switch (arg.type) {
1065
1262
  case 'string_literal': return 'string';
@@ -1077,33 +1274,151 @@ function findCallsInCode(code, parser) {
1077
1274
  case 'null_literal': return 'null';
1078
1275
  case 'object_creation_expression': {
1079
1276
  const tn = arg.childForFieldName('type');
1080
- return tn ? `new:${bareTypeName(tn.text)}` : 'expr';
1277
+ return tn ? `new:${qualifyTypeName(tn.text)}` : 'expr';
1081
1278
  }
1082
1279
  case 'cast_expression': {
1083
1280
  const tn = arg.childForFieldName('type');
1084
- return tn ? `cast:${bareTypeName(tn.text)}` : 'expr';
1281
+ return tn ? `cast:${qualifyTypeName(tn.text)}` : 'expr';
1085
1282
  }
1086
1283
  case 'class_literal': {
1087
1284
  const tn = arg.namedChild(0);
1088
1285
  return tn ? `class:${bareTypeName(tn.text)}` : 'class:Object';
1089
1286
  }
1090
1287
  case 'identifier': {
1091
- const typeName = getReceiverType(arg.text);
1092
- 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';
1093
1321
  }
1094
1322
  case 'method_invocation': {
1095
1323
  // Preserve compiler-visible producer ownership for nested
1096
- // static factories: build(CodeBlock.of(...)) can select a
1097
- // 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.
1098
1327
  const ownerNode = arg.childForFieldName('object');
1099
1328
  const methodNode = arg.childForFieldName('name');
1100
- if (ownerNode?.type === 'identifier' && methodNode) {
1101
- const ownerType = getReceiverType(ownerNode.text) ||
1102
- (/^[A-Z]/.test(ownerNode.text) ? ownerNode.text : null);
1103
- 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('#')}`;
1104
1405
  }
1105
1406
  return 'expr';
1106
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';
1107
1422
  case 'lambda_expression':
1108
1423
  case 'method_reference': return 'lambda';
1109
1424
  case 'unary_expression':
@@ -1133,7 +1448,10 @@ function findCallsInCode(code, parser) {
1133
1448
  endLine: node.endPosition.row + 1
1134
1449
  };
1135
1450
  functionStack.push(entry);
1136
- 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);
1137
1455
  scopeDeclared.set(entry.startLine, collectDeclaredNames(node));
1138
1456
  }
1139
1457
 
@@ -1144,16 +1462,25 @@ function findCallsInCode(code, parser) {
1144
1462
  const typeNode = node.childForFieldName('type');
1145
1463
  const nameNode = node.childForFieldName('name');
1146
1464
  const typeName = extractTypeName(typeNode);
1465
+ const typeQualifier = extractTypeQualifier(typeNode);
1147
1466
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1148
1467
  const typeMap = scopeTypes.get(scopeKey);
1149
1468
  if (nameNode && typeName && typeMap) {
1469
+ const rawTypeMap = scopeRawTypes.get(scopeKey);
1150
1470
  scopedTypeRestores.set(node.id, {
1151
1471
  typeMap,
1472
+ rawTypeMap,
1152
1473
  name: nameNode.text,
1153
1474
  had: typeMap.has(nameNode.text),
1154
1475
  previous: typeMap.get(nameNode.text),
1476
+ rawHad: rawTypeMap?.has(nameNode.text),
1477
+ rawPrevious: rawTypeMap?.get(nameNode.text),
1155
1478
  });
1156
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);
1157
1484
  }
1158
1485
  }
1159
1486
 
@@ -1180,11 +1507,17 @@ function findCallsInCode(code, parser) {
1180
1507
  ? receiverNode.text : undefined);
1181
1508
  const receiverType = castReceiverType ||
1182
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));
1183
1515
  // fix #202: one-hop declared-field receivers —
1184
1516
  // this.service.execute(), svc.client.run(), and bare
1185
1517
  // service.execute() where service is a class field (only when
1186
1518
  // no same-named local is declared anywhere in the method).
1187
1519
  let receiverRoot, receiverFieldName, receiverRootType;
1520
+ let receiverRootNamespace;
1188
1521
  if (objNode && !receiverType) {
1189
1522
  if (objNode.type === 'field_access') {
1190
1523
  const rootNode = objNode.childForFieldName('object');
@@ -1205,6 +1538,8 @@ function findCallsInCode(code, parser) {
1205
1538
  receiverRoot = rootNode.text;
1206
1539
  receiverFieldName = fldNode.text;
1207
1540
  receiverRootType = rootType;
1541
+ receiverRootNamespace =
1542
+ getReceiverTypeQualifier(rootNode.text);
1208
1543
  }
1209
1544
  }
1210
1545
  }
@@ -1234,6 +1569,13 @@ function findCallsInCode(code, parser) {
1234
1569
  const firstArg = getFirstStringArg(node);
1235
1570
  const callArgs = getCallArgs(node);
1236
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;
1237
1579
  calls.push({
1238
1580
  name: nameNode.text,
1239
1581
  // Multi-line chains (builder.x()\n.y()) must report each
@@ -1243,12 +1585,18 @@ function findCallsInCode(code, parser) {
1243
1585
  isMethod: !!objNode,
1244
1586
  receiver,
1245
1587
  ...(receiverType && { receiverType }),
1588
+ ...(receiverTypeQualifier && { receiverTypeQualifier }),
1589
+ ...(receiverIsTypeQualified && { receiverIsTypeQualified: true }),
1246
1590
  ...(castReceiverType && { receiverTypeCast: true }),
1247
1591
  ...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
1248
1592
  ...(receiverFieldName && receiverRootType && { receiverRootType }),
1593
+ ...(receiverFieldName && receiverRootNamespace && {
1594
+ receiverRootNamespace,
1595
+ }),
1249
1596
  ...(receiverCall && { receiverCall }),
1250
1597
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1251
1598
  ...(receiverCallLine && { receiverCallLine }),
1599
+ ...(receiverCallTypePath && { receiverCallTypePath }),
1252
1600
  argCount: callArgs.argCount,
1253
1601
  ...(callArgs.argKinds && { argKinds: callArgs.argKinds }),
1254
1602
  ...(assignedTo && { assignedTo }),
@@ -1400,6 +1748,8 @@ function findCallsInCode(code, parser) {
1400
1748
  const declTypeNode = node.childForFieldName('type');
1401
1749
  const declaredType = declTypeNode && declTypeNode.text !== 'var'
1402
1750
  ? extractTypeName(declTypeNode) : null;
1751
+ const declaredTypeQualifier = declTypeNode && declTypeNode.text !== 'var'
1752
+ ? extractTypeQualifier(declTypeNode) : null;
1403
1753
  for (let i = 0; i < node.namedChildCount; i++) {
1404
1754
  const child = node.namedChild(i);
1405
1755
  if (child.type === 'variable_declarator') {
@@ -1410,11 +1760,23 @@ function findCallsInCode(code, parser) {
1410
1760
  let typeName = valueNode?.type === 'object_creation_expression'
1411
1761
  ? extractTypeName(valueNode.childForFieldName('type'))
1412
1762
  : null;
1763
+ let typeQualifier = valueNode?.type === 'object_creation_expression'
1764
+ ? extractTypeQualifier(valueNode.childForFieldName('type'))
1765
+ : null;
1413
1766
  if (!typeName) typeName = declaredType;
1767
+ if (!typeQualifier) typeQualifier = declaredTypeQualifier;
1414
1768
  if (nameNode && typeName) {
1415
1769
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1416
1770
  const typeMap = scopeTypes.get(scopeKey);
1417
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);
1418
1780
  }
1419
1781
  }
1420
1782
  }
@@ -1430,13 +1792,25 @@ function findCallsInCode(code, parser) {
1430
1792
  let typeName = resValueNode?.type === 'object_creation_expression'
1431
1793
  ? extractTypeName(resValueNode.childForFieldName('type'))
1432
1794
  : null;
1795
+ let typeQualifier = resValueNode?.type === 'object_creation_expression'
1796
+ ? extractTypeQualifier(resValueNode.childForFieldName('type'))
1797
+ : null;
1433
1798
  if (!typeName && resTypeNode && resTypeNode.text !== 'var') {
1434
1799
  typeName = extractTypeName(resTypeNode);
1800
+ typeQualifier = extractTypeQualifier(resTypeNode);
1435
1801
  }
1436
1802
  if (resNameNode && typeName) {
1437
1803
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1438
1804
  const typeMap = scopeTypes.get(scopeKey);
1439
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);
1440
1814
  }
1441
1815
  }
1442
1816
 
@@ -1447,12 +1821,21 @@ function findCallsInCode(code, parser) {
1447
1821
  if (restore) {
1448
1822
  if (restore.had) restore.typeMap.set(restore.name, restore.previous);
1449
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
+ }
1450
1831
  scopedTypeRestores.delete(node.id);
1451
1832
  }
1452
1833
  if (isFunctionNode(node)) {
1453
1834
  const leaving = functionStack.pop();
1454
1835
  if (leaving) {
1455
1836
  scopeTypes.delete(leaving.startLine);
1837
+ scopeRawTypes.delete(leaving.startLine);
1838
+ scopeTypeQualifiers.delete(leaving.startLine);
1456
1839
  }
1457
1840
  }
1458
1841
  }