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.
- 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 +438 -305
- 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 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- 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 +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- 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 +212 -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 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- 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 +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- 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
|
|
|
@@ -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:${
|
|
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:${
|
|
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
|
|
1092
|
-
|
|
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
|
-
//
|
|
1097
|
-
//
|
|
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 (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
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
|
}
|