ucn 5.3.4 → 5.3.6
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 +22 -0
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +5 -1
- package/core/callers.js +989 -124
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +8 -3
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +185 -77
- package/languages/type-evidence.js +63 -0
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared by receiver resolution and the pure provenance validator.
|
|
4
|
+
const BUILTIN_RECEIVER_TYPES = new Set([
|
|
5
|
+
'dict', 'list', 'set', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'frozenset',
|
|
6
|
+
'Mapping', 'MutableMapping', 'Sequence', 'MutableSequence',
|
|
7
|
+
'Collection', 'Iterable', 'Iterator', 'KeysView', 'ValuesView', 'ItemsView',
|
|
8
|
+
'IO', 'TextIO', 'BinaryIO', 'StringIO', 'BytesIO',
|
|
9
|
+
'ZlibCompress', 'ZlibDecompress',
|
|
10
|
+
'AsyncEvent',
|
|
11
|
+
'Generator', 'AsyncGenerator', 'ContextManager', 'AsyncContextManager',
|
|
12
|
+
'Array', 'String', 'Object', 'RegExp', 'Number', 'Boolean', 'Map', 'Set', 'Promise',
|
|
13
|
+
'WeakMap', 'WeakSet',
|
|
14
|
+
'string', 'number', 'boolean', 'bigint', 'symbol',
|
|
15
|
+
'object', 'dynamic', 'decimal', 'byte', 'sbyte', 'char',
|
|
16
|
+
'short', 'ushort', 'uint', 'long', 'ulong', 'double',
|
|
17
|
+
'List', 'Dictionary', 'HashSet', 'Queue', 'Stack',
|
|
18
|
+
'Task', 'ValueTask', 'IEnumerable', 'ICollection', 'IList',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function isProvenanceBuiltinReceiver(type, language) {
|
|
22
|
+
return BUILTIN_RECEIVER_TYPES.has(type) ||
|
|
23
|
+
(language === 'rust' && ['Option', 'Result', 'Vec'].includes(type));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { BUILTIN_RECEIVER_TYPES, isProvenanceBuiltinReceiver };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { declarationIdentity, identityKey } = require('./provenance');
|
|
5
|
+
const { resolveRustImport } = require('./imports');
|
|
6
|
+
|
|
7
|
+
const TYPE_KINDS = new Set(['class', 'struct', 'enum', 'type', 'trait', 'interface']);
|
|
8
|
+
|
|
9
|
+
function parseGeneric(value) {
|
|
10
|
+
if (typeof value !== 'string') return null;
|
|
11
|
+
const text = value.trim(), start = text.indexOf('<');
|
|
12
|
+
if (start < 1 || !text.endsWith('>')) return null;
|
|
13
|
+
const args = [];
|
|
14
|
+
let depth = 0, current = '';
|
|
15
|
+
for (const character of text.slice(start + 1, -1)) {
|
|
16
|
+
if ('<(['.includes(character)) depth++;
|
|
17
|
+
else if ('>)]'.includes(character)) depth--;
|
|
18
|
+
if (character === ',' && depth === 0) { args.push(current.trim()); current = ''; }
|
|
19
|
+
else current += character;
|
|
20
|
+
if (depth < 0) return null;
|
|
21
|
+
}
|
|
22
|
+
if (depth) return null;
|
|
23
|
+
args.push(current.trim());
|
|
24
|
+
return args.every(Boolean) ? { head: text.slice(0, start).trim(), args } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function projectTuple(text, projection) {
|
|
28
|
+
for (const position of projection) {
|
|
29
|
+
text = text.trim();
|
|
30
|
+
if (!Number.isInteger(position) || position < 0 || !text.startsWith('(') || !text.endsWith(')')) return null;
|
|
31
|
+
const tuple = parseGeneric(`Tuple<${text.slice(1, -1)}>`);
|
|
32
|
+
if (!tuple || position >= tuple.args.length) return null;
|
|
33
|
+
text = tuple.args[position];
|
|
34
|
+
}
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Preserve the payload of a proven standard Result/Option annotation.
|
|
39
|
+
* Generic arguments retain their own declaration scope through alias hops;
|
|
40
|
+
* a project type merely named Result is not a standard wrapper.
|
|
41
|
+
*/
|
|
42
|
+
function rustWrapperContract(index, text, file, options) {
|
|
43
|
+
const expression = parseGeneric(text);
|
|
44
|
+
if (!expression) return null;
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const chain = [];
|
|
47
|
+
const resolve = (head, args, context) => {
|
|
48
|
+
const key = `${context}:${head}`;
|
|
49
|
+
if (seen.has(key) || seen.size >= 12) return null;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
const entry = index.files.get(context);
|
|
52
|
+
if (!entry) return null;
|
|
53
|
+
const standard = /^(?:std|core)::(?:result::Result|option::Option)$/.test(head);
|
|
54
|
+
if (standard) {
|
|
55
|
+
const root = head.split('::')[0];
|
|
56
|
+
if ((index.symbols.get(root) || []).some(d => d.file === context) ||
|
|
57
|
+
(entry.importBindings || []).some(b => (b.alias || b.name) === root)) return null;
|
|
58
|
+
const kind = head.split('::').at(-1);
|
|
59
|
+
if (args.length !== (kind === 'Result' ? 2 : 1)) return null;
|
|
60
|
+
chain.push({ standard: head, fromFile: entry.relativePath,
|
|
61
|
+
rootDeclarations: [], rootBindings: [] });
|
|
62
|
+
return { kind, item: args[0] };
|
|
63
|
+
}
|
|
64
|
+
if (head.includes('::')) {
|
|
65
|
+
const parts = head.split('::');
|
|
66
|
+
const name = parts.pop();
|
|
67
|
+
const prefix = parts.join('::');
|
|
68
|
+
const relative = entry.moduleResolved?.[prefix];
|
|
69
|
+
const destination = relative ? path.resolve(index.root, relative)
|
|
70
|
+
: resolveRustImport(prefix, context, index.root);
|
|
71
|
+
if (!destination || !index.files.has(destination)) return null;
|
|
72
|
+
chain.push({ fromFile: entry.relativePath, typePath: head,
|
|
73
|
+
toFile: path.relative(index.root, destination) });
|
|
74
|
+
return resolve(name, args, destination);
|
|
75
|
+
}
|
|
76
|
+
const local = (index.symbols.get(head) || []).filter(d =>
|
|
77
|
+
d.file === context && TYPE_KINDS.has(d.type));
|
|
78
|
+
if (local.length) {
|
|
79
|
+
if (local.length !== 1) return null;
|
|
80
|
+
const alias = local[0];
|
|
81
|
+
if (alias.type !== 'type' || !alias.aliasTypeText ||
|
|
82
|
+
!alias.aliasTypeParameters?.every(Boolean) ||
|
|
83
|
+
alias.aliasTypeParameters.length < args.length) return null;
|
|
84
|
+
const body = parseGeneric(alias.aliasTypeText);
|
|
85
|
+
if (!body) return null;
|
|
86
|
+
const parameters = new Map();
|
|
87
|
+
for (const [i, name] of alias.aliasTypeParameters.entries()) {
|
|
88
|
+
const fallback = alias.aliasTypeDefaults?.[i];
|
|
89
|
+
const value = args[i] || (fallback && (parameters.get(fallback) || { text: fallback, file: context }));
|
|
90
|
+
if (!value || (!args[i] && !parameters.has(fallback) &&
|
|
91
|
+
fallback.split(/[^\w]+/).some(token => parameters.has(token)))) return null;
|
|
92
|
+
parameters.set(name, value);
|
|
93
|
+
}
|
|
94
|
+
const forwarded = body.args.map(text => parameters.get(text) || { text, file: context });
|
|
95
|
+
// Nested generic substitution needs its own type-expression model.
|
|
96
|
+
// Do not resolve an unsubstituted parameter in the alias's scope.
|
|
97
|
+
if (body.args.some(text => !parameters.has(text) &&
|
|
98
|
+
text.split(/[^\w]+/).some(token => parameters.has(token)))) return null;
|
|
99
|
+
chain.push({ declaration: declarationIdentity(alias), type: alias.aliasTypeText,
|
|
100
|
+
parameters: alias.aliasTypeParameters, defaults: alias.aliasTypeDefaults,
|
|
101
|
+
arguments: args.map(argument => ({ text: argument.text, file: path.relative(index.root, argument.file) })) });
|
|
102
|
+
return resolve(body.head, forwarded, context);
|
|
103
|
+
}
|
|
104
|
+
const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === head);
|
|
105
|
+
if (bindings.length) {
|
|
106
|
+
if (bindings.length !== 1) return null;
|
|
107
|
+
const binding = bindings[0];
|
|
108
|
+
const destination = entry.moduleResolved?.[binding.module];
|
|
109
|
+
chain.push({ fromFile: entry.relativePath, binding: { ...binding }, toFile: destination || null });
|
|
110
|
+
return destination
|
|
111
|
+
? resolve(binding.module.split('::').at(-1), args, path.resolve(index.root, destination))
|
|
112
|
+
: resolve(binding.module, args, context);
|
|
113
|
+
}
|
|
114
|
+
if (head === 'Result' || head === 'Option') {
|
|
115
|
+
if ((entry.importBindings || []).some(b => b.name === '*' || b.module?.endsWith('::*'))) return null;
|
|
116
|
+
chain.push({ prelude: head, fromFile: entry.relativePath,
|
|
117
|
+
declarations: [], bindings: [], wildcardImports: [] });
|
|
118
|
+
return resolve(head === 'Result' ? 'std::result::Result' : 'std::option::Option', args, context);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
const resolved = resolve(expression.head, expression.args.map(text => ({ text, file })), file);
|
|
123
|
+
if (!resolved) return null;
|
|
124
|
+
const wrapper = { kind: resolved.kind, annotation: { text, file: path.relative(index.root, file) }, chain };
|
|
125
|
+
const projection = options.projection || [];
|
|
126
|
+
const projected = projectTuple(resolved.item.text, projection);
|
|
127
|
+
const item = projected && options.parseType(projected);
|
|
128
|
+
if (!item) return options.allowUnknownPayload ? wrapper : null;
|
|
129
|
+
const origin = options.resolveType(resolved.item.file, item.name, item.qualifier);
|
|
130
|
+
const declarations = origin?.fromFile && (index.symbols.get(item.name) || []).filter(d =>
|
|
131
|
+
d.file === origin.fromFile && TYPE_KINDS.has(d.type));
|
|
132
|
+
if (declarations?.length !== 1) return options.allowUnknownPayload ? wrapper : null;
|
|
133
|
+
return { kind: resolved.kind, type: item.name, fromFile: origin.fromFile,
|
|
134
|
+
payload: { text: resolved.item.text, file: path.relative(index.root, resolved.item.file),
|
|
135
|
+
...(projection.length && { projection, projected }),
|
|
136
|
+
declaration: declarationIdentity(declarations[0]) },
|
|
137
|
+
annotation: { text, file: path.relative(index.root, file) }, chain };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Replay the type arguments and scope hops without consulting the resolver. */
|
|
141
|
+
function validateRustWrapperContract(contract, ownershipOnly = false) {
|
|
142
|
+
const initial = parseGeneric(contract?.annotation?.text);
|
|
143
|
+
if (!initial || !contract.annotation.file || !Array.isArray(contract.chain) ||
|
|
144
|
+
contract.chain.length > 24 || !identityKey(contract.producer) ||
|
|
145
|
+
contract.producer.file !== contract.annotation.file ||
|
|
146
|
+
contract.producer.returnType !== contract.annotation.text) return false;
|
|
147
|
+
let head = initial.head, file = contract.annotation.file;
|
|
148
|
+
let args = initial.args.map(text => ({ text, file }));
|
|
149
|
+
const empty = value => Array.isArray(value) && value.length === 0;
|
|
150
|
+
let standard;
|
|
151
|
+
for (const hop of contract.chain) {
|
|
152
|
+
if (standard) return false;
|
|
153
|
+
if (hop.declaration) {
|
|
154
|
+
if (!identityKey(hop.declaration) || hop.declaration.name !== head || hop.declaration.file !== file ||
|
|
155
|
+
hop.declaration.kind !== 'type' || !Array.isArray(hop.parameters) ||
|
|
156
|
+
!hop.parameters.every(name => typeof name === 'string' && name) ||
|
|
157
|
+
hop.parameters.length < args.length ||
|
|
158
|
+
JSON.stringify(hop.arguments) !== JSON.stringify(args)) return false;
|
|
159
|
+
const body = parseGeneric(hop.type);
|
|
160
|
+
if (!body) return false;
|
|
161
|
+
const parameters = new Map();
|
|
162
|
+
for (const [i, name] of hop.parameters.entries()) {
|
|
163
|
+
const fallback = hop.defaults?.[i];
|
|
164
|
+
const value = args[i] || (fallback && (parameters.get(fallback) || { text: fallback, file }));
|
|
165
|
+
if (!value || (!args[i] && !parameters.has(fallback) &&
|
|
166
|
+
fallback.split(/[^\w]+/).some(token => parameters.has(token)))) return false;
|
|
167
|
+
parameters.set(name, value);
|
|
168
|
+
}
|
|
169
|
+
if (body.args.some(text => !parameters.has(text) &&
|
|
170
|
+
text.split(/[^\w]+/).some(token => parameters.has(token)))) return false;
|
|
171
|
+
args = body.args.map(text => parameters.get(text) || { text, file });
|
|
172
|
+
head = body.head;
|
|
173
|
+
} else if (hop.binding) {
|
|
174
|
+
if (hop.fromFile !== file || (hop.binding.alias || hop.binding.name) !== head ||
|
|
175
|
+
typeof hop.binding.module !== 'string') return false;
|
|
176
|
+
head = hop.toFile ? hop.binding.module.split('::').at(-1) : hop.binding.module;
|
|
177
|
+
file = hop.toFile || file;
|
|
178
|
+
} else if (hop.typePath) {
|
|
179
|
+
if (hop.fromFile !== file || hop.typePath !== head || !hop.toFile) return false;
|
|
180
|
+
head = head.split('::').at(-1);
|
|
181
|
+
file = hop.toFile;
|
|
182
|
+
} else if (hop.prelude) {
|
|
183
|
+
if (hop.fromFile !== file || hop.prelude !== head || !['Result', 'Option'].includes(head) ||
|
|
184
|
+
!empty(hop.declarations) || !empty(hop.bindings) || !empty(hop.wildcardImports)) return false;
|
|
185
|
+
head = head === 'Result' ? 'std::result::Result' : 'std::option::Option';
|
|
186
|
+
} else if (hop.standard) {
|
|
187
|
+
if (hop.fromFile !== file || hop.standard !== head ||
|
|
188
|
+
!/^(std|core)::(result::Result|option::Option)$/.test(head) ||
|
|
189
|
+
!empty(hop.rootDeclarations) || !empty(hop.rootBindings)) return false;
|
|
190
|
+
standard = head.split('::').at(-1);
|
|
191
|
+
} else return false;
|
|
192
|
+
}
|
|
193
|
+
if (!standard || standard !== contract.kind || args.length !== (standard === 'Result' ? 2 : 1)) return false;
|
|
194
|
+
if (ownershipOnly) return true;
|
|
195
|
+
if (args[0].text !== contract.payload?.text || args[0].file !== contract.payload?.file) return false;
|
|
196
|
+
const projected = projectTuple(args[0].text, contract.payload.projection || []);
|
|
197
|
+
if (!projected || (contract.payload.projection?.length && contract.payload.projected !== projected)) return false;
|
|
198
|
+
let payloadHead = projected.replace(/^&(?:\s*'\w+)?\s*/, '').replace(/^mut\s+/, '').trim();
|
|
199
|
+
payloadHead = (parseGeneric(payloadHead)?.head || payloadHead).split('::').at(-1);
|
|
200
|
+
if (payloadHead === 'Self') payloadHead = contract.producer.className;
|
|
201
|
+
return payloadHead === contract.type && identityKey(contract.payload.declaration) !== null &&
|
|
202
|
+
contract.payload.declaration?.name === contract.type &&
|
|
203
|
+
TYPE_KINDS.has(contract.payload.declaration?.kind);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { rustWrapperContract, validateRustWrapperContract };
|
package/core/verify.js
CHANGED
|
@@ -9,6 +9,7 @@ const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits } =
|
|
|
9
9
|
const { sameNode } = require('../languages/utils');
|
|
10
10
|
const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
|
|
11
11
|
const { findAccessorReferences } = require('./accessors');
|
|
12
|
+
const { validateCallMismatch, declarationIdentity } = require('./provenance');
|
|
12
13
|
|
|
13
14
|
function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
14
15
|
if (!Number.isInteger(byteColumn) || byteColumn < 0) return null;
|
|
@@ -1340,7 +1341,10 @@ function verify(index, name, options = {}) {
|
|
|
1340
1341
|
|
|
1341
1342
|
// Convert caller results to usage-like objects for analyzeCallSite.
|
|
1342
1343
|
// Carry callerFile/callerStartLine through so we can compute inTestCase.
|
|
1343
|
-
const
|
|
1344
|
+
const invalidFamilyCalls = sweepUnverified.filter(c =>
|
|
1345
|
+
validateCallMismatch(c.provenance, declarationIdentity(def)));
|
|
1346
|
+
const calls = [...callerResults, ...invalidFamilyCalls].map(c => ({
|
|
1347
|
+
invalidOverload: invalidFamilyCalls.includes(c),
|
|
1344
1348
|
file: c.file,
|
|
1345
1349
|
relativePath: c.relativePath,
|
|
1346
1350
|
line: c.line,
|
|
@@ -1448,12 +1452,12 @@ function verify(index, name, options = {}) {
|
|
|
1448
1452
|
const countOk = hasRest
|
|
1449
1453
|
? argCount >= minArgs
|
|
1450
1454
|
: (argCount >= minArgs && argCount <= expectedParamCount);
|
|
1451
|
-
if (!countOk) {
|
|
1455
|
+
if (!countOk || call.invalidOverload) {
|
|
1452
1456
|
mismatches.push({
|
|
1453
1457
|
file: call.relativePath,
|
|
1454
1458
|
line: call.line,
|
|
1455
1459
|
expression: call.content.trim(),
|
|
1456
|
-
expected: hasRest
|
|
1460
|
+
expected: call.invalidOverload && countOk ? 'a compatible overload signature' : hasRest
|
|
1457
1461
|
? `at least ${minArgs} arg(s)`
|
|
1458
1462
|
: (minArgs === expectedParamCount
|
|
1459
1463
|
? `${expectedParamCount} arg(s)`
|
package/languages/adapter.js
CHANGED
package/languages/c-family.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* source fallback.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const { typeOrigin } = require('./type-evidence');
|
|
12
|
+
|
|
13
|
+
|
|
11
14
|
const {
|
|
12
15
|
traverseTree,
|
|
13
16
|
traverseTreeCached,
|
|
@@ -1946,6 +1949,10 @@ function buildVariableTypes(tree) {
|
|
|
1946
1949
|
name: identity.name,
|
|
1947
1950
|
type,
|
|
1948
1951
|
staticType: declaratorStaticType(type, declarator),
|
|
1952
|
+
origin: typeOrigin(node.type === 'declaration' &&
|
|
1953
|
+
(declarator.type === 'identifier' ||
|
|
1954
|
+
declarator.childForFieldName('value')?.type === 'new_expression')
|
|
1955
|
+
? 'constructor' : 'annotation', declarator),
|
|
1949
1956
|
...(pointeeType && { pointeeType }),
|
|
1950
1957
|
declaredAt: node.type === 'parameter_declaration'
|
|
1951
1958
|
? scope.startIndex : declarator.startIndex,
|
|
@@ -2044,6 +2051,12 @@ function buildVariableTypes(tree) {
|
|
|
2044
2051
|
return {
|
|
2045
2052
|
get: (name, atNode) => resolveBinding(name, atNode)?.type,
|
|
2046
2053
|
getStatic: (name, atNode) => resolveBinding(name, atNode)?.staticType,
|
|
2054
|
+
evidence: (name, atNode) => {
|
|
2055
|
+
const binding = resolveBinding(name, atNode);
|
|
2056
|
+
return { receiverTypeSource: binding?.origin?.source || 'unknown',
|
|
2057
|
+
...(binding && { receiverTypeEvidence: { ...binding.origin, name, type: binding.type } }),
|
|
2058
|
+
};
|
|
2059
|
+
},
|
|
2047
2060
|
getPointee: (name, atNode) =>
|
|
2048
2061
|
resolveBinding(name, atNode)?.pointeeType,
|
|
2049
2062
|
has: (name, atNode) => resolveBinding(name, atNode) !== undefined,
|
|
@@ -2501,7 +2514,7 @@ function findCallsInTree(code, parser, _options = {}, existingTree = null,
|
|
|
2501
2514
|
}),
|
|
2502
2515
|
...(compileTimeOnly && { compileTimeOnly }),
|
|
2503
2516
|
...(macroArguments.length > 0 && { macroArguments }),
|
|
2504
|
-
...(directReceiverType && { receiverType: directReceiverType }),
|
|
2517
|
+
...(directReceiverType && { receiverType: directReceiverType, ...variableTypes.evidence(receiverRoot, node) }),
|
|
2505
2518
|
...(receiverCall && {
|
|
2506
2519
|
receiverCall,
|
|
2507
2520
|
receiverIsChainRoot: true,
|
package/languages/csharp.js
CHANGED
|
@@ -587,6 +587,9 @@ function enclosingClassName(node) {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
/** Whether a bare identifier is a field/property/event of the enclosing type. */
|
|
590
|
+
|
|
591
|
+
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
592
|
+
|
|
590
593
|
function enclosingTypeDeclaresMember(node, memberName) {
|
|
591
594
|
let typeNode = null;
|
|
592
595
|
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
@@ -648,10 +651,10 @@ function variableScopeKey(node) {
|
|
|
648
651
|
}
|
|
649
652
|
|
|
650
653
|
function buildVariableTypes(tree, parser) {
|
|
651
|
-
const byScope = new Map([['global', new
|
|
654
|
+
const byScope = new Map([['global', new ReceiverTypeMap()]]);
|
|
652
655
|
const conflictsByScope = new Map([['global', new Set()]]);
|
|
653
656
|
const scopeStack = [];
|
|
654
|
-
const setType = (scope, name, type) => {
|
|
657
|
+
const setType = (scope, name, type, source = 'unknown', node = null) => {
|
|
655
658
|
if (!name || !type) return;
|
|
656
659
|
if (!conflictsByScope.has(scope)) conflictsByScope.set(scope, new Set());
|
|
657
660
|
const conflicts = conflictsByScope.get(scope);
|
|
@@ -663,14 +666,14 @@ function buildVariableTypes(tree, parser) {
|
|
|
663
666
|
conflicts.add(name);
|
|
664
667
|
return;
|
|
665
668
|
}
|
|
666
|
-
types.set(name, type);
|
|
669
|
+
types.set(name, type, source, node);
|
|
667
670
|
};
|
|
668
671
|
traverseTree(tree.rootNode, node => {
|
|
669
672
|
if (CALLABLE_SCOPE_NODES.has(node.type) &&
|
|
670
673
|
!isControlFlowLocalArtifact(node)) {
|
|
671
674
|
const key = node.startPosition.row + 1;
|
|
672
675
|
scopeStack.push(key);
|
|
673
|
-
if (!byScope.has(key)) byScope.set(key, new
|
|
676
|
+
if (!byScope.has(key)) byScope.set(key, new ReceiverTypeMap());
|
|
674
677
|
if (!conflictsByScope.has(key)) conflictsByScope.set(key, new Set());
|
|
675
678
|
}
|
|
676
679
|
const currentKey = scopeStack[scopeStack.length - 1] || 'global';
|
|
@@ -694,7 +697,7 @@ function buildVariableTypes(tree, parser) {
|
|
|
694
697
|
const type =
|
|
695
698
|
recoveredNode.childForFieldName('type')?.text ||
|
|
696
699
|
recoveredNode.namedChild(0)?.text;
|
|
697
|
-
setType(currentKey, name, type);
|
|
700
|
+
setType(currentKey, name, type, 'annotation', node);
|
|
698
701
|
return true;
|
|
699
702
|
});
|
|
700
703
|
}
|
|
@@ -711,14 +714,14 @@ function buildVariableTypes(tree, parser) {
|
|
|
711
714
|
if (artifactParameter) return true;
|
|
712
715
|
const name = node.childForFieldName('name')?.text;
|
|
713
716
|
const type = node.childForFieldName('type')?.text;
|
|
714
|
-
setType(currentKey, name, type);
|
|
717
|
+
setType(currentKey, name, type, 'annotation', node);
|
|
715
718
|
} else if (node.type === 'declaration_pattern' ||
|
|
716
719
|
node.type === 'declaration_expression') {
|
|
717
720
|
const name = node.childForFieldName('name')?.text ||
|
|
718
721
|
node.namedChildren.at(-1)?.text;
|
|
719
722
|
const type = node.childForFieldName('type')?.text ||
|
|
720
723
|
node.namedChild(0)?.text;
|
|
721
|
-
setType(currentKey, name, type);
|
|
724
|
+
setType(currentKey, name, type, 'annotation', node);
|
|
722
725
|
} else if (node.type === 'variable_declaration') {
|
|
723
726
|
// Class fields have their own declared-field receiver path; do not
|
|
724
727
|
// leak them into the top-level-program local scope.
|
|
@@ -747,7 +750,7 @@ function buildVariableTypes(tree, parser) {
|
|
|
747
750
|
const dynamicType = value?.type === 'object_creation_expression'
|
|
748
751
|
? value.childForFieldName('type')?.text : null;
|
|
749
752
|
const type = dynamicType || (typeNode?.text !== 'var' ? typeNode?.text : null);
|
|
750
|
-
setType(currentKey, name, type);
|
|
753
|
+
setType(currentKey, name, type, dynamicType ? 'constructor' : 'annotation', value || typeNode);
|
|
751
754
|
}
|
|
752
755
|
}
|
|
753
756
|
return true;
|
|
@@ -1206,12 +1209,19 @@ function findCallsInCode(code, parser) {
|
|
|
1206
1209
|
}
|
|
1207
1210
|
}
|
|
1208
1211
|
calls.push({
|
|
1212
|
+
callSite: typeOrigin('call', identity.nameNode || node),
|
|
1209
1213
|
name: identity.name,
|
|
1210
1214
|
line: identity.nameNode?.startPosition.row + 1 || node.startPosition.row + 1,
|
|
1211
1215
|
isMethod: identity.isMethod,
|
|
1212
1216
|
...(identity.receiver && { receiver: identity.receiver }),
|
|
1213
1217
|
...(receiverIsTypeQualified && { receiverIsTypeQualified: true }),
|
|
1214
|
-
...(receiverType && { receiverType
|
|
1218
|
+
...(receiverType && { receiverType, ...(unwrappedReceiverNode?.type === 'cast_expression' ? {
|
|
1219
|
+
receiverTypeSource: 'cast', receiverTypeEvidence: typeOrigin('cast', unwrappedReceiverNode),
|
|
1220
|
+
} : unwrappedReceiverNode?.type === 'object_creation_expression' ? {
|
|
1221
|
+
receiverTypeSource: 'constructor', receiverTypeEvidence: typeOrigin('constructor', unwrappedReceiverNode),
|
|
1222
|
+
} : literalReceiverType(unwrappedReceiverNode) ? {
|
|
1223
|
+
receiverTypeSource: 'literal', receiverTypeEvidence: typeOrigin('literal', unwrappedReceiverNode),
|
|
1224
|
+
} : variableTypes.fields(identity.receiver)) }),
|
|
1215
1225
|
...(receiverCastThis && { receiverCastThis: true }),
|
|
1216
1226
|
...(receiverType && receiverTypeInfo.namespace && {
|
|
1217
1227
|
receiverTypeNamespace: receiverTypeInfo.namespace,
|
|
@@ -1255,6 +1265,7 @@ function findCallsInCode(code, parser) {
|
|
|
1255
1265
|
}
|
|
1256
1266
|
if (!callbackName) continue;
|
|
1257
1267
|
calls.push({
|
|
1268
|
+
callSite: typeOrigin('call', value),
|
|
1258
1269
|
name: callbackName,
|
|
1259
1270
|
line: value.startPosition.row + 1,
|
|
1260
1271
|
isMethod: !!callbackReceiver,
|
|
@@ -1274,6 +1285,7 @@ function findCallsInCode(code, parser) {
|
|
|
1274
1285
|
const raw = typeNode.text.replace(/<.*>$/, '');
|
|
1275
1286
|
const name = raw.split('.').pop();
|
|
1276
1287
|
calls.push({
|
|
1288
|
+
callSite: typeOrigin('call', typeNode),
|
|
1277
1289
|
name,
|
|
1278
1290
|
line: typeNode.startPosition.row + 1,
|
|
1279
1291
|
isMethod: false,
|