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
package/languages/rust.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* modules, macros, and const/static declarations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
const {
|
|
9
12
|
traverseTree,
|
|
10
13
|
traverseTreeCached,
|
|
@@ -916,6 +919,14 @@ function _processClass(node, types, processedRanges, lines, code) {
|
|
|
916
919
|
members: [],
|
|
917
920
|
modifiers: visibility ? [visibility] : [],
|
|
918
921
|
...(aliasOf && { aliasOf }),
|
|
922
|
+
aliasTypeText: node.childForFieldName('type')?.text,
|
|
923
|
+
aliasTypeParameters: (node.childForFieldName('type_parameters')?.namedChildren || [])
|
|
924
|
+
.map(parameter => parameter.type === 'type_identifier' ? parameter.text
|
|
925
|
+
: parameter.childForFieldName('name')?.text ||
|
|
926
|
+
parameter.namedChildren.find(child => child.type === 'type_identifier')?.text)
|
|
927
|
+
.map(name => name || null),
|
|
928
|
+
aliasTypeDefaults: (node.childForFieldName('type_parameters')?.namedChildren || [])
|
|
929
|
+
.map(parameter => parameter.childForFieldName('default_type')?.text || null),
|
|
919
930
|
...(docstring && { docstring })
|
|
920
931
|
});
|
|
921
932
|
}
|
|
@@ -1525,12 +1536,57 @@ function _tokenTreeCallArgsAfter(children, nameIndex) {
|
|
|
1525
1536
|
return args?.type === 'token_tree' && args.text.startsWith('(') ? args : null;
|
|
1526
1537
|
}
|
|
1527
1538
|
|
|
1539
|
+
// Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
|
|
1540
|
+
function extractTypeName(typeNode) {
|
|
1541
|
+
if (!typeNode) return null;
|
|
1542
|
+
if (typeNode.type === 'type_identifier' ||
|
|
1543
|
+
typeNode.type === 'primitive_type') {
|
|
1544
|
+
return typeNode.text;
|
|
1545
|
+
}
|
|
1546
|
+
if (typeNode.type === 'reference_type') {
|
|
1547
|
+
// &Filter or &mut Filter -> Filter
|
|
1548
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
1549
|
+
const r = extractTypeName(typeNode.namedChild(i));
|
|
1550
|
+
if (r) return r;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (typeNode.type === 'abstract_type' || typeNode.type === 'dynamic_type') {
|
|
1554
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
1555
|
+
const r = extractTypeName(typeNode.namedChild(i));
|
|
1556
|
+
if (r) return r;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (typeNode.type === 'generic_type') {
|
|
1560
|
+
// Box<Filter> -> Filter (or get the outer type)
|
|
1561
|
+
return extractTypeName(typeNode.namedChild(0));
|
|
1562
|
+
}
|
|
1563
|
+
if (typeNode.type === 'scoped_type_identifier') {
|
|
1564
|
+
// module::Type -> Type
|
|
1565
|
+
const nameNode = typeNode.childForFieldName('name');
|
|
1566
|
+
return nameNode?.text || null;
|
|
1567
|
+
}
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
// Shared by ordinary AST calls and macro token-tree calls. Both carry the
|
|
1573
|
+
// same self-field owner identity; a field spelling is never a local receiver.
|
|
1574
|
+
function findEnclosingImplType(node) {
|
|
1575
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
1576
|
+
if (parent.type === 'impl_item') {
|
|
1577
|
+
const type = parent.childForFieldName('type');
|
|
1578
|
+
return (type && extractTypeName(type)) || undefined;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return undefined;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1528
1584
|
function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverType,
|
|
1529
1585
|
isPatternShadow, isFlowInvalidated, context = 'invocation') {
|
|
1530
1586
|
const contextKind = typeof context === 'string' ? context : context.kind;
|
|
1531
1587
|
const containerMacro = typeof context === 'object' ? context.containerMacro : undefined;
|
|
1532
1588
|
const inheritedTokenTypes = typeof context === 'object' && context.tokenTypes
|
|
1533
|
-
? context.tokenTypes : new
|
|
1589
|
+
? context.tokenTypes : new ReceiverTypeMap();
|
|
1534
1590
|
const children = [];
|
|
1535
1591
|
for (let i = 0; i < tree.childCount; i++) children.push(tree.child(i));
|
|
1536
1592
|
// Macro arguments are token trees, so a typed closure parameter is not a
|
|
@@ -1544,7 +1600,7 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1544
1600
|
let close = i + 1;
|
|
1545
1601
|
while (close < children.length && children[close].type !== '|') close++;
|
|
1546
1602
|
if (close >= children.length) break;
|
|
1547
|
-
const bindings = new
|
|
1603
|
+
const bindings = new ReceiverTypeMap(inheritedTokenTypes);
|
|
1548
1604
|
let cursor = i + 1;
|
|
1549
1605
|
while (cursor < close) {
|
|
1550
1606
|
const nameNode = children[cursor];
|
|
@@ -1562,7 +1618,7 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1562
1618
|
if (simplePath) {
|
|
1563
1619
|
const identifiers = typeTokens.filter(token => token.type === 'identifier');
|
|
1564
1620
|
if (identifiers.length > 0) {
|
|
1565
|
-
bindings.set(nameNode.text, identifiers[identifiers.length - 1].text);
|
|
1621
|
+
bindings.set(nameNode.text, identifiers[identifiers.length - 1].text, 'annotation', nameNode);
|
|
1566
1622
|
}
|
|
1567
1623
|
}
|
|
1568
1624
|
cursor = end + 1;
|
|
@@ -1703,12 +1759,15 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1703
1759
|
? ({ string_literal: 'str', raw_string_literal: 'str',
|
|
1704
1760
|
char_literal: 'char', boolean_literal: 'bool' })[recvTok.type]
|
|
1705
1761
|
: undefined;
|
|
1706
|
-
const
|
|
1762
|
+
const receiverRootType = receiverRoot === 'self'
|
|
1763
|
+
? findEnclosingImplType(tok) : getReceiverType?.(receiverRoot, tok);
|
|
1764
|
+
const receiverType = receiverField ? undefined : (receiver && receiver !== 'self')
|
|
1707
1765
|
? (getReceiverType?.(receiver, tok) || inheritedTokenTypes.get(receiver))
|
|
1708
1766
|
: litType;
|
|
1709
1767
|
const receiverPatternShadow = !!(receiver && isPatternShadow?.(tok, receiver));
|
|
1710
1768
|
const receiverFlowInvalidated = !!(receiver && isFlowInvalidated?.(tok, receiver));
|
|
1711
1769
|
const iterationSource = rustIterationSourceOf(tok, receiver);
|
|
1770
|
+
const patternSource = rustPatternBindingOf(tok, receiver);
|
|
1712
1771
|
const producer = !receiver && lastProducer &&
|
|
1713
1772
|
lastProducer.callEnd === recvTok?.endIndex ? lastProducer : null;
|
|
1714
1773
|
const record = {
|
|
@@ -1719,10 +1778,12 @@ function extractCallsFromTokenTree(tree, enclosingFunction, calls, getReceiverTy
|
|
|
1719
1778
|
isMethod: true,
|
|
1720
1779
|
receiver: receiverField ? undefined : receiver,
|
|
1721
1780
|
...(receiverField && { receiverRoot, receiverField }),
|
|
1722
|
-
...(
|
|
1781
|
+
...(receiverRootType && { receiverRootType }),
|
|
1782
|
+
...(receiverType && { receiverType, ...(getReceiverType?.(receiver, tok, true) || inheritedTokenTypes.fields(receiver, receiverType)) }),
|
|
1723
1783
|
...(receiverPatternShadow && { receiverPatternShadow: true }),
|
|
1724
1784
|
...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
|
|
1725
1785
|
...(iterationSource || {}),
|
|
1786
|
+
...(patternSource || {}),
|
|
1726
1787
|
...(producer && {
|
|
1727
1788
|
receiverCall: producer.name,
|
|
1728
1789
|
...(producer.isMethod && { receiverCallIsMethod: true }),
|
|
@@ -1948,26 +2009,13 @@ function rustPatternBindingOf(node, receiver) {
|
|
|
1948
2009
|
return identifiers.length === 1 ? identifiers[0] : null;
|
|
1949
2010
|
};
|
|
1950
2011
|
|
|
1951
|
-
|
|
1952
|
-
if (ancestor.type === 'function_item') break;
|
|
1953
|
-
if (ancestor.type === 'closure_expression') {
|
|
1954
|
-
const params = ancestor.childForFieldName('parameters');
|
|
1955
|
-
if (params && patternContainsIdentifier(params, receiver)) break;
|
|
1956
|
-
continue; // captured binding from an outer match arm
|
|
1957
|
-
}
|
|
1958
|
-
if (ancestor.type !== 'match_arm') continue;
|
|
1959
|
-
let matchExpression = ancestor.parent;
|
|
1960
|
-
while (matchExpression && matchExpression.type !== 'match_expression' &&
|
|
1961
|
-
matchExpression.type !== 'function_item') {
|
|
1962
|
-
matchExpression = matchExpression.parent;
|
|
1963
|
-
}
|
|
1964
|
-
const matchValue = matchExpression?.type === 'match_expression'
|
|
1965
|
-
? matchExpression.childForFieldName('value')
|
|
1966
|
-
: null;
|
|
2012
|
+
const readPattern = (root, matchValue) => {
|
|
1967
2013
|
const source = matchValue?.type === 'identifier'
|
|
1968
2014
|
? { receiverPatternSourceVariable: matchValue.text }
|
|
2015
|
+
: matchValue?.type === 'call_expression'
|
|
2016
|
+
? { receiverPatternSourceCallStart: matchValue.startIndex,
|
|
2017
|
+
receiverPatternSourceCallEnd: matchValue.endIndex }
|
|
1969
2018
|
: {};
|
|
1970
|
-
const root = ancestor.childForFieldName('pattern');
|
|
1971
2019
|
const pending = root ? [root] : [];
|
|
1972
2020
|
while (pending.length > 0) {
|
|
1973
2021
|
const pattern = pending.pop();
|
|
@@ -1976,7 +2024,19 @@ function rustPatternBindingOf(node, receiver) {
|
|
|
1976
2024
|
const positional = pattern.namedChildren
|
|
1977
2025
|
.filter(child => !typeNode || child.id !== typeNode.id);
|
|
1978
2026
|
for (let i = 0; i < positional.length; i++) {
|
|
1979
|
-
|
|
2027
|
+
const tupleProjection = (part, steps = []) => {
|
|
2028
|
+
if (directBindingName(part) === receiver) return steps;
|
|
2029
|
+
if (part.type !== 'tuple_pattern') return null;
|
|
2030
|
+
// Unnamed `_` nodes still occupy a tuple position.
|
|
2031
|
+
const elements = part.children.filter(child => !['(', ')', ','].includes(child.type));
|
|
2032
|
+
for (const [position, child] of elements.entries()) {
|
|
2033
|
+
const found = tupleProjection(child, [...steps, position]);
|
|
2034
|
+
if (found) return found;
|
|
2035
|
+
}
|
|
2036
|
+
return null;
|
|
2037
|
+
};
|
|
2038
|
+
const projection = tupleProjection(positional[i]);
|
|
2039
|
+
if (!projection) continue;
|
|
1980
2040
|
const pathText = typeNode?.text;
|
|
1981
2041
|
if (!pathText) return null;
|
|
1982
2042
|
const segments = pathText.split('::').filter(Boolean);
|
|
@@ -1985,6 +2045,7 @@ function rustPatternBindingOf(node, receiver) {
|
|
|
1985
2045
|
return {
|
|
1986
2046
|
receiverPatternVariant: variant,
|
|
1987
2047
|
receiverPatternIndex: i,
|
|
2048
|
+
...(projection.length && { receiverPatternProjection: projection }),
|
|
1988
2049
|
...source,
|
|
1989
2050
|
...(segments.length > 0 && {
|
|
1990
2051
|
receiverPatternOwner: segments.join('::'),
|
|
@@ -1997,6 +2058,49 @@ function rustPatternBindingOf(node, receiver) {
|
|
|
1997
2058
|
}
|
|
1998
2059
|
}
|
|
1999
2060
|
return null;
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
2064
|
+
if (ancestor.type === 'function_item') break;
|
|
2065
|
+
if (ancestor.type === 'block') {
|
|
2066
|
+
for (const child of [...ancestor.namedChildren].reverse()) {
|
|
2067
|
+
if (child.endIndex > node.startIndex) continue;
|
|
2068
|
+
const declaration = child.type === 'expression_statement' ? child.namedChild(0) : child;
|
|
2069
|
+
const pattern = declaration?.type === 'let_declaration'
|
|
2070
|
+
? declaration.childForFieldName('pattern')
|
|
2071
|
+
: declaration?.type === 'assignment_expression'
|
|
2072
|
+
? declaration.childForFieldName('left') : null;
|
|
2073
|
+
if (patternContainsIdentifier(pattern, receiver)) {
|
|
2074
|
+
return declaration.type === 'let_declaration' && declaration.childForFieldName('alternative')
|
|
2075
|
+
? readPattern(pattern, declaration.childForFieldName('value')) : null;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (ancestor.type === 'for_expression' &&
|
|
2080
|
+
patternContainsIdentifier(ancestor.childForFieldName('pattern'), receiver)) return null;
|
|
2081
|
+
if (ancestor.type === 'closure_expression') {
|
|
2082
|
+
const params = ancestor.childForFieldName('parameters');
|
|
2083
|
+
if (params && patternContainsIdentifier(params, receiver)) break;
|
|
2084
|
+
continue; // captured binding from an outer match arm
|
|
2085
|
+
}
|
|
2086
|
+
let matchValue, root;
|
|
2087
|
+
if (ancestor.type === 'match_arm') {
|
|
2088
|
+
let matchExpression = ancestor.parent;
|
|
2089
|
+
while (matchExpression && matchExpression.type !== 'match_expression' &&
|
|
2090
|
+
matchExpression.type !== 'function_item') matchExpression = matchExpression.parent;
|
|
2091
|
+
matchValue = matchExpression?.type === 'match_expression'
|
|
2092
|
+
? matchExpression.childForFieldName('value') : null;
|
|
2093
|
+
root = ancestor.childForFieldName('pattern');
|
|
2094
|
+
} else if (['if_expression', 'while_expression'].includes(ancestor.type)) {
|
|
2095
|
+
const body = ancestor.childForFieldName(ancestor.type === 'if_expression' ? 'consequence' : 'body');
|
|
2096
|
+
const condition = ancestor.childForFieldName('condition');
|
|
2097
|
+
if (!body || node.startIndex < body.startIndex || node.endIndex > body.endIndex ||
|
|
2098
|
+
condition?.type !== 'let_condition') continue;
|
|
2099
|
+
matchValue = condition.childForFieldName('value');
|
|
2100
|
+
root = condition.childForFieldName('pattern');
|
|
2101
|
+
} else continue;
|
|
2102
|
+
if (!patternContainsIdentifier(root, receiver)) continue;
|
|
2103
|
+
return readPattern(root, matchValue);
|
|
2000
2104
|
}
|
|
2001
2105
|
return null;
|
|
2002
2106
|
}
|
|
@@ -2067,38 +2171,6 @@ function findCallsInCode(code, parser) {
|
|
|
2067
2171
|
return ['function_item', 'closure_expression'].includes(node.type);
|
|
2068
2172
|
};
|
|
2069
2173
|
|
|
2070
|
-
// Extract the base type name from a Rust type node (strips &, &mut, Box<>, etc.)
|
|
2071
|
-
const extractTypeName = (typeNode) => {
|
|
2072
|
-
if (!typeNode) return null;
|
|
2073
|
-
if (typeNode.type === 'type_identifier' ||
|
|
2074
|
-
typeNode.type === 'primitive_type') {
|
|
2075
|
-
return typeNode.text;
|
|
2076
|
-
}
|
|
2077
|
-
if (typeNode.type === 'reference_type') {
|
|
2078
|
-
// &Filter or &mut Filter -> Filter
|
|
2079
|
-
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
2080
|
-
const r = extractTypeName(typeNode.namedChild(i));
|
|
2081
|
-
if (r) return r;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
if (typeNode.type === 'abstract_type' || typeNode.type === 'dynamic_type') {
|
|
2085
|
-
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
2086
|
-
const r = extractTypeName(typeNode.namedChild(i));
|
|
2087
|
-
if (r) return r;
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
if (typeNode.type === 'generic_type') {
|
|
2091
|
-
// Box<Filter> -> Filter (or get the outer type)
|
|
2092
|
-
return extractTypeName(typeNode.namedChild(0));
|
|
2093
|
-
}
|
|
2094
|
-
if (typeNode.type === 'scoped_type_identifier') {
|
|
2095
|
-
// module::Type -> Type
|
|
2096
|
-
const nameNode = typeNode.childForFieldName('name');
|
|
2097
|
-
return nameNode?.text || null;
|
|
2098
|
-
}
|
|
2099
|
-
return null;
|
|
2100
|
-
};
|
|
2101
|
-
|
|
2102
2174
|
const extractTypeQualifier = (typeNode) => {
|
|
2103
2175
|
if (!typeNode) return null;
|
|
2104
2176
|
if (typeNode.type === 'reference_type') {
|
|
@@ -2120,7 +2192,7 @@ function findCallsInCode(code, parser) {
|
|
|
2120
2192
|
|
|
2121
2193
|
// Build type map from function parameters (including self receiver for impl methods)
|
|
2122
2194
|
const buildScopeTypeMap = (node) => {
|
|
2123
|
-
const typeMap = new
|
|
2195
|
+
const typeMap = new ReceiverTypeMap();
|
|
2124
2196
|
typeMap.qualifiers = new Map();
|
|
2125
2197
|
typeMap.iteratorItems = new Map();
|
|
2126
2198
|
typeMap.annotationTexts = new Map();
|
|
@@ -2154,7 +2226,7 @@ function findCallsInCode(code, parser) {
|
|
|
2154
2226
|
// Pattern can be identifier or _
|
|
2155
2227
|
const name = patternNode.type === 'identifier' ? patternNode.text : null;
|
|
2156
2228
|
if (name) {
|
|
2157
|
-
typeMap.set(name, typeName);
|
|
2229
|
+
typeMap.set(name, typeName, 'annotation', param);
|
|
2158
2230
|
typeMap.annotationTexts.set(name, typeNode.text);
|
|
2159
2231
|
if (qualifier) typeMap.qualifiers.set(name, qualifier);
|
|
2160
2232
|
if (iteratorItem) typeMap.iteratorItems.set(name, iteratorItem);
|
|
@@ -2253,13 +2325,21 @@ function findCallsInCode(code, parser) {
|
|
|
2253
2325
|
!!flowEventAt(node, varName)?.invalidated;
|
|
2254
2326
|
|
|
2255
2327
|
// Look up variable type from scope chain
|
|
2256
|
-
const getReceiverType = (varName, atNode) => {
|
|
2328
|
+
const getReceiverType = (varName, atNode, evidence = false) => {
|
|
2257
2329
|
if (atNode && patternShadowsAt(atNode, varName)) return undefined;
|
|
2258
2330
|
const flow = flowEventAt(atNode, varName);
|
|
2259
|
-
if (flow?.type)
|
|
2331
|
+
if (flow?.type) {
|
|
2332
|
+
const { until, ...origin } = flow;
|
|
2333
|
+
return evidence ? { receiverTypeSource: 'flow',
|
|
2334
|
+
receiverTypeEvidence: { ...typeOrigin('flow', atNode), ...origin,
|
|
2335
|
+
...(Number.isFinite(until) && { until }) },
|
|
2336
|
+
...(flow.qualifier && { receiverTypeQualifier: flow.qualifier }) } : flow.type;
|
|
2337
|
+
}
|
|
2260
2338
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
2261
2339
|
const typeMap = scopeTypes.get(functionStack[i].startLine);
|
|
2262
|
-
if (typeMap?.has(varName)) return typeMap.
|
|
2340
|
+
if (typeMap?.has(varName)) return evidence ? { ...typeMap.fields(varName),
|
|
2341
|
+
...(typeMap.qualifiers?.has(varName) && { receiverTypeQualifier: typeMap.qualifiers.get(varName) }) }
|
|
2342
|
+
: typeMap.get(varName);
|
|
2263
2343
|
if (typeMap?.boundNames?.has(varName)) return undefined;
|
|
2264
2344
|
}
|
|
2265
2345
|
return undefined;
|
|
@@ -2363,15 +2443,31 @@ function findCallsInCode(code, parser) {
|
|
|
2363
2443
|
};
|
|
2364
2444
|
};
|
|
2365
2445
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
return
|
|
2446
|
+
const copiedBindingType = (value, assignment, retainedName = null) => {
|
|
2447
|
+
let expression = value;
|
|
2448
|
+
while (expression && ['reference_expression', 'parenthesized_expression'].includes(expression.type)) {
|
|
2449
|
+
expression = expression.childForFieldName('value') || expression.namedChildren.at(-1);
|
|
2450
|
+
}
|
|
2451
|
+
const dereferenced = !retainedName && expression?.type === 'unary_expression' && expression.child(0)?.text === '*';
|
|
2452
|
+
if (dereferenced) expression = expression.namedChild(0);
|
|
2453
|
+
const name = retainedName || (expression?.type === 'identifier' ? expression.text : null);
|
|
2454
|
+
if (!name) return null;
|
|
2455
|
+
const type = getReceiverType(name, assignment);
|
|
2456
|
+
const facts = type && getReceiverType(name, assignment, true);
|
|
2457
|
+
if (!facts?.receiverTypeEvidence || ['unknown', 'guess'].includes(facts.receiverTypeSource)) return null;
|
|
2458
|
+
const annotation = dereferenced && getReceiverAnnotationText(name, assignment);
|
|
2459
|
+
// Deref on a custom owner can change the type. Only an actual
|
|
2460
|
+
// reference annotation proves the built-in `&*reference` reborrow.
|
|
2461
|
+
if (dereferenced && (facts.receiverTypeSource !== 'annotation' || !annotation?.trim().startsWith('&'))) return null;
|
|
2462
|
+
const qualifier = getReceiverTypeQualifier(name, assignment);
|
|
2463
|
+
return { type, ...(qualifier && { qualifier }), aliasBinding: {
|
|
2464
|
+
variable: name, target: assignment.childForFieldName(
|
|
2465
|
+
assignment.type === 'let_declaration' ? 'pattern' : 'left')?.text,
|
|
2466
|
+
type, origin: facts.receiverTypeEvidence,
|
|
2467
|
+
assignment: typeOrigin('flow', assignment),
|
|
2468
|
+
...(dereferenced && { referenceAnnotation: annotation }),
|
|
2469
|
+
kind: retainedName ? 'reassignment' : value.type === 'identifier' ? 'copy' : 'borrow',
|
|
2470
|
+
} };
|
|
2375
2471
|
};
|
|
2376
2472
|
|
|
2377
2473
|
const closureContractSource = (node) => {
|
|
@@ -2495,7 +2591,11 @@ function findCallsInCode(code, parser) {
|
|
|
2495
2591
|
at: node.endIndex,
|
|
2496
2592
|
until,
|
|
2497
2593
|
...(() => {
|
|
2498
|
-
|
|
2594
|
+
// Rust assignment preserves a variable's static
|
|
2595
|
+
// type; a fresh `let` may shadow it with another.
|
|
2596
|
+
const inferred = copiedBindingType(value, node,
|
|
2597
|
+
node.type === 'assignment_expression' ? pattern.text : null) ||
|
|
2598
|
+
matchBindingType(value, node);
|
|
2499
2599
|
return inferred
|
|
2500
2600
|
? { invalidated: false, ...inferred }
|
|
2501
2601
|
: { invalidated: !valueHasFlowProducer(value) };
|
|
@@ -2761,7 +2861,7 @@ function findCallsInCode(code, parser) {
|
|
|
2761
2861
|
callEnd: node.endIndex,
|
|
2762
2862
|
isMethod: true,
|
|
2763
2863
|
receiver,
|
|
2764
|
-
...(receiverType && { receiverType }),
|
|
2864
|
+
...(receiverType && { receiverType, ...(getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
2765
2865
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
2766
2866
|
...(receiverIteratorItemType && { receiverIteratorItemType }),
|
|
2767
2867
|
...(receiverPatternShadow && { receiverPatternShadow: true }),
|
|
@@ -2987,7 +3087,7 @@ function findCallsInCode(code, parser) {
|
|
|
2987
3087
|
line: fieldNode.startPosition.row + 1,
|
|
2988
3088
|
isMethod: true,
|
|
2989
3089
|
receiver,
|
|
2990
|
-
...(receiverType && { receiverType }),
|
|
3090
|
+
...(receiverType && { receiverType, ...(getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
2991
3091
|
...(receiverPatternShadow && { receiverPatternShadow: true }),
|
|
2992
3092
|
...(receiverFlowInvalidated && { receiverFlowInvalidated: true }),
|
|
2993
3093
|
isFunctionReference: true,
|
|
@@ -3061,7 +3161,7 @@ function findCallsInCode(code, parser) {
|
|
|
3061
3161
|
}
|
|
3062
3162
|
}
|
|
3063
3163
|
if (typeName) {
|
|
3064
|
-
typeMap.set(varName, typeName);
|
|
3164
|
+
typeMap.set(varName, typeName, typeAnnotation ? 'annotation' : valueNode?.type === 'call_expression' ? 'guess' : 'constructor', typeAnnotation || valueNode);
|
|
3065
3165
|
if (typeQualifier) typeMap.qualifiers.set(varName, typeQualifier);
|
|
3066
3166
|
}
|
|
3067
3167
|
}
|
|
@@ -3121,7 +3221,7 @@ function findImportsInCode(code, parser) {
|
|
|
3121
3221
|
const right = String(suffix || '').replace(/^::/, '');
|
|
3122
3222
|
return left && right ? `${left}::${right}` : left || right;
|
|
3123
3223
|
};
|
|
3124
|
-
const addLeaf = (module, localName, type = 'use', dynamic = false, line) => {
|
|
3224
|
+
const addLeaf = (module, localName, type = 'use', dynamic = false, line, rename = null) => {
|
|
3125
3225
|
if (!module || !localName) return;
|
|
3126
3226
|
imports.push({
|
|
3127
3227
|
module,
|
|
@@ -3129,6 +3229,7 @@ function findImportsInCode(code, parser) {
|
|
|
3129
3229
|
type,
|
|
3130
3230
|
dynamic,
|
|
3131
3231
|
line,
|
|
3232
|
+
...(rename && { renames: [rename] }),
|
|
3132
3233
|
});
|
|
3133
3234
|
};
|
|
3134
3235
|
const collectUseTree = (node, prefix, line) => {
|
|
@@ -3150,13 +3251,20 @@ function findImportsInCode(code, parser) {
|
|
|
3150
3251
|
const pathNode = node.namedChild(0);
|
|
3151
3252
|
const aliasNode = node.childForFieldName('alias') || node.namedChild(1);
|
|
3152
3253
|
if (pathNode && aliasNode) {
|
|
3153
|
-
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3154
|
-
'use', false, line);
|
|
3155
3254
|
// fix #353: `use alpha::widget as renamed; renamed()` — the
|
|
3156
3255
|
// alias pairing feeds findCallers' import-rename surface
|
|
3157
3256
|
// (calledAs), exactly like Python/JS `import x as y`.
|
|
3257
|
+
// fix #357: `renames` pairs the alias with ITS record so
|
|
3258
|
+
// createImportBindings emits {name: original, alias: local}
|
|
3259
|
+
// (the #348 Python shape); two renames of one source name
|
|
3260
|
+
// from different modules each pair with their own module
|
|
3261
|
+
// instead of both bindings matching both calls. `names`
|
|
3262
|
+
// keeps the local spelling (parser contract, imports command).
|
|
3158
3263
|
const original = String(pathNode.text).split('::').pop();
|
|
3159
|
-
|
|
3264
|
+
const renamed = original && original !== aliasNode.text && original !== 'self';
|
|
3265
|
+
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3266
|
+
'use', false, line, renamed ? { original, local: aliasNode.text } : null);
|
|
3267
|
+
if (renamed) {
|
|
3160
3268
|
if (!imports.aliases) imports.aliases = [];
|
|
3161
3269
|
imports.aliases.push({ original, local: aliasNode.text });
|
|
3162
3270
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Data-only AST witness. Never retain a tree-sitter node in a call cache. */
|
|
4
|
+
function typeOrigin(source = 'unknown', node = null) {
|
|
5
|
+
return {
|
|
6
|
+
source,
|
|
7
|
+
...(node && {
|
|
8
|
+
line: node.startPosition.row + 1,
|
|
9
|
+
column: node.startPosition.column,
|
|
10
|
+
start: node.startIndex,
|
|
11
|
+
end: node.endIndex,
|
|
12
|
+
nodeType: node.type,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A string-valued type map with independent provenance. Existing inference
|
|
19
|
+
* keeps its Map semantics; adding witnesses must not change classification.
|
|
20
|
+
* Reassignment, deletion, and scope restoration update the witness together
|
|
21
|
+
* with the inferred type so a stale constructor can never justify a new value.
|
|
22
|
+
*/
|
|
23
|
+
class ReceiverTypeMap extends Map {
|
|
24
|
+
constructor(entries) {
|
|
25
|
+
super();
|
|
26
|
+
this.origins = new Map();
|
|
27
|
+
if (entries) this.restore(entries);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(name, type, source = 'unknown', node = null) {
|
|
31
|
+
super.set(name, type);
|
|
32
|
+
this.origins.set(name, typeof source === 'object' ? { ...source } : typeOrigin(source, node));
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
delete(name) {
|
|
37
|
+
this.origins.delete(name);
|
|
38
|
+
return super.delete(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear() {
|
|
42
|
+
this.origins.clear();
|
|
43
|
+
super.clear();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
restore(entries) {
|
|
47
|
+
this.clear();
|
|
48
|
+
for (const [name, type] of entries) {
|
|
49
|
+
this.set(name, type, entries.origins?.get(name) || 'unknown');
|
|
50
|
+
}
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fields(name, type = this.get(name)) {
|
|
55
|
+
const origin = type && type === this.get(name) ? this.origins.get(name) : null;
|
|
56
|
+
return {
|
|
57
|
+
receiverTypeSource: origin?.source || 'unknown',
|
|
58
|
+
...(origin && { receiverTypeEvidence: { ...origin, name, type } }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { ReceiverTypeMap, typeOrigin };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.6",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"version": "node scripts/sync-server-version.js && git add server.json",
|
|
13
|
-
"test": "node --test test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
13
|
+
"test": "node --test test/evidence-provenance.test.js test/provenance-unit.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
|
|
14
14
|
"benchmark:agent": "node test/agent-public-surface-benchmark.js",
|
|
15
15
|
"benchmark:agent:gate": "node test/agent-public-surface-benchmark.js --gate",
|
|
16
16
|
"benchmark:agent:legacy": "node test/agent-understanding-benchmark.js",
|