ucn 4.1.3 → 4.2.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 +80 -353
- package/.claude/skills/ucn/references/commands.md +93 -0
- package/.claude/skills/ucn/references/trust-contract.md +63 -0
- package/README.md +67 -44
- package/cli/index.js +24 -19
- package/core/account.js +18 -0
- package/core/analysis.js +27 -7
- package/core/bridge.js +0 -1
- package/core/brief.js +2 -3
- package/core/build-worker.js +5 -0
- package/core/cache.js +51 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +1 -2
- package/core/entrypoints.js +32 -27
- package/core/execute.js +19 -3
- package/core/graph-build.js +33 -0
- package/core/output/analysis.js +53 -14
- package/core/output/check.js +11 -0
- package/core/output/doctor.js +40 -19
- package/core/output/endpoints.js +0 -2
- package/core/output/search.js +26 -1
- package/core/parser.js +1 -1
- package/core/project.js +25 -4
- package/core/registry.js +2 -2
- package/core/reporting.js +166 -108
- package/core/search.js +271 -49
- package/core/tracing.js +2 -2
- package/core/trust-matrix.js +158 -0
- package/core/verify.js +0 -1
- package/eslint.config.js +2 -2
- package/languages/go.js +143 -13
- package/languages/html.js +5 -0
- package/languages/index.js +5 -0
- package/languages/java.js +61 -4
- package/languages/javascript.js +233 -42
- package/languages/python.js +54 -15
- package/languages/rust.js +180 -22
- package/languages/utils.js +0 -1
- package/mcp/server.js +118 -38
- package/package.json +11 -4
package/languages/go.js
CHANGED
|
@@ -31,6 +31,14 @@ function extractReturnType(node) {
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Compiler-declared result of a returned function type, if any. */
|
|
35
|
+
function extractReturnedFunctionResult(node) {
|
|
36
|
+
const resultNode = node.childForFieldName('result');
|
|
37
|
+
if (resultNode?.type !== 'function_type') return null;
|
|
38
|
+
const innerResult = resultNode.childForFieldName('result');
|
|
39
|
+
return innerResult?.text.trim() || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
/**
|
|
35
43
|
* Extract Go parameters
|
|
36
44
|
*/
|
|
@@ -70,6 +78,46 @@ function extractReceiver(receiverNode) {
|
|
|
70
78
|
* Returns true if node was matched, false otherwise
|
|
71
79
|
*/
|
|
72
80
|
function _processFunction(node, functions, processedRanges, lines) {
|
|
81
|
+
// Package-level function-valued variables are callable symbols with a
|
|
82
|
+
// compiler-declared signature (`var DefaultLogger func(next
|
|
83
|
+
// http.Handler) http.Handler`). Indexing them as functions lets return
|
|
84
|
+
// flow type the result and prevents downstream interface calls from
|
|
85
|
+
// falling into global same-name guesses. Local function variables remain
|
|
86
|
+
// lexical values and are handled by call-site shadowing instead.
|
|
87
|
+
if (node.type === 'var_spec') {
|
|
88
|
+
const container = node.parent?.type === 'var_spec_list' ? node.parent?.parent : node.parent;
|
|
89
|
+
const isPackageLevel = container?.type === 'var_declaration' &&
|
|
90
|
+
container.parent?.type === 'source_file';
|
|
91
|
+
const typeNode = node.childForFieldName('type');
|
|
92
|
+
if (!isPackageLevel || typeNode?.type !== 'function_type') return false;
|
|
93
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
94
|
+
if (processedRanges.has(rangeKey)) return false;
|
|
95
|
+
processedRanges.add(rangeKey);
|
|
96
|
+
const paramsNode = typeNode.childForFieldName('parameters');
|
|
97
|
+
const resultNode = typeNode.childForFieldName('result');
|
|
98
|
+
const names = [];
|
|
99
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
100
|
+
const child = node.namedChild(i);
|
|
101
|
+
if (child.type === 'identifier') names.push(child);
|
|
102
|
+
}
|
|
103
|
+
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
104
|
+
for (const nameNode of names) {
|
|
105
|
+
const isExported = /^[A-Z]/.test(nameNode.text);
|
|
106
|
+
functions.push({
|
|
107
|
+
name: nameNode.text,
|
|
108
|
+
params: extractGoParams(paramsNode),
|
|
109
|
+
paramsStructured: parseStructuredParams(paramsNode, 'go'),
|
|
110
|
+
startLine,
|
|
111
|
+
endLine,
|
|
112
|
+
indent,
|
|
113
|
+
modifiers: isExported ? ['export'] : [],
|
|
114
|
+
isFunctionVariable: true,
|
|
115
|
+
...(resultNode?.text.trim() && { returnType: resultNode.text.trim() }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
if (node.type === 'function_declaration') {
|
|
74
122
|
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
75
123
|
if (processedRanges.has(rangeKey)) return false;
|
|
@@ -81,6 +129,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
81
129
|
if (nameNode) {
|
|
82
130
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
83
131
|
const returnType = extractReturnType(node);
|
|
132
|
+
const returnedFunctionResult = extractReturnedFunctionResult(node);
|
|
84
133
|
const docstring = extractGoDocstring(lines, startLine);
|
|
85
134
|
const typeParams = extractTypeParams(node);
|
|
86
135
|
const isExported = /^[A-Z]/.test(nameNode.text);
|
|
@@ -94,6 +143,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
94
143
|
indent,
|
|
95
144
|
modifiers: isExported ? ['export'] : [],
|
|
96
145
|
...(returnType && { returnType }),
|
|
146
|
+
...(returnedFunctionResult && { returnedFunctionResult }),
|
|
97
147
|
...(docstring && { docstring }),
|
|
98
148
|
...(typeParams && { generics: typeParams })
|
|
99
149
|
});
|
|
@@ -114,6 +164,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
114
164
|
const { startLine, endLine, indent } = nodeToLocation(node, lines);
|
|
115
165
|
const receiver = extractReceiver(receiverNode);
|
|
116
166
|
const returnType = extractReturnType(node);
|
|
167
|
+
const returnedFunctionResult = extractReturnedFunctionResult(node);
|
|
117
168
|
const docstring = extractGoDocstring(lines, startLine);
|
|
118
169
|
const isExported = /^[A-Z]/.test(nameNode.text);
|
|
119
170
|
|
|
@@ -128,6 +179,7 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
128
179
|
receiver,
|
|
129
180
|
modifiers: isExported ? ['export'] : [],
|
|
130
181
|
...(returnType && { returnType }),
|
|
182
|
+
...(returnedFunctionResult && { returnedFunctionResult }),
|
|
131
183
|
...(docstring && { docstring })
|
|
132
184
|
});
|
|
133
185
|
}
|
|
@@ -567,6 +619,7 @@ function parse(code, parser) {
|
|
|
567
619
|
functions,
|
|
568
620
|
classes,
|
|
569
621
|
stateObjects,
|
|
622
|
+
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
570
623
|
imports: [],
|
|
571
624
|
exports: []
|
|
572
625
|
};
|
|
@@ -656,7 +709,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
656
709
|
if (!argsNode) return null;
|
|
657
710
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
658
711
|
const arg = argsNode.namedChild(i);
|
|
659
|
-
if (arg.type
|
|
712
|
+
if (arg.type.endsWith('comment')) continue;
|
|
660
713
|
// fmt.Sprintf interpolation
|
|
661
714
|
if (arg.type === 'call_expression') {
|
|
662
715
|
const inner = arg.childForFieldName('function');
|
|
@@ -678,6 +731,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
678
731
|
const closureScopes = new Map();
|
|
679
732
|
// Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
|
|
680
733
|
const scopeTypes = new Map();
|
|
734
|
+
// Package qualifier paired with an inferred/declared receiver type.
|
|
735
|
+
const scopeTypeQualifiers = new Map();
|
|
681
736
|
// Names whose scope type is a New*-prefix GUESS (fix #266) — per scope
|
|
682
737
|
const scopeGuesses = new Map();
|
|
683
738
|
// Track function-typed parameter names per scope (scopeStartLine -> Set<name>)
|
|
@@ -717,11 +772,31 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
717
772
|
return null;
|
|
718
773
|
};
|
|
719
774
|
|
|
775
|
+
// Preserve the package qualifier separately from the bare type name.
|
|
776
|
+
// `http.Handler` and a project-local `Handler` are different identities;
|
|
777
|
+
// dropping `http.` lets an unrelated same-named project type steal method
|
|
778
|
+
// dispatch. Pointer wrappers do not change that provenance.
|
|
779
|
+
const extractTypeQualifier = (typeNode) => {
|
|
780
|
+
if (!typeNode) return null;
|
|
781
|
+
if (typeNode.type === 'pointer_type') {
|
|
782
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
783
|
+
const q = extractTypeQualifier(typeNode.namedChild(i));
|
|
784
|
+
if (q) return q;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (typeNode.type === 'qualified_type') {
|
|
788
|
+
const pkg = typeNode.childForFieldName('package') || typeNode.namedChild(0);
|
|
789
|
+
return pkg?.text || null;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
};
|
|
793
|
+
|
|
720
794
|
// Build type map from function/method parameters and receiver.
|
|
721
795
|
// Also returns funcParamNames: parameter names with function types (func(...) ...)
|
|
722
796
|
// so calls to them can be skipped (they're local parameter calls, not global function calls).
|
|
723
797
|
const buildScopeTypeMap = (node) => {
|
|
724
798
|
const typeMap = new Map();
|
|
799
|
+
const typeQualifierMap = new Map();
|
|
725
800
|
const funcParamNames = new Set();
|
|
726
801
|
|
|
727
802
|
// Method receiver: func (f *Framework) Method()
|
|
@@ -736,6 +811,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
736
811
|
const typeName = extractTypeName(typeNode);
|
|
737
812
|
if (nameNode && typeName) {
|
|
738
813
|
typeMap.set(nameNode.text, typeName);
|
|
814
|
+
const qualifier = extractTypeQualifier(typeNode);
|
|
815
|
+
if (qualifier) typeQualifierMap.set(nameNode.text, qualifier);
|
|
739
816
|
}
|
|
740
817
|
}
|
|
741
818
|
}
|
|
@@ -764,14 +841,18 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
764
841
|
} else {
|
|
765
842
|
const typeName = extractTypeName(typeNode);
|
|
766
843
|
if (typeName) {
|
|
767
|
-
|
|
844
|
+
const qualifier = extractTypeQualifier(typeNode);
|
|
845
|
+
for (const nn of nameNodes) {
|
|
846
|
+
typeMap.set(nn.text, typeName);
|
|
847
|
+
if (qualifier) typeQualifierMap.set(nn.text, qualifier);
|
|
848
|
+
}
|
|
768
849
|
}
|
|
769
850
|
}
|
|
770
851
|
}
|
|
771
852
|
}
|
|
772
853
|
}
|
|
773
854
|
|
|
774
|
-
return { typeMap, funcParamNames };
|
|
855
|
+
return { typeMap, typeQualifierMap, funcParamNames };
|
|
775
856
|
};
|
|
776
857
|
|
|
777
858
|
// Helper to extract function name from a function node
|
|
@@ -824,6 +905,13 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
824
905
|
}
|
|
825
906
|
return undefined;
|
|
826
907
|
};
|
|
908
|
+
const getReceiverTypeQualifier = (varName) => {
|
|
909
|
+
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
910
|
+
const qualifiers = scopeTypeQualifiers.get(functionStack[i].startLine);
|
|
911
|
+
if (qualifiers?.has(varName)) return qualifiers.get(varName);
|
|
912
|
+
}
|
|
913
|
+
return undefined;
|
|
914
|
+
};
|
|
827
915
|
// Is the FIRST scope-chain hit for this variable a New*-prefix GUESS
|
|
828
916
|
// (fix #266, viper-measured)? `registry := NewCodecRegistry()` types
|
|
829
917
|
// registry as 'CodecRegistry' by NAME CONVENTION — the actual return
|
|
@@ -947,8 +1035,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
947
1035
|
endLine: node.endPosition.row + 1
|
|
948
1036
|
};
|
|
949
1037
|
functionStack.push(entry);
|
|
950
|
-
const { typeMap, funcParamNames } = buildScopeTypeMap(node);
|
|
1038
|
+
const { typeMap, typeQualifierMap, funcParamNames } = buildScopeTypeMap(node);
|
|
951
1039
|
scopeTypes.set(entry.startLine, typeMap);
|
|
1040
|
+
scopeTypeQualifiers.set(entry.startLine, typeQualifierMap);
|
|
952
1041
|
if (funcParamNames.size > 0) {
|
|
953
1042
|
funcParamScopes.set(entry.startLine, funcParamNames);
|
|
954
1043
|
}
|
|
@@ -974,15 +1063,20 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
974
1063
|
for (let vi = 0; vi < Math.min(names.length, values.length); vi++) {
|
|
975
1064
|
const val = values[vi];
|
|
976
1065
|
let typeName = null;
|
|
1066
|
+
let typeQualifier = null;
|
|
977
1067
|
let typeGuessed = false;
|
|
978
1068
|
// &Type{...} or Type{...}
|
|
979
1069
|
if (val.type === 'composite_literal') {
|
|
980
|
-
|
|
1070
|
+
const typeNode = val.childForFieldName('type');
|
|
1071
|
+
typeName = extractTypeName(typeNode);
|
|
1072
|
+
typeQualifier = extractTypeQualifier(typeNode);
|
|
981
1073
|
} else if (val.type === 'unary_expression' && val.childCount > 0) {
|
|
982
1074
|
for (let ci = 0; ci < val.namedChildCount; ci++) {
|
|
983
1075
|
const ch = val.namedChild(ci);
|
|
984
1076
|
if (ch.type === 'composite_literal') {
|
|
985
|
-
|
|
1077
|
+
const typeNode = ch.childForFieldName('type');
|
|
1078
|
+
typeName = extractTypeName(typeNode);
|
|
1079
|
+
typeQualifier = extractTypeQualifier(typeNode);
|
|
986
1080
|
break;
|
|
987
1081
|
}
|
|
988
1082
|
}
|
|
@@ -1020,12 +1114,20 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1020
1114
|
: argNode.type === 'selector_expression'
|
|
1021
1115
|
? argNode.childForFieldName('field')?.text || null
|
|
1022
1116
|
: extractTypeName(argNode);
|
|
1117
|
+
if (argNode.type === 'selector_expression') {
|
|
1118
|
+
typeQualifier = argNode.childForFieldName('operand')?.text || null;
|
|
1119
|
+
} else {
|
|
1120
|
+
typeQualifier = extractTypeQualifier(argNode);
|
|
1121
|
+
}
|
|
1023
1122
|
}
|
|
1024
1123
|
}
|
|
1025
1124
|
}
|
|
1026
1125
|
}
|
|
1027
1126
|
if (typeName) {
|
|
1028
1127
|
typeMap.set(names[vi], typeName);
|
|
1128
|
+
const qualifiers = scopeTypeQualifiers.get(scopeKey);
|
|
1129
|
+
if (typeQualifier) qualifiers?.set(names[vi], typeQualifier);
|
|
1130
|
+
else qualifiers?.delete(names[vi]);
|
|
1029
1131
|
let gset = scopeGuesses.get(scopeKey);
|
|
1030
1132
|
if (typeGuessed) {
|
|
1031
1133
|
if (!gset) { gset = new Set(); scopeGuesses.set(scopeKey, gset); }
|
|
@@ -1052,10 +1154,14 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1052
1154
|
if (spec.type !== 'var_spec') return;
|
|
1053
1155
|
const typeName = extractTypeName(spec.childForFieldName('type'));
|
|
1054
1156
|
if (!typeName) return;
|
|
1157
|
+
const qualifier = extractTypeQualifier(spec.childForFieldName('type'));
|
|
1055
1158
|
for (let j = 0; j < spec.namedChildCount; j++) {
|
|
1056
1159
|
const id = spec.namedChild(j);
|
|
1057
1160
|
if (id.type === 'identifier') {
|
|
1058
1161
|
varTypeMap.set(id.text, typeName);
|
|
1162
|
+
const qualifiers = scopeTypeQualifiers.get(scopeKey);
|
|
1163
|
+
if (qualifier) qualifiers?.set(id.text, qualifier);
|
|
1164
|
+
else qualifiers?.delete(id.text);
|
|
1059
1165
|
scopeGuesses.get(scopeKey)?.delete(id.text); // declared type clears any guess
|
|
1060
1166
|
}
|
|
1061
1167
|
}
|
|
@@ -1134,7 +1240,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1134
1240
|
let argSpread = false;
|
|
1135
1241
|
if (argsNode) {
|
|
1136
1242
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1137
|
-
if (argsNode.namedChild(i).type
|
|
1243
|
+
if (argsNode.namedChild(i).type.endsWith('comment')) continue;
|
|
1138
1244
|
argCount++;
|
|
1139
1245
|
}
|
|
1140
1246
|
for (let i = 0; i < argsNode.childCount; i++) {
|
|
@@ -1183,20 +1289,32 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1183
1289
|
// If receiver is a known import alias, this is a package call, not a method call
|
|
1184
1290
|
const isPkgCall = receiver && importAliases.has(receiver);
|
|
1185
1291
|
const receiverType = (!isPkgCall && receiver) ? getReceiverType(receiver) : undefined;
|
|
1292
|
+
const receiverTypeQualifier = receiverType
|
|
1293
|
+
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1186
1294
|
// fix #202: one-hop declared-field receivers — h.inner.Run().
|
|
1187
1295
|
// receiverRoot/Field/RootType let findCallers hop to the
|
|
1188
1296
|
// field's declared struct-field type cross-file.
|
|
1189
|
-
let receiverRoot, receiverFieldName, receiverRootType,
|
|
1297
|
+
let receiverRoot, receiverFieldName, receiverRootType,
|
|
1298
|
+
receiverRootTypeQualifier, receiverRootTypeGuessed,
|
|
1299
|
+
receiverRootIsModule;
|
|
1190
1300
|
if (!receiver && operandNode?.type === 'selector_expression') {
|
|
1191
1301
|
const rootNode = operandNode.childForFieldName('operand');
|
|
1192
1302
|
const fldNode = operandNode.childForFieldName('field');
|
|
1193
|
-
if (rootNode?.type === 'identifier' && fldNode
|
|
1194
|
-
!importAliases.has(rootNode.text)) {
|
|
1303
|
+
if (rootNode?.type === 'identifier' && fldNode) {
|
|
1195
1304
|
receiverRoot = rootNode.text;
|
|
1196
1305
|
receiverFieldName = fldNode.text;
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1306
|
+
if (importAliases.has(rootNode.text)) {
|
|
1307
|
+
// Package-owned value receiver:
|
|
1308
|
+
// io.Discard.Write(...). The package qualifier
|
|
1309
|
+
// is identity evidence even though the value's
|
|
1310
|
+
// concrete type lives outside the project.
|
|
1311
|
+
receiverRootIsModule = true;
|
|
1312
|
+
} else {
|
|
1313
|
+
receiverRootType = getReceiverType(rootNode.text);
|
|
1314
|
+
receiverRootTypeQualifier = getReceiverTypeQualifier(rootNode.text);
|
|
1315
|
+
if (receiverRootType && isGuessedType(rootNode.text)) {
|
|
1316
|
+
receiverRootTypeGuessed = true;
|
|
1317
|
+
}
|
|
1200
1318
|
}
|
|
1201
1319
|
}
|
|
1202
1320
|
}
|
|
@@ -1243,9 +1361,12 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1243
1361
|
isMethod: !isPkgCall,
|
|
1244
1362
|
receiver,
|
|
1245
1363
|
...(receiverType && { receiverType }),
|
|
1364
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1246
1365
|
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1247
1366
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1367
|
+
...(receiverRootIsModule && { receiverRootIsModule: true }),
|
|
1248
1368
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1369
|
+
...(receiverFieldName && receiverRootTypeQualifier && { receiverRootTypeQualifier }),
|
|
1249
1370
|
...(receiverFieldName && receiverRootType && receiverRootTypeGuessed && { receiverRootTypeGuessed: true }),
|
|
1250
1371
|
...(receiverCall && { receiverCall }),
|
|
1251
1372
|
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
@@ -1323,6 +1444,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1323
1444
|
if (fieldNode && operandNode) {
|
|
1324
1445
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
1325
1446
|
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
1447
|
+
const receiverTypeQualifier = receiverType
|
|
1448
|
+
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1326
1449
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1327
1450
|
calls.push({
|
|
1328
1451
|
name: fieldNode.text,
|
|
@@ -1330,6 +1453,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1330
1453
|
isMethod: true,
|
|
1331
1454
|
receiver,
|
|
1332
1455
|
...(receiverType && { receiverType }),
|
|
1456
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1333
1457
|
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1334
1458
|
enclosingFunction,
|
|
1335
1459
|
isPotentialCallback: true,
|
|
@@ -1379,6 +1503,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1379
1503
|
if (fieldNode && operandNode) {
|
|
1380
1504
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
1381
1505
|
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
1506
|
+
const receiverTypeQualifier = receiverType
|
|
1507
|
+
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1382
1508
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1383
1509
|
calls.push({
|
|
1384
1510
|
name: fieldNode.text,
|
|
@@ -1386,6 +1512,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1386
1512
|
isMethod: true,
|
|
1387
1513
|
receiver,
|
|
1388
1514
|
...(receiverType && { receiverType }),
|
|
1515
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1389
1516
|
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1390
1517
|
enclosingFunction,
|
|
1391
1518
|
isPotentialCallback: true,
|
|
@@ -1474,6 +1601,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1474
1601
|
if (fieldNode && operandNode) {
|
|
1475
1602
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
1476
1603
|
const receiverType = receiver ? getReceiverType(receiver) : undefined;
|
|
1604
|
+
const receiverTypeQualifier = receiverType
|
|
1605
|
+
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1477
1606
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
1478
1607
|
calls.push({
|
|
1479
1608
|
name: fieldNode.text,
|
|
@@ -1481,6 +1610,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1481
1610
|
isMethod: true,
|
|
1482
1611
|
receiver,
|
|
1483
1612
|
...(receiverType && { receiverType }),
|
|
1613
|
+
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
1484
1614
|
...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
|
|
1485
1615
|
enclosingFunction,
|
|
1486
1616
|
isPotentialCallback: true,
|
package/languages/html.js
CHANGED
|
@@ -224,6 +224,9 @@ function extractEventHandlerCalls(htmlContent, htmlParser) {
|
|
|
224
224
|
// ── Exported language module interface ──────────────────────────────────────
|
|
225
225
|
|
|
226
226
|
function parse(code, parser) {
|
|
227
|
+
const { safeParse, getParseOptions } = require('./index');
|
|
228
|
+
const htmlTree = safeParse(parser, code, undefined, getParseOptions(code.length));
|
|
229
|
+
const htmlRecovery = !!htmlTree.rootNode.hasError;
|
|
227
230
|
const result = extractJS(code, parser);
|
|
228
231
|
if (!result) {
|
|
229
232
|
return {
|
|
@@ -232,6 +235,7 @@ function parse(code, parser) {
|
|
|
232
235
|
functions: [],
|
|
233
236
|
classes: [],
|
|
234
237
|
stateObjects: [],
|
|
238
|
+
...(htmlRecovery && { parseRecovery: true }),
|
|
235
239
|
imports: [],
|
|
236
240
|
exports: []
|
|
237
241
|
};
|
|
@@ -240,6 +244,7 @@ function parse(code, parser) {
|
|
|
240
244
|
const jsResult = result.jsModule.parse(result.virtualJS, result.jsParser);
|
|
241
245
|
jsResult.language = 'html';
|
|
242
246
|
jsResult.totalLines = code.split('\n').length;
|
|
247
|
+
if (htmlRecovery) jsResult.parseRecovery = true;
|
|
243
248
|
return jsResult;
|
|
244
249
|
}
|
|
245
250
|
|
package/languages/index.js
CHANGED
|
@@ -50,6 +50,9 @@ const STRUCTURAL_TRAITS = {
|
|
|
50
50
|
// assignment rebinds functions onto objects (obj.print = print), so the
|
|
51
51
|
// #218b gate routes such calls visible instead of excluding (fix #220).
|
|
52
52
|
methodCallReachesFunctions: true,
|
|
53
|
+
// Whether `Type(...)` constructs a class without a `new` token. Python
|
|
54
|
+
// classes are ordinary callable objects; JS/TS classes require `new`.
|
|
55
|
+
classesCallableWithoutNew: false,
|
|
53
56
|
};
|
|
54
57
|
const NOMINAL_TRAITS = {
|
|
55
58
|
typeSystem: 'nominal',
|
|
@@ -113,6 +116,7 @@ const NOMINAL_TRAITS = {
|
|
|
113
116
|
// Go method values (obj.Method) and Java `::` references DO denote
|
|
114
117
|
// methods — false. Per-language override, not preset-wide.
|
|
115
118
|
memberAccessNeverMethod: false,
|
|
119
|
+
classesCallableWithoutNew: false,
|
|
116
120
|
};
|
|
117
121
|
|
|
118
122
|
// Language configurations
|
|
@@ -170,6 +174,7 @@ const LANGUAGES = {
|
|
|
170
174
|
// and query code treats such receivers as module receivers. JS
|
|
171
175
|
// from-imports bind values only (`import * as ns` is parser-marked).
|
|
172
176
|
submoduleImports: true,
|
|
177
|
+
classesCallableWithoutNew: true,
|
|
173
178
|
testFileCandidates: (base, ext) => [`test_${base}.py`, `${base}_test.py`],
|
|
174
179
|
testDirs: ['tests'],
|
|
175
180
|
},
|
package/languages/java.js
CHANGED
|
@@ -295,6 +295,7 @@ function _processFunction(node, functions, processedRanges, lines, code) {
|
|
|
295
295
|
isConstructor: true,
|
|
296
296
|
...(docstring && { docstring }),
|
|
297
297
|
...(annotations.length > 0 && { annotations }),
|
|
298
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
298
299
|
...(nameLine !== startLine && { nameLine })
|
|
299
300
|
});
|
|
300
301
|
}
|
|
@@ -423,7 +424,8 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
423
424
|
members: extractEnumConstants(node, lines),
|
|
424
425
|
modifiers,
|
|
425
426
|
...(docstring && { docstring }),
|
|
426
|
-
...(annotations.length > 0 && { annotations })
|
|
427
|
+
...(annotations.length > 0 && { annotations }),
|
|
428
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs })
|
|
427
429
|
});
|
|
428
430
|
}
|
|
429
431
|
return true;
|
|
@@ -479,6 +481,7 @@ function _processClass(node, classes, processedRanges, lines, code) {
|
|
|
479
481
|
...(docstring && { docstring }),
|
|
480
482
|
...(generics && { generics }),
|
|
481
483
|
...(annotations.length > 0 && { annotations }),
|
|
484
|
+
...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
|
|
482
485
|
...(implementsInfo.length > 0 && { implements: implementsInfo })
|
|
483
486
|
});
|
|
484
487
|
}
|
|
@@ -843,6 +846,7 @@ function parse(code, parser) {
|
|
|
843
846
|
functions,
|
|
844
847
|
classes,
|
|
845
848
|
stateObjects,
|
|
849
|
+
...(tree.rootNode.hasError && { parseRecovery: true }),
|
|
846
850
|
imports: [],
|
|
847
851
|
exports: []
|
|
848
852
|
};
|
|
@@ -869,7 +873,7 @@ function findCallsInCode(code, parser) {
|
|
|
869
873
|
if (!argsNode) return null;
|
|
870
874
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
871
875
|
const arg = argsNode.namedChild(i);
|
|
872
|
-
if (arg.type
|
|
876
|
+
if (arg.type.endsWith('comment')) continue;
|
|
873
877
|
return _extractStringArg(arg);
|
|
874
878
|
}
|
|
875
879
|
return null;
|
|
@@ -979,6 +983,7 @@ function findCallsInCode(code, parser) {
|
|
|
979
983
|
// as an implicit-this field when NO local of that name is declared —
|
|
980
984
|
// mistyping a shadowed local could wrongly exclude a true caller.
|
|
981
985
|
const scopeDeclared = new Map();
|
|
986
|
+
const scopedTypeRestores = new Map();
|
|
982
987
|
const collectDeclaredNames = (fnNode) => {
|
|
983
988
|
const declared = new Set();
|
|
984
989
|
const walk = (n) => {
|
|
@@ -1064,6 +1069,27 @@ function findCallsInCode(code, parser) {
|
|
|
1064
1069
|
const tn = arg.childForFieldName('type');
|
|
1065
1070
|
return tn ? `cast:${bareTypeName(tn.text)}` : 'expr';
|
|
1066
1071
|
}
|
|
1072
|
+
case 'class_literal': {
|
|
1073
|
+
const tn = arg.namedChild(0);
|
|
1074
|
+
return tn ? `class:${bareTypeName(tn.text)}` : 'class:Object';
|
|
1075
|
+
}
|
|
1076
|
+
case 'identifier': {
|
|
1077
|
+
const typeName = getReceiverType(arg.text);
|
|
1078
|
+
return typeName ? `type:${bareTypeName(typeName)}` : 'expr';
|
|
1079
|
+
}
|
|
1080
|
+
case 'method_invocation': {
|
|
1081
|
+
// Preserve compiler-visible producer ownership for nested
|
|
1082
|
+
// static factories: build(CodeBlock.of(...)) can select a
|
|
1083
|
+
// CodeBlock overload without guessing from the method name.
|
|
1084
|
+
const ownerNode = arg.childForFieldName('object');
|
|
1085
|
+
const methodNode = arg.childForFieldName('name');
|
|
1086
|
+
if (ownerNode?.type === 'identifier' && methodNode) {
|
|
1087
|
+
const ownerType = getReceiverType(ownerNode.text) ||
|
|
1088
|
+
(/^[A-Z]/.test(ownerNode.text) ? ownerNode.text : null);
|
|
1089
|
+
if (ownerType) return `call:${bareTypeName(ownerType)}:${methodNode.text}`;
|
|
1090
|
+
}
|
|
1091
|
+
return 'expr';
|
|
1092
|
+
}
|
|
1067
1093
|
case 'lambda_expression':
|
|
1068
1094
|
case 'method_reference': return 'lambda';
|
|
1069
1095
|
case 'unary_expression':
|
|
@@ -1078,7 +1104,7 @@ function findCallsInCode(code, parser) {
|
|
|
1078
1104
|
const kinds = [];
|
|
1079
1105
|
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1080
1106
|
const arg = argsNode.namedChild(i);
|
|
1081
|
-
if (arg.type
|
|
1107
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1082
1108
|
kinds.push(argKindOf(arg));
|
|
1083
1109
|
}
|
|
1084
1110
|
return { argCount: kinds.length, argKinds: kinds.some(k => k !== 'expr') ? kinds : null };
|
|
@@ -1097,6 +1123,26 @@ function findCallsInCode(code, parser) {
|
|
|
1097
1123
|
scopeDeclared.set(entry.startLine, collectDeclaredNames(node));
|
|
1098
1124
|
}
|
|
1099
1125
|
|
|
1126
|
+
// Enhanced-for variables are compiler-typed locals whose scope is
|
|
1127
|
+
// exactly the loop body. Record the type before visiting the body and
|
|
1128
|
+
// restore any shadowed outer binding on leave.
|
|
1129
|
+
if (node.type === 'enhanced_for_statement' && functionStack.length > 0) {
|
|
1130
|
+
const typeNode = node.childForFieldName('type');
|
|
1131
|
+
const nameNode = node.childForFieldName('name');
|
|
1132
|
+
const typeName = extractTypeName(typeNode);
|
|
1133
|
+
const scopeKey = functionStack[functionStack.length - 1].startLine;
|
|
1134
|
+
const typeMap = scopeTypes.get(scopeKey);
|
|
1135
|
+
if (nameNode && typeName && typeMap) {
|
|
1136
|
+
scopedTypeRestores.set(node.id, {
|
|
1137
|
+
typeMap,
|
|
1138
|
+
name: nameNode.text,
|
|
1139
|
+
had: typeMap.has(nameNode.text),
|
|
1140
|
+
previous: typeMap.get(nameNode.text),
|
|
1141
|
+
});
|
|
1142
|
+
typeMap.set(nameNode.text, typeName);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1100
1146
|
// Handle method invocations: foo(), obj.foo(), this.foo()
|
|
1101
1147
|
if (node.type === 'method_invocation') {
|
|
1102
1148
|
const nameNode = node.childForFieldName('name');
|
|
@@ -1121,7 +1167,12 @@ function findCallsInCode(code, parser) {
|
|
|
1121
1167
|
receiverFieldName = fldNode.text;
|
|
1122
1168
|
receiverRootType = findEnclosingClassName(node);
|
|
1123
1169
|
} else if (rootNode.type === 'identifier') {
|
|
1124
|
-
|
|
1170
|
+
// `TypeName.CONSTANT.method()` is a one-hop
|
|
1171
|
+
// static-field receiver. The capitalized root
|
|
1172
|
+
// is type identity, then the field declaration
|
|
1173
|
+
// supplies the value's static type.
|
|
1174
|
+
const rootType = getReceiverType(rootNode.text) ||
|
|
1175
|
+
(/^[A-Z]/.test(rootNode.text) ? rootNode.text : undefined);
|
|
1125
1176
|
if (rootType) {
|
|
1126
1177
|
receiverRoot = rootNode.text;
|
|
1127
1178
|
receiverFieldName = fldNode.text;
|
|
@@ -1363,6 +1414,12 @@ function findCallsInCode(code, parser) {
|
|
|
1363
1414
|
return true;
|
|
1364
1415
|
}, {
|
|
1365
1416
|
onLeave: (node) => {
|
|
1417
|
+
const restore = scopedTypeRestores.get(node.id);
|
|
1418
|
+
if (restore) {
|
|
1419
|
+
if (restore.had) restore.typeMap.set(restore.name, restore.previous);
|
|
1420
|
+
else restore.typeMap.delete(restore.name);
|
|
1421
|
+
scopedTypeRestores.delete(node.id);
|
|
1422
|
+
}
|
|
1366
1423
|
if (isFunctionNode(node)) {
|
|
1367
1424
|
const leaving = functionStack.pop();
|
|
1368
1425
|
if (leaving) {
|