ucn 3.8.26 → 4.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 +31 -17
- package/README.md +95 -28
- package/cli/index.js +32 -7
- package/core/account.js +354 -0
- package/core/analysis.js +335 -15
- package/core/build-worker.js +21 -1
- package/core/cache.js +52 -3
- package/core/callers.js +3421 -159
- package/core/confidence.js +82 -19
- package/core/deadcode.js +211 -21
- package/core/execute.js +6 -1
- package/core/graph-build.js +45 -3
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +19 -3
- package/core/output/shared.js +33 -2
- package/core/output/tracing.js +208 -10
- package/core/project.js +19 -2
- package/core/registry.js +15 -3
- package/core/shared.js +21 -0
- package/core/tracing.js +534 -190
- package/languages/go.js +317 -6
- package/languages/index.js +79 -0
- package/languages/java.js +243 -16
- package/languages/javascript.js +357 -24
- package/languages/python.js +423 -28
- package/languages/rust.js +377 -8
- package/languages/utils.js +72 -18
- package/mcp/server.js +5 -4
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/languages/java.js
CHANGED
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
traverseTreeCached,
|
|
11
11
|
nodeToLocation,
|
|
12
12
|
parseStructuredParams,
|
|
13
|
-
extractJavaDocstring
|
|
13
|
+
extractJavaDocstring,
|
|
14
|
+
visitNameNodes,
|
|
14
15
|
} = require('./utils');
|
|
15
16
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
16
17
|
|
|
@@ -704,6 +705,29 @@ function extractClassMembers(classNode, codeOrLines) {
|
|
|
704
705
|
// (one for class, one for constructor), forcing users to disambiguate.
|
|
705
706
|
// Constructor signature info (params, line) remains accessible by reading
|
|
706
707
|
// the class body when needed (e.g. via verify's AST walk).
|
|
708
|
+
|
|
709
|
+
// Field declarations: declared types drive receiver disambiguation
|
|
710
|
+
// (fix #202) — Rust/Go already emit field members with fieldType.
|
|
711
|
+
if (child.type === 'field_declaration') {
|
|
712
|
+
const typeNode = child.childForFieldName('type');
|
|
713
|
+
const fieldTypeText = typeNode ? typeNode.text : null;
|
|
714
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
715
|
+
const decl = child.namedChild(j);
|
|
716
|
+
if (decl.type === 'variable_declarator') {
|
|
717
|
+
const nameNode = decl.childForFieldName('name');
|
|
718
|
+
if (nameNode && fieldTypeText) {
|
|
719
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
720
|
+
members.push({
|
|
721
|
+
name: nameNode.text,
|
|
722
|
+
startLine,
|
|
723
|
+
endLine,
|
|
724
|
+
memberType: 'field',
|
|
725
|
+
fieldType: fieldTypeText
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
707
731
|
}
|
|
708
732
|
|
|
709
733
|
return members;
|
|
@@ -899,6 +923,135 @@ function findCallsInCode(code, parser) {
|
|
|
899
923
|
return undefined;
|
|
900
924
|
};
|
|
901
925
|
|
|
926
|
+
// Variable receiving this call's result (fix #207 return-type flow):
|
|
927
|
+
// `var x = find();` / `x = find();` → 'x'. Declared-type locals are
|
|
928
|
+
// already typed directly above — this covers `var` and reassignment,
|
|
929
|
+
// letting findCallers type x from the producer's declared return type.
|
|
930
|
+
const assignmentTargetOf = (callNode) => {
|
|
931
|
+
const p = callNode.parent;
|
|
932
|
+
if (p?.type === 'variable_declarator') {
|
|
933
|
+
const value = p.childForFieldName('value');
|
|
934
|
+
const nameNode = p.childForFieldName('name');
|
|
935
|
+
if (value && value.id === callNode.id && nameNode?.type === 'identifier') return nameNode.text;
|
|
936
|
+
}
|
|
937
|
+
if (p?.type === 'assignment_expression') {
|
|
938
|
+
const right = p.childForFieldName('right');
|
|
939
|
+
const left = p.childForFieldName('left');
|
|
940
|
+
if (right && right.id === callNode.id && left?.type === 'identifier') return left.text;
|
|
941
|
+
}
|
|
942
|
+
return undefined;
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// All names declared anywhere in a function body (locals, for/catch/lambda
|
|
946
|
+
// params). Guard for fix #202: a bare identifier receiver is only treated
|
|
947
|
+
// as an implicit-this field when NO local of that name is declared —
|
|
948
|
+
// mistyping a shadowed local could wrongly exclude a true caller.
|
|
949
|
+
const scopeDeclared = new Map();
|
|
950
|
+
const collectDeclaredNames = (fnNode) => {
|
|
951
|
+
const declared = new Set();
|
|
952
|
+
const walk = (n) => {
|
|
953
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
954
|
+
const c = n.namedChild(i);
|
|
955
|
+
if (c.type === 'variable_declarator' ||
|
|
956
|
+
c.type === 'enhanced_for_statement' ||
|
|
957
|
+
c.type === 'catch_formal_parameter') {
|
|
958
|
+
const nn = c.childForFieldName('name');
|
|
959
|
+
if (nn) declared.add(nn.text);
|
|
960
|
+
} else if (c.type === 'lambda_expression') {
|
|
961
|
+
const params = c.childForFieldName('parameters');
|
|
962
|
+
if (params?.type === 'identifier') declared.add(params.text);
|
|
963
|
+
else if (params) {
|
|
964
|
+
for (let j = 0; j < params.namedChildCount; j++) {
|
|
965
|
+
const pc = params.namedChild(j);
|
|
966
|
+
if (pc.type === 'identifier') declared.add(pc.text);
|
|
967
|
+
else {
|
|
968
|
+
const pn = pc.childForFieldName('name');
|
|
969
|
+
if (pn) declared.add(pn.text);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
walk(c);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
walk(fnNode);
|
|
978
|
+
return declared;
|
|
979
|
+
};
|
|
980
|
+
const isDeclaredLocal = (varName) => {
|
|
981
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
982
|
+
const declared = scopeDeclared.get(functionStack[i].startLine);
|
|
983
|
+
if (declared?.has(varName)) return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// Nearest enclosing class/interface/enum/record name (for implicit-this fields)
|
|
989
|
+
const findEnclosingClassName = (n) => {
|
|
990
|
+
for (let p = n.parent; p; p = p.parent) {
|
|
991
|
+
if (p.type === 'class_declaration' || p.type === 'interface_declaration' ||
|
|
992
|
+
p.type === 'enum_declaration' || p.type === 'record_declaration') {
|
|
993
|
+
return p.childForFieldName('name')?.text;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return undefined;
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// Call-site argument shape: count + per-arg static kind. Kinds feed the
|
|
1000
|
+
// overload discipline in findCallers (Java is the only supported language
|
|
1001
|
+
// with arity/type overloading): literal kinds can prove a call binds a
|
|
1002
|
+
// DIFFERENT same-class overload than the pinned one. Unknown args are
|
|
1003
|
+
// 'expr' — never evidence.
|
|
1004
|
+
const bareTypeName = (text) => {
|
|
1005
|
+
let t = text;
|
|
1006
|
+
const g = t.indexOf('<');
|
|
1007
|
+
if (g > 0) t = t.substring(0, g);
|
|
1008
|
+
const d = t.lastIndexOf('.');
|
|
1009
|
+
if (d >= 0) t = t.substring(d + 1);
|
|
1010
|
+
return t.trim();
|
|
1011
|
+
};
|
|
1012
|
+
const argKindOf = (arg) => {
|
|
1013
|
+
switch (arg.type) {
|
|
1014
|
+
case 'string_literal': return 'string';
|
|
1015
|
+
case 'character_literal': return 'char';
|
|
1016
|
+
case 'decimal_integer_literal':
|
|
1017
|
+
case 'hex_integer_literal':
|
|
1018
|
+
case 'octal_integer_literal':
|
|
1019
|
+
case 'binary_integer_literal':
|
|
1020
|
+
return /[lL]$/.test(arg.text) ? 'long' : 'int';
|
|
1021
|
+
case 'decimal_floating_point_literal':
|
|
1022
|
+
case 'hex_floating_point_literal':
|
|
1023
|
+
return /[fF]$/.test(arg.text) ? 'float' : 'double';
|
|
1024
|
+
case 'true':
|
|
1025
|
+
case 'false': return 'boolean';
|
|
1026
|
+
case 'null_literal': return 'null';
|
|
1027
|
+
case 'object_creation_expression': {
|
|
1028
|
+
const tn = arg.childForFieldName('type');
|
|
1029
|
+
return tn ? `new:${bareTypeName(tn.text)}` : 'expr';
|
|
1030
|
+
}
|
|
1031
|
+
case 'cast_expression': {
|
|
1032
|
+
const tn = arg.childForFieldName('type');
|
|
1033
|
+
return tn ? `cast:${bareTypeName(tn.text)}` : 'expr';
|
|
1034
|
+
}
|
|
1035
|
+
case 'lambda_expression':
|
|
1036
|
+
case 'method_reference': return 'lambda';
|
|
1037
|
+
case 'unary_expression':
|
|
1038
|
+
// -1, -2.5 — numeric literal kinds survive negation
|
|
1039
|
+
return arg.namedChildCount === 1 ? argKindOf(arg.namedChild(0)) : 'expr';
|
|
1040
|
+
default: return 'expr';
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
const getCallArgs = (callNode) => {
|
|
1044
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1045
|
+
if (!argsNode) return { argCount: 0, argKinds: null };
|
|
1046
|
+
const kinds = [];
|
|
1047
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1048
|
+
const arg = argsNode.namedChild(i);
|
|
1049
|
+
if (arg.type === 'comment') continue;
|
|
1050
|
+
kinds.push(argKindOf(arg));
|
|
1051
|
+
}
|
|
1052
|
+
return { argCount: kinds.length, argKinds: kinds.some(k => k !== 'expr') ? kinds : null };
|
|
1053
|
+
};
|
|
1054
|
+
|
|
902
1055
|
traverseTree(tree.rootNode, (node) => {
|
|
903
1056
|
// Track function entry
|
|
904
1057
|
if (isFunctionNode(node)) {
|
|
@@ -909,6 +1062,7 @@ function findCallsInCode(code, parser) {
|
|
|
909
1062
|
};
|
|
910
1063
|
functionStack.push(entry);
|
|
911
1064
|
scopeTypes.set(entry.startLine, buildScopeTypeMap(node));
|
|
1065
|
+
scopeDeclared.set(entry.startLine, collectDeclaredNames(node));
|
|
912
1066
|
}
|
|
913
1067
|
|
|
914
1068
|
// Handle method invocations: foo(), obj.foo(), this.foo()
|
|
@@ -920,13 +1074,68 @@ function findCallsInCode(code, parser) {
|
|
|
920
1074
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
921
1075
|
const receiver = (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined;
|
|
922
1076
|
const receiverType = (receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined;
|
|
1077
|
+
// fix #202: one-hop declared-field receivers —
|
|
1078
|
+
// this.service.execute(), svc.client.run(), and bare
|
|
1079
|
+
// service.execute() where service is a class field (only when
|
|
1080
|
+
// no same-named local is declared anywhere in the method).
|
|
1081
|
+
let receiverRoot, receiverFieldName, receiverRootType;
|
|
1082
|
+
if (objNode && !receiverType) {
|
|
1083
|
+
if (objNode.type === 'field_access') {
|
|
1084
|
+
const rootNode = objNode.childForFieldName('object');
|
|
1085
|
+
const fldNode = objNode.childForFieldName('field');
|
|
1086
|
+
if (fldNode?.type === 'identifier' && rootNode) {
|
|
1087
|
+
if (rootNode.type === 'this') {
|
|
1088
|
+
receiverRoot = 'this';
|
|
1089
|
+
receiverFieldName = fldNode.text;
|
|
1090
|
+
receiverRootType = findEnclosingClassName(node);
|
|
1091
|
+
} else if (rootNode.type === 'identifier') {
|
|
1092
|
+
const rootType = getReceiverType(rootNode.text);
|
|
1093
|
+
if (rootType) {
|
|
1094
|
+
receiverRoot = rootNode.text;
|
|
1095
|
+
receiverFieldName = fldNode.text;
|
|
1096
|
+
receiverRootType = rootType;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} else if (objNode.type === 'identifier' && receiver &&
|
|
1101
|
+
!isDeclaredLocal(receiver)) {
|
|
1102
|
+
// Implicit-this field (or a class name — the field-type
|
|
1103
|
+
// hop in findCallers simply finds no field and no-ops).
|
|
1104
|
+
receiverRoot = 'this';
|
|
1105
|
+
receiverFieldName = receiver;
|
|
1106
|
+
receiverRootType = findEnclosingClassName(node);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Chained receiver (fix #220): the receiver IS a call —
|
|
1110
|
+
// getConfig().validate() — record the producer so findCallers
|
|
1111
|
+
// can type it from the declared return annotation.
|
|
1112
|
+
let receiverCall, receiverCallIsMethod;
|
|
1113
|
+
if (!receiver && !receiverFieldName && objNode?.type === 'method_invocation') {
|
|
1114
|
+
const prodName = objNode.childForFieldName('name');
|
|
1115
|
+
if (prodName) {
|
|
1116
|
+
receiverCall = prodName.text;
|
|
1117
|
+
if (objNode.childForFieldName('object')) receiverCallIsMethod = true;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
923
1120
|
const firstArg = getFirstStringArg(node);
|
|
1121
|
+
const callArgs = getCallArgs(node);
|
|
1122
|
+
const assignedTo = assignmentTargetOf(node);
|
|
924
1123
|
calls.push({
|
|
925
1124
|
name: nameNode.text,
|
|
926
|
-
line
|
|
1125
|
+
// Multi-line chains (builder.x()\n.y()) must report each
|
|
1126
|
+
// method's OWN name line, not the chain-start line — the
|
|
1127
|
+
// account's ground set is keyed by the name's line
|
|
1128
|
+
line: nameNode.startPosition.row + 1,
|
|
927
1129
|
isMethod: !!objNode,
|
|
928
1130
|
receiver,
|
|
929
1131
|
...(receiverType && { receiverType }),
|
|
1132
|
+
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1133
|
+
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1134
|
+
...(receiverCall && { receiverCall }),
|
|
1135
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1136
|
+
argCount: callArgs.argCount,
|
|
1137
|
+
...(callArgs.argKinds && { argKinds: callArgs.argKinds }),
|
|
1138
|
+
...(assignedTo && { assignedTo }),
|
|
930
1139
|
enclosingFunction,
|
|
931
1140
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
932
1141
|
});
|
|
@@ -944,18 +1153,27 @@ function findCallsInCode(code, parser) {
|
|
|
944
1153
|
if (genericIdx > 0) {
|
|
945
1154
|
typeName = typeName.substring(0, genericIdx);
|
|
946
1155
|
}
|
|
947
|
-
// Handle qualified names like pkg.Class
|
|
1156
|
+
// Handle qualified names like pkg.Class — keep the qualifier
|
|
1157
|
+
// as receiver (fix #206): a qualified type must not resolve to
|
|
1158
|
+
// a same-file binding of an unrelated same-name symbol.
|
|
1159
|
+
let typeQualifier = null;
|
|
948
1160
|
const dotIdx = typeName.lastIndexOf('.');
|
|
949
1161
|
if (dotIdx > 0) {
|
|
1162
|
+
const qualParts = typeName.substring(0, dotIdx).split('.');
|
|
1163
|
+
typeQualifier = qualParts[qualParts.length - 1] || null;
|
|
950
1164
|
typeName = typeName.substring(dotIdx + 1);
|
|
951
1165
|
}
|
|
952
1166
|
|
|
953
1167
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1168
|
+
const ctorArgs = getCallArgs(node);
|
|
954
1169
|
calls.push({
|
|
955
1170
|
name: typeName,
|
|
956
1171
|
line: node.startPosition.row + 1,
|
|
957
1172
|
isMethod: false,
|
|
958
1173
|
isConstructor: true,
|
|
1174
|
+
...(typeQualifier && { receiver: typeQualifier }),
|
|
1175
|
+
argCount: ctorArgs.argCount,
|
|
1176
|
+
...(ctorArgs.argKinds && { argKinds: ctorArgs.argKinds }),
|
|
959
1177
|
enclosingFunction
|
|
960
1178
|
});
|
|
961
1179
|
}
|
|
@@ -984,22 +1202,29 @@ function findCallsInCode(code, parser) {
|
|
|
984
1202
|
return true;
|
|
985
1203
|
}
|
|
986
1204
|
|
|
987
|
-
// Track local variable types from
|
|
988
|
-
//
|
|
1205
|
+
// Track local variable types from declarations (fix #207 extends #202-era
|
|
1206
|
+
// new-Type() inference): the DECLARED type is compiler-checked evidence —
|
|
1207
|
+
// `Service s = lookup();` types s as Service regardless of the value
|
|
1208
|
+
// expression. `var` declarations fall back to new Type() value inference.
|
|
989
1209
|
if (node.type === 'local_variable_declaration' && functionStack.length > 0) {
|
|
1210
|
+
const declTypeNode = node.childForFieldName('type');
|
|
1211
|
+
const declaredType = declTypeNode && declTypeNode.text !== 'var'
|
|
1212
|
+
? extractTypeName(declTypeNode) : null;
|
|
990
1213
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
991
1214
|
const child = node.namedChild(i);
|
|
992
1215
|
if (child.type === 'variable_declarator') {
|
|
993
1216
|
const nameNode = child.childForFieldName('name');
|
|
994
1217
|
const valueNode = child.childForFieldName('value');
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1218
|
+
// new Type() is the DYNAMIC type — more precise than the
|
|
1219
|
+
// declared static type (Foo f = new Bar() dispatches to Bar)
|
|
1220
|
+
let typeName = valueNode?.type === 'object_creation_expression'
|
|
1221
|
+
? extractTypeName(valueNode.childForFieldName('type'))
|
|
1222
|
+
: null;
|
|
1223
|
+
if (!typeName) typeName = declaredType;
|
|
1224
|
+
if (nameNode && typeName) {
|
|
1225
|
+
const scopeKey = functionStack[functionStack.length - 1].startLine;
|
|
1226
|
+
const typeMap = scopeTypes.get(scopeKey);
|
|
1227
|
+
if (typeMap) typeMap.set(nameNode.text, typeName);
|
|
1003
1228
|
}
|
|
1004
1229
|
}
|
|
1005
1230
|
}
|
|
@@ -1156,7 +1381,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1156
1381
|
const tree = parseTree(parser, code);
|
|
1157
1382
|
const usages = [];
|
|
1158
1383
|
|
|
1159
|
-
|
|
1384
|
+
visitNameNodes(tree, code, name, (node) => {
|
|
1160
1385
|
// Look for identifiers and type_identifiers with the matching name
|
|
1161
1386
|
// type_identifier is used in Java for type references: new ClassName(), extends ClassName, field types
|
|
1162
1387
|
if ((node.type !== 'identifier' && node.type !== 'type_identifier') || node.text !== name) {
|
|
@@ -1243,10 +1468,12 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1243
1468
|
usageType = 'call';
|
|
1244
1469
|
}
|
|
1245
1470
|
}
|
|
1246
|
-
//
|
|
1471
|
+
// Object position of a method call: x.method() — x is a receiver
|
|
1472
|
+
// (variable or ClassName), referenced, not called. The call belongs
|
|
1473
|
+
// to the name field, handled above.
|
|
1247
1474
|
else if (parent.type === 'method_invocation' &&
|
|
1248
1475
|
parent.childForFieldName('object') === node) {
|
|
1249
|
-
usageType = '
|
|
1476
|
+
usageType = 'reference';
|
|
1250
1477
|
}
|
|
1251
1478
|
// Field access: obj.field
|
|
1252
1479
|
else if (parent.type === 'field_access' &&
|