ucn 3.8.26 → 4.0.0
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 +28 -5
- 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 +3434 -158
- package/core/confidence.js +82 -19
- package/core/deadcode.js +114 -21
- package/core/execute.js +4 -0
- package/core/graph-build.js +44 -2
- package/core/imports.js +118 -1
- package/core/output/analysis.js +345 -83
- package/core/output/reporting.js +8 -2
- 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/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 +3 -3
- package/package.json +9 -3
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -79
package/languages/go.js
CHANGED
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
traverseTreeCached,
|
|
11
11
|
nodeToLocation,
|
|
12
12
|
parseStructuredParams,
|
|
13
|
-
extractGoDocstring
|
|
13
|
+
extractGoDocstring,
|
|
14
|
+
visitNameNodes,
|
|
14
15
|
} = require('./utils');
|
|
15
16
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
16
17
|
|
|
@@ -181,6 +182,28 @@ function _processClass(node, types, processedRanges, lines) {
|
|
|
181
182
|
...(embeddedBases.length > 0 && { extends: embeddedBases.join(', ') })
|
|
182
183
|
});
|
|
183
184
|
}
|
|
185
|
+
} else if (spec.type === 'type_alias') {
|
|
186
|
+
// `type A = B` — A IS B (compiler identity, methods carry over;
|
|
187
|
+
// unlike `type A B` defined types, which get NO methods from B).
|
|
188
|
+
// Record the aliased base so callers can treat A-qualified
|
|
189
|
+
// receivers as B (fix #208).
|
|
190
|
+
const nameNode = spec.childForFieldName('name');
|
|
191
|
+
const typeNode = spec.childForFieldName('type');
|
|
192
|
+
if (nameNode && typeNode) {
|
|
193
|
+
const aliasOf = typeNode.type === 'type_identifier' ? typeNode.text
|
|
194
|
+
: typeNode.type === 'qualified_type' ? typeNode.childForFieldName('name')?.text
|
|
195
|
+
: null;
|
|
196
|
+
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
197
|
+
types.push({
|
|
198
|
+
name: nameNode.text,
|
|
199
|
+
startLine,
|
|
200
|
+
endLine,
|
|
201
|
+
type: 'type',
|
|
202
|
+
members: [],
|
|
203
|
+
modifiers: /^[A-Z]/.test(nameNode.text) ? ['export'] : [],
|
|
204
|
+
...(aliasOf && { aliasOf }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
184
207
|
}
|
|
185
208
|
}
|
|
186
209
|
return true;
|
|
@@ -555,6 +578,63 @@ const GO_BUILTINS = new Set([
|
|
|
555
578
|
'println', 'real', 'recover'
|
|
556
579
|
]);
|
|
557
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Variable receiving this call's result (fix #207 return-type flow):
|
|
583
|
+
* bb := balancer.Get(n) → { assignedTo: 'bb' }
|
|
584
|
+
* x, err := pkg.Make() → { assignedTo: 'x', assignedTuple: true }
|
|
585
|
+
* (tuple unpack — the flow map pairs the first
|
|
586
|
+
* return element with the first variable)
|
|
587
|
+
* y = q() → { assignedTo: 'y' } (plain `=` only — `+=` etc.
|
|
588
|
+
* don't bind the call's type to the variable)
|
|
589
|
+
* a, b := g(), h() → parallel assignment: each call pairs with its
|
|
590
|
+
* own LHS position, single-value semantics
|
|
591
|
+
* Identifier targets only; blank (`_`) targets return undefined.
|
|
592
|
+
*/
|
|
593
|
+
function goAssignmentTargetOf(callNode) {
|
|
594
|
+
let n = callNode;
|
|
595
|
+
let p = n.parent;
|
|
596
|
+
let rhsIndex = 0;
|
|
597
|
+
let rhsCount = 1;
|
|
598
|
+
if (p && p.type === 'expression_list') {
|
|
599
|
+
rhsCount = p.namedChildCount;
|
|
600
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
601
|
+
if (p.namedChild(i).id === n.id) { rhsIndex = i; break; }
|
|
602
|
+
}
|
|
603
|
+
n = p; p = n.parent;
|
|
604
|
+
}
|
|
605
|
+
if (!p || (p.type !== 'short_var_declaration' && p.type !== 'assignment_statement')) return undefined;
|
|
606
|
+
if (p.type === 'assignment_statement') {
|
|
607
|
+
const op = p.childForFieldName('operator');
|
|
608
|
+
if (op && op.text !== '=') return undefined;
|
|
609
|
+
}
|
|
610
|
+
const right = p.childForFieldName('right');
|
|
611
|
+
if (!right || right.id !== n.id) return undefined;
|
|
612
|
+
const left = p.childForFieldName('left');
|
|
613
|
+
if (!left) return undefined;
|
|
614
|
+
const names = left.type === 'expression_list'
|
|
615
|
+
? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
|
|
616
|
+
: [left];
|
|
617
|
+
if (rhsCount > 1) {
|
|
618
|
+
const target = names[rhsIndex];
|
|
619
|
+
return target?.type === 'identifier' && target.text !== '_'
|
|
620
|
+
? { assignedTo: target.text } : undefined;
|
|
621
|
+
}
|
|
622
|
+
const target = names[0];
|
|
623
|
+
if (target?.type !== 'identifier' || target.text === '_') return undefined;
|
|
624
|
+
if (names.length > 1) {
|
|
625
|
+
// All LHS names (fix #220): an EXTERNAL producer decides every tuple
|
|
626
|
+
// element's type, not just the first — `tmpFile, err := os.CreateTemp`
|
|
627
|
+
// marks err external-flow too. The TYPED flow keeps pairing only
|
|
628
|
+
// element 0 with the producer's return tuple (#207).
|
|
629
|
+
const rest = names.slice(1)
|
|
630
|
+
.filter(t => t.type === 'identifier' && t.text !== '_')
|
|
631
|
+
.map(t => t.text);
|
|
632
|
+
return { assignedTo: target.text, assignedTuple: true,
|
|
633
|
+
...(rest.length > 0 && { assignedTupleRest: rest }) };
|
|
634
|
+
}
|
|
635
|
+
return { assignedTo: target.text };
|
|
636
|
+
}
|
|
637
|
+
|
|
558
638
|
function findCallsInCode(code, parser, options = {}) {
|
|
559
639
|
const tree = parseTree(parser, code);
|
|
560
640
|
const calls = [];
|
|
@@ -736,6 +816,104 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
736
816
|
return undefined;
|
|
737
817
|
};
|
|
738
818
|
|
|
819
|
+
// fix #203 (Go): is a bare-identifier function REFERENCE shadowed by an
|
|
820
|
+
// enclosing func-literal/function parameter, method receiver, range/init
|
|
821
|
+
// binding, or a := / var local declared before use? grpc-go-measured:
|
|
822
|
+
// `&clusterInfo{unsubscribe: unsubscribe}` inside `func(ref int32,
|
|
823
|
+
// unsubscribe func())` references the parameter, never a same-name
|
|
824
|
+
// package symbol. The enclosing INDEXED symbol's params are checked at
|
|
825
|
+
// query time in findCallers — func-literal params and block locals are
|
|
826
|
+
// only visible here. (Rust/Java need no equivalent: their parsers emit
|
|
827
|
+
// no bare-identifier callback references — Rust only obj.method field
|
|
828
|
+
// expressions, Java only :: method references.)
|
|
829
|
+
const _paramListDeclares = (paramsNode, name) => {
|
|
830
|
+
if (!paramsNode) return false;
|
|
831
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
832
|
+
const pd = paramsNode.namedChild(i);
|
|
833
|
+
if (pd.type !== 'parameter_declaration' && pd.type !== 'variadic_parameter_declaration') continue;
|
|
834
|
+
// Go allows several names per declaration: `a, b int`
|
|
835
|
+
for (let j = 0; j < pd.namedChildCount; j++) {
|
|
836
|
+
const c = pd.namedChild(j);
|
|
837
|
+
if (c.type === 'identifier' && c.text === name) return true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return false;
|
|
841
|
+
};
|
|
842
|
+
const _declaresLocal = (stmt, name, refNode) => {
|
|
843
|
+
if (!stmt) return false;
|
|
844
|
+
// The declaration CONTAINING the reference is not a shadow: in
|
|
845
|
+
// `unsubscribe := unsubscribe` the RHS names the OUTER binding —
|
|
846
|
+
// Go's := declares the LHS only after the statement.
|
|
847
|
+
if (stmt.startIndex <= refNode.startIndex && stmt.endIndex >= refNode.endIndex) return false;
|
|
848
|
+
if (stmt.type === 'short_var_declaration') {
|
|
849
|
+
const left = stmt.childForFieldName('left');
|
|
850
|
+
if (left) {
|
|
851
|
+
for (let i = 0; i < left.namedChildCount; i++) {
|
|
852
|
+
const id = left.namedChild(i);
|
|
853
|
+
if (id.type === 'identifier' && id.text === name) return true;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} else if (stmt.type === 'var_declaration') {
|
|
857
|
+
for (let i = 0; i < stmt.namedChildCount; i++) {
|
|
858
|
+
const spec = stmt.namedChild(i);
|
|
859
|
+
if (spec.type !== 'var_spec') continue;
|
|
860
|
+
for (let j = 0; j < spec.namedChildCount; j++) {
|
|
861
|
+
const id = spec.namedChild(j);
|
|
862
|
+
if (id.type === 'identifier' && id.text === name) return true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return false;
|
|
867
|
+
};
|
|
868
|
+
const isShadowedByLocal = (refNode, name) => {
|
|
869
|
+
for (let p = refNode.parent; p; p = p.parent) {
|
|
870
|
+
if (p.type === 'block') {
|
|
871
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
872
|
+
const stmt = p.namedChild(i);
|
|
873
|
+
if (stmt.startIndex >= refNode.startIndex) break; // declaration-before-use
|
|
874
|
+
if (_declaresLocal(stmt, name, refNode)) return true;
|
|
875
|
+
}
|
|
876
|
+
} else if (p.type === 'for_statement') {
|
|
877
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
878
|
+
const c = p.namedChild(i);
|
|
879
|
+
if (c.type === 'range_clause') {
|
|
880
|
+
const left = c.childForFieldName('left');
|
|
881
|
+
if (left) {
|
|
882
|
+
for (let j = 0; j < left.namedChildCount; j++) {
|
|
883
|
+
const id = left.namedChild(j);
|
|
884
|
+
if (id.type === 'identifier' && id.text === name) return true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
} else if (c.type === 'for_clause') {
|
|
888
|
+
if (_declaresLocal(c.childForFieldName('initializer'), name, refNode)) return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} else if (p.type === 'if_statement' || p.type === 'expression_switch_statement' ||
|
|
892
|
+
p.type === 'type_switch_statement') {
|
|
893
|
+
if (_declaresLocal(p.childForFieldName('initializer'), name, refNode)) return true;
|
|
894
|
+
// if/switch initializers are plain named children in some
|
|
895
|
+
// grammar versions; type switches bind `v := x.(type)`
|
|
896
|
+
for (let i = 0; i < p.namedChildCount; i++) {
|
|
897
|
+
const c = p.namedChild(i);
|
|
898
|
+
if (c.type === 'short_var_declaration' && _declaresLocal(c, name, refNode)) return true;
|
|
899
|
+
if (p.type === 'type_switch_statement' && c.type === 'expression_list' &&
|
|
900
|
+
c.nextSibling?.type === ':=') {
|
|
901
|
+
for (let j = 0; j < c.namedChildCount; j++) {
|
|
902
|
+
const id = c.namedChild(j);
|
|
903
|
+
if (id.type === 'identifier' && id.text === name) return true;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else if (p.type === 'func_literal' || p.type === 'function_declaration' ||
|
|
908
|
+
p.type === 'method_declaration') {
|
|
909
|
+
if (_paramListDeclares(p.childForFieldName('parameters'), name)) return true;
|
|
910
|
+
if (p.type === 'method_declaration' &&
|
|
911
|
+
_paramListDeclares(p.childForFieldName('receiver'), name)) return true;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return false;
|
|
915
|
+
};
|
|
916
|
+
|
|
739
917
|
traverseTree(tree.rootNode, (node) => {
|
|
740
918
|
// Track function entry
|
|
741
919
|
if (isFunctionNode(node)) {
|
|
@@ -795,6 +973,23 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
795
973
|
if (callName && /^New[A-Z]/.test(callName)) {
|
|
796
974
|
typeName = callName.slice(3);
|
|
797
975
|
if (!typeName || !/^[A-Z]/.test(typeName)) typeName = null;
|
|
976
|
+
} else if (callFuncNode.type === 'identifier' &&
|
|
977
|
+
callFuncNode.text === 'new') {
|
|
978
|
+
// buf := new(bytes.Buffer) — the builtin
|
|
979
|
+
// allocator returns *T (fix #220,
|
|
980
|
+
// cobra-measured: buf.String() is
|
|
981
|
+
// bytes.Buffer's, never a project method).
|
|
982
|
+
// The argument parses as an expression:
|
|
983
|
+
// identifier or selector_expression.
|
|
984
|
+
const args = val.childForFieldName('arguments');
|
|
985
|
+
const argNode = args && args.namedChild(0);
|
|
986
|
+
if (argNode) {
|
|
987
|
+
typeName = argNode.type === 'identifier'
|
|
988
|
+
? argNode.text
|
|
989
|
+
: argNode.type === 'selector_expression'
|
|
990
|
+
? argNode.childForFieldName('field')?.text || null
|
|
991
|
+
: extractTypeName(argNode);
|
|
992
|
+
}
|
|
798
993
|
}
|
|
799
994
|
}
|
|
800
995
|
}
|
|
@@ -804,6 +999,34 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
804
999
|
}
|
|
805
1000
|
}
|
|
806
1001
|
|
|
1002
|
+
// Explicitly typed var declarations: `var buf bytes.Buffer`,
|
|
1003
|
+
// `var sb strings.Builder` (fix #220, cobra-measured — sb.String()
|
|
1004
|
+
// on an untyped receiver fell to single-owner confirmation). Same
|
|
1005
|
+
// semantics as parameter annotations: the declared type is the
|
|
1006
|
+
// receiver's compile-time type.
|
|
1007
|
+
if (node.type === 'var_declaration' && functionStack.length > 0) {
|
|
1008
|
+
const scopeKey = functionStack[functionStack.length - 1].startLine;
|
|
1009
|
+
const varTypeMap = scopeTypes.get(scopeKey);
|
|
1010
|
+
if (varTypeMap) {
|
|
1011
|
+
const recordSpec = (spec) => {
|
|
1012
|
+
if (spec.type !== 'var_spec') return;
|
|
1013
|
+
const typeName = extractTypeName(spec.childForFieldName('type'));
|
|
1014
|
+
if (!typeName) return;
|
|
1015
|
+
for (let j = 0; j < spec.namedChildCount; j++) {
|
|
1016
|
+
const id = spec.namedChild(j);
|
|
1017
|
+
if (id.type === 'identifier') varTypeMap.set(id.text, typeName);
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1021
|
+
const c = node.namedChild(i);
|
|
1022
|
+
if (c.type === 'var_spec') recordSpec(c);
|
|
1023
|
+
else if (c.type === 'var_spec_list') {
|
|
1024
|
+
for (let j = 0; j < c.namedChildCount; j++) recordSpec(c.namedChild(j));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
807
1030
|
// Track local closures: atoi := func(...) { ... } or var handler = func(...) { ... }
|
|
808
1031
|
if (node.type === 'short_var_declaration' || node.type === 'var_declaration') {
|
|
809
1032
|
// Check if a subtree contains a func_literal
|
|
@@ -861,6 +1084,26 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
861
1084
|
const enclosingFunction = getCurrentEnclosingFunction();
|
|
862
1085
|
let uncertain = false;
|
|
863
1086
|
|
|
1087
|
+
// Call-site arg count for arity pruning. Slice-spread (`xs...`)
|
|
1088
|
+
// makes the count open-ended — flag it so pruning skips the site.
|
|
1089
|
+
const argsNode = node.childForFieldName('arguments');
|
|
1090
|
+
let argCount = 0;
|
|
1091
|
+
let argSpread = false;
|
|
1092
|
+
if (argsNode) {
|
|
1093
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1094
|
+
if (argsNode.namedChild(i).type === 'comment') continue;
|
|
1095
|
+
argCount++;
|
|
1096
|
+
}
|
|
1097
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
1098
|
+
if (argsNode.child(i).type === '...') { argSpread = true; break; }
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Assignment target for return-type flow (fix #207):
|
|
1103
|
+
// bb := balancer.Get(n) lets findCallers type bb from Get's
|
|
1104
|
+
// declared return type at query time.
|
|
1105
|
+
const assigned = goAssignmentTargetOf(node);
|
|
1106
|
+
|
|
864
1107
|
if (funcNode.type === 'identifier') {
|
|
865
1108
|
const callName = funcNode.text;
|
|
866
1109
|
// Skip Go built-in function calls
|
|
@@ -877,6 +1120,11 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
877
1120
|
name: callName,
|
|
878
1121
|
line: node.startPosition.row + 1,
|
|
879
1122
|
isMethod: false,
|
|
1123
|
+
argCount,
|
|
1124
|
+
...(argSpread && { argSpread: true }),
|
|
1125
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1126
|
+
...(assigned?.assignedTuple && { assignedTuple: true }),
|
|
1127
|
+
...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
|
|
880
1128
|
enclosingFunction,
|
|
881
1129
|
uncertain,
|
|
882
1130
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
@@ -892,13 +1140,67 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
892
1140
|
// If receiver is a known import alias, this is a package call, not a method call
|
|
893
1141
|
const isPkgCall = receiver && importAliases.has(receiver);
|
|
894
1142
|
const receiverType = (!isPkgCall && receiver) ? getReceiverType(receiver) : undefined;
|
|
1143
|
+
// fix #202: one-hop declared-field receivers — h.inner.Run().
|
|
1144
|
+
// receiverRoot/Field/RootType let findCallers hop to the
|
|
1145
|
+
// field's declared struct-field type cross-file.
|
|
1146
|
+
let receiverRoot, receiverFieldName, receiverRootType;
|
|
1147
|
+
if (!receiver && operandNode?.type === 'selector_expression') {
|
|
1148
|
+
const rootNode = operandNode.childForFieldName('operand');
|
|
1149
|
+
const fldNode = operandNode.childForFieldName('field');
|
|
1150
|
+
if (rootNode?.type === 'identifier' && fldNode &&
|
|
1151
|
+
!importAliases.has(rootNode.text)) {
|
|
1152
|
+
receiverRoot = rootNode.text;
|
|
1153
|
+
receiverFieldName = fldNode.text;
|
|
1154
|
+
receiverRootType = getReceiverType(rootNode.text);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Chained receiver (fix #220, cobra-measured): the receiver
|
|
1158
|
+
// IS a call — rootCmd.Flags().String(...) — record the
|
|
1159
|
+
// producer so findCallers can type the receiver from its
|
|
1160
|
+
// declared return (*pflag.FlagSet → external → routed).
|
|
1161
|
+
// Package-qualified producers (os.CreateTemp().Name())
|
|
1162
|
+
// carry the qualifier for strict import-package resolution.
|
|
1163
|
+
let receiverCall, receiverCallIsMethod, receiverCallReceiver;
|
|
1164
|
+
if (!receiver && !receiverFieldName && operandNode?.type === 'call_expression') {
|
|
1165
|
+
const prodFunc = operandNode.childForFieldName('function');
|
|
1166
|
+
if (prodFunc?.type === 'identifier') {
|
|
1167
|
+
receiverCall = prodFunc.text;
|
|
1168
|
+
} else if (prodFunc?.type === 'selector_expression') {
|
|
1169
|
+
const pf = prodFunc.childForFieldName('field');
|
|
1170
|
+
const po = prodFunc.childForFieldName('operand');
|
|
1171
|
+
if (pf) {
|
|
1172
|
+
receiverCall = pf.text;
|
|
1173
|
+
if (po?.type === 'identifier' && importAliases.has(po.text)) {
|
|
1174
|
+
receiverCallReceiver = po.text;
|
|
1175
|
+
} else {
|
|
1176
|
+
receiverCallIsMethod = true;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
895
1181
|
const firstArg = getFirstStringArg(node);
|
|
896
1182
|
calls.push({
|
|
897
1183
|
name: fieldNode.text,
|
|
898
|
-
|
|
1184
|
+
// Name-node line convention (#201/RUST-2, fix #223):
|
|
1185
|
+
// a method call on a multi-line receiver —
|
|
1186
|
+
// (&pkg.Name{...}).String() — reports the FIELD's own
|
|
1187
|
+
// line, not the chain-start line. The account's ground
|
|
1188
|
+
// set and the oracles key by the name's line; Go was
|
|
1189
|
+
// the only parser still using the call node's start.
|
|
1190
|
+
line: fieldNode.startPosition.row + 1,
|
|
899
1191
|
isMethod: !isPkgCall,
|
|
900
1192
|
receiver,
|
|
901
1193
|
...(receiverType && { receiverType }),
|
|
1194
|
+
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1195
|
+
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1196
|
+
...(receiverCall && { receiverCall }),
|
|
1197
|
+
...(receiverCallIsMethod && { receiverCallIsMethod: true }),
|
|
1198
|
+
...(receiverCallReceiver && { receiverCallReceiver }),
|
|
1199
|
+
argCount,
|
|
1200
|
+
...(argSpread && { argSpread: true }),
|
|
1201
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
1202
|
+
...(assigned?.assignedTuple && { assignedTuple: true }),
|
|
1203
|
+
...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
|
|
902
1204
|
enclosingFunction,
|
|
903
1205
|
uncertain,
|
|
904
1206
|
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
@@ -926,13 +1228,18 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
926
1228
|
const typeNode = node.childForFieldName('type');
|
|
927
1229
|
if (typeNode) {
|
|
928
1230
|
let typeName = null;
|
|
1231
|
+
let typeQualifier = null;
|
|
929
1232
|
if (typeNode.type === 'type_identifier') {
|
|
930
1233
|
// Foo{...}
|
|
931
1234
|
typeName = typeNode.text;
|
|
932
1235
|
} else if (typeNode.type === 'qualified_type') {
|
|
933
|
-
// pkg.Foo{...}
|
|
1236
|
+
// pkg.Foo{...} — keep the package qualifier as receiver: a
|
|
1237
|
+
// package-qualified type can never resolve to a same-file
|
|
1238
|
+
// binding (Go cannot self-import), so resolution must not
|
|
1239
|
+
// claim local same-name symbols for it.
|
|
934
1240
|
const tn = typeNode.childForFieldName('name');
|
|
935
1241
|
if (tn) typeName = tn.text;
|
|
1242
|
+
typeQualifier = typeNode.childForFieldName('package')?.text || null;
|
|
936
1243
|
}
|
|
937
1244
|
// Skip anonymous types (slice_type, map_type, array_type, struct_type, etc.)
|
|
938
1245
|
if (typeName) {
|
|
@@ -942,6 +1249,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
942
1249
|
line: node.startPosition.row + 1,
|
|
943
1250
|
isMethod: false,
|
|
944
1251
|
isConstructor: true,
|
|
1252
|
+
...(typeQualifier && { receiver: typeQualifier }),
|
|
945
1253
|
enclosingFunction,
|
|
946
1254
|
uncertain: false
|
|
947
1255
|
});
|
|
@@ -988,7 +1296,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
988
1296
|
isFunctionReference: true,
|
|
989
1297
|
isPotentialCallback: true,
|
|
990
1298
|
enclosingFunction,
|
|
991
|
-
uncertain: false
|
|
1299
|
+
uncertain: false,
|
|
1300
|
+
...(isShadowedByLocal(node, name) && { localShadow: true }),
|
|
992
1301
|
});
|
|
993
1302
|
}
|
|
994
1303
|
}
|
|
@@ -1039,7 +1348,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1039
1348
|
isFunctionReference: true,
|
|
1040
1349
|
isPotentialCallback: true,
|
|
1041
1350
|
enclosingFunction,
|
|
1042
|
-
uncertain: false
|
|
1351
|
+
uncertain: false,
|
|
1352
|
+
...(isShadowedByLocal(rhs, name) && { localShadow: true }),
|
|
1043
1353
|
});
|
|
1044
1354
|
}
|
|
1045
1355
|
}
|
|
@@ -1097,6 +1407,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1097
1407
|
uncertain: false,
|
|
1098
1408
|
...(compositeType && { compositeType }),
|
|
1099
1409
|
...(fieldName && { fieldName }),
|
|
1410
|
+
...(isShadowedByLocal(valueNode, name) && { localShadow: true }),
|
|
1100
1411
|
});
|
|
1101
1412
|
}
|
|
1102
1413
|
}
|
|
@@ -1329,7 +1640,7 @@ function findUsagesInCode(code, name, parser) {
|
|
|
1329
1640
|
const tree = parseTree(parser, code);
|
|
1330
1641
|
const usages = [];
|
|
1331
1642
|
|
|
1332
|
-
|
|
1643
|
+
visitNameNodes(tree, code, name, (node) => {
|
|
1333
1644
|
// Look for identifier, field_identifier (method names in selector expressions),
|
|
1334
1645
|
// and type_identifier (type references in params, return types, composite literals, etc.)
|
|
1335
1646
|
const isIdentifier = node.type === 'identifier' || node.type === 'field_identifier' || node.type === 'type_identifier';
|
package/languages/index.js
CHANGED
|
@@ -21,6 +21,25 @@ const STRUCTURAL_TRAITS = {
|
|
|
21
21
|
exportVisibility: 'keyword',
|
|
22
22
|
hasDynamicImports: true,
|
|
23
23
|
testDirs: [],
|
|
24
|
+
allMethodsVirtual: false,
|
|
25
|
+
hasArityOverloads: false,
|
|
26
|
+
// Class members are public unless marked otherwise (#name, _name,
|
|
27
|
+
// `private`). An exported class therefore exposes its non-private methods
|
|
28
|
+
// as public API — deadcode treats them as exported (fix #211). Languages
|
|
29
|
+
// with explicit member visibility (Rust `pub`, Java `public` — already
|
|
30
|
+
// captured as modifiers) or capitalization rules (Go) set false: there
|
|
31
|
+
// the member's own marker decides.
|
|
32
|
+
implicitlyPublicMembers: true,
|
|
33
|
+
// A bare (receiver-less) name can never denote a METHOD here — JS/Python
|
|
34
|
+
// methods are reached through their receiver; only a rebound alias could,
|
|
35
|
+
// which is separate name-level evidence. Java sets true: `execute()`
|
|
36
|
+
// inside a class means this.execute(), and static imports bind foreign
|
|
37
|
+
// class methods to bare names (fix #220).
|
|
38
|
+
bareCallReachesMethods: false,
|
|
39
|
+
// A method-shaped call CAN reach a standalone function here: attribute
|
|
40
|
+
// assignment rebinds functions onto objects (obj.print = print), so the
|
|
41
|
+
// #218b gate routes such calls visible instead of excluding (fix #220).
|
|
42
|
+
methodCallReachesFunctions: true,
|
|
24
43
|
};
|
|
25
44
|
const NOMINAL_TRAITS = {
|
|
26
45
|
typeSystem: 'nominal',
|
|
@@ -30,6 +49,58 @@ const NOMINAL_TRAITS = {
|
|
|
30
49
|
exportVisibility: 'keyword',
|
|
31
50
|
hasDynamicImports: true,
|
|
32
51
|
testDirs: [],
|
|
52
|
+
// Whether ANY instance method call can dynamically dispatch to a subtype
|
|
53
|
+
// override (Java: all instance methods are virtual). Go struct method
|
|
54
|
+
// sets and Rust inherent methods bind statically — only interface/trait
|
|
55
|
+
// receivers dispatch there, which is detected per-type, not per-language.
|
|
56
|
+
allMethodsVirtual: false,
|
|
57
|
+
// Whether one class can define several same-name methods differing only
|
|
58
|
+
// in parameters (Java overloading). Drives the overload discipline in
|
|
59
|
+
// the caller contract: a pinned overload is only confirmed when the call
|
|
60
|
+
// site provably binds it.
|
|
61
|
+
hasArityOverloads: false,
|
|
62
|
+
// What a TYPE-QUALIFIED method call looks like, so a receiver that merely
|
|
63
|
+
// shares the target type's NAME isn't mistaken for the type itself:
|
|
64
|
+
// 'static' — Type.method() static form, any arity (Java).
|
|
65
|
+
// 'method-expr' — Go method expressions T.M(recv, ...): the receiver
|
|
66
|
+
// instance is the FIRST argument, so a zero-arg call on
|
|
67
|
+
// a type-named receiver must be a variable (grpc-go
|
|
68
|
+
// names builder structs and Builder locals both `bb`).
|
|
69
|
+
// 'path' — Rust Type::method (isPathCall); a DOT-call receiver
|
|
70
|
+
// matching a type name is a variable, never the type.
|
|
71
|
+
typeQualifiedCallStyle: 'static',
|
|
72
|
+
implicitlyPublicMembers: false,
|
|
73
|
+
// The implicit root supertype every class extends without declaring it
|
|
74
|
+
// (Java `Object`). A receiver declared with this type can hold ANY project
|
|
75
|
+
// instance, so it is dispatch-capable toward every override — but the
|
|
76
|
+
// edge is invisible to declared-ancestry walks (fix #212). Routing only,
|
|
77
|
+
// never exclusion evidence. Go/Rust: null — Go's interface{}/any cannot
|
|
78
|
+
// receive method calls without an assertion, Rust has no universal
|
|
79
|
+
// supertype. Structural languages: null — any/object/unknown receivers
|
|
80
|
+
// are already refused as exclusion evidence by the trust gate.
|
|
81
|
+
universalSupertype: null,
|
|
82
|
+
// A bare (receiver-less) call or reference can never denote a METHOD:
|
|
83
|
+
// Go method values/expressions require an explicit receiver or type
|
|
84
|
+
// qualifier (m.Helper(), T.Helper); Rust requires self./Type:: and `use`
|
|
85
|
+
// cannot import associated functions. A bare MarkFlagDirname(...) inside
|
|
86
|
+
// Command.MarkFlagDirname denotes the package FUNCTION (fix #220,
|
|
87
|
+
// cobra/grpc-go-measured). Java overrides true (implicit this-calls,
|
|
88
|
+
// static imports).
|
|
89
|
+
bareCallReachesMethods: false,
|
|
90
|
+
// Whether a method-shaped call (x.f()) can reach a standalone FUNCTION:
|
|
91
|
+
// Go func-typed fields are name-callable (s.Run() may invoke a stored
|
|
92
|
+
// function), so exclusion needs !bindingId there; Rust requires (s.f)()
|
|
93
|
+
// parens and Java requires .apply() — a dot-call provably never binds a
|
|
94
|
+
// free function (fix #220, ripgrep-measured).
|
|
95
|
+
methodCallReachesFunctions: false,
|
|
96
|
+
// Whether a paren-less member access (x.name) can denote a METHOD. Rust
|
|
97
|
+
// sets true for the inverse — `x.name` is ALWAYS a field there (method
|
|
98
|
+
// values are path-only: Type::method), so a member-access reference
|
|
99
|
+
// against a method target is excluded (fix #220, ripgrep-measured:
|
|
100
|
+
// `self.paths.has_implicit_path` is the bool FIELD, not the method).
|
|
101
|
+
// Go method values (obj.Method) and Java `::` references DO denote
|
|
102
|
+
// methods — false. Per-language override, not preset-wide.
|
|
103
|
+
memberAccessNeverMethod: false,
|
|
33
104
|
};
|
|
34
105
|
|
|
35
106
|
// Language configurations
|
|
@@ -99,6 +170,8 @@ const LANGUAGES = {
|
|
|
99
170
|
hasReceiverPackageCalls: true,
|
|
100
171
|
exportVisibility: 'capitalization',
|
|
101
172
|
hasDynamicImports: false,
|
|
173
|
+
typeQualifiedCallStyle: 'method-expr',
|
|
174
|
+
methodCallReachesFunctions: true,
|
|
102
175
|
testFileCandidates: (base, ext) => [`${base}_test.go`],
|
|
103
176
|
},
|
|
104
177
|
},
|
|
@@ -112,6 +185,8 @@ const LANGUAGES = {
|
|
|
112
185
|
...NOMINAL_TRAITS,
|
|
113
186
|
selfParam: ['self', '&self', '&mut self', 'mut self'],
|
|
114
187
|
hasDynamicImports: false,
|
|
188
|
+
typeQualifiedCallStyle: 'path',
|
|
189
|
+
memberAccessNeverMethod: true,
|
|
115
190
|
testFileCandidates: (base, ext) => [`${base}_test.rs`],
|
|
116
191
|
testDirs: ['tests'],
|
|
117
192
|
},
|
|
@@ -125,6 +200,10 @@ const LANGUAGES = {
|
|
|
125
200
|
traits: {
|
|
126
201
|
...NOMINAL_TRAITS,
|
|
127
202
|
selfParam: ['this'],
|
|
203
|
+
allMethodsVirtual: true,
|
|
204
|
+
hasArityOverloads: true,
|
|
205
|
+
universalSupertype: 'Object',
|
|
206
|
+
bareCallReachesMethods: true,
|
|
128
207
|
testFileCandidates: (base, ext) => [`${base}Test.java`, `${base}Tests.java`, `${base}TestCase.java`],
|
|
129
208
|
},
|
|
130
209
|
},
|