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.
- package/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +445 -300
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -131
- package/core/cache.js +533 -11
- package/core/callers.js +5533 -494
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +421 -20
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +204 -42
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +216 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -177
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +371 -116
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +428 -16
- package/languages/javascript.js +452 -49
- package/languages/python.js +1041 -32
- package/languages/rust.js +1415 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +41 -24
- package/eslint.config.js +0 -43
- 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
|
|
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
|
-
|
|
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:${
|
|
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:${
|
|
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
|
|
1078
|
-
|
|
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
|
-
//
|
|
1083
|
-
//
|
|
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 (
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
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
|
}
|