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/javascript.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* interfaces, type aliases, enums, and state objects.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
const {
|
|
9
12
|
traverseTree,
|
|
10
13
|
traverseTreeCached,
|
|
@@ -1898,6 +1901,23 @@ function tsTypeName(node) {
|
|
|
1898
1901
|
}
|
|
1899
1902
|
}
|
|
1900
1903
|
|
|
1904
|
+
function tsArrayElement(node) {
|
|
1905
|
+
if (!node) return null;
|
|
1906
|
+
if (['type_annotation', 'parenthesized_type', 'readonly_type'].includes(node.type)) {
|
|
1907
|
+
return tsArrayElement(node.namedChild(0));
|
|
1908
|
+
}
|
|
1909
|
+
if (node.type === 'union_type') {
|
|
1910
|
+
const present = node.namedChildren.filter(child =>
|
|
1911
|
+
!(child.type === 'literal_type' && ['null', 'undefined'].includes(child.text)));
|
|
1912
|
+
return present.length === 1 ? tsArrayElement(present[0]) : null;
|
|
1913
|
+
}
|
|
1914
|
+
if (node.type !== 'array_type') return null;
|
|
1915
|
+
const item = node.namedChild(0);
|
|
1916
|
+
if (!['type_identifier', 'nested_type_identifier', 'generic_type', 'predefined_type'].includes(item?.type)) return null;
|
|
1917
|
+
const type = tsTypeName(item);
|
|
1918
|
+
return type ? { type, qualifier: tsTypeQualifier(item), node: item } : null;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1901
1921
|
/**
|
|
1902
1922
|
* Variable receiving this call's result: `const x = foo()` / `x = await foo()`
|
|
1903
1923
|
* → 'x'. Identifier targets only. Compared by node id — tree-sitter wrapper
|
|
@@ -1990,6 +2010,7 @@ function findCallsInCode(code, parser) {
|
|
|
1990
2010
|
const moduleCompositions = new Map();
|
|
1991
2011
|
const unsafeModuleCompositions = new Set();
|
|
1992
2012
|
const namespaceAliases = new Set();
|
|
2013
|
+
const arrayAnnotationNames = new Set();
|
|
1993
2014
|
const accessRoot = (node) => {
|
|
1994
2015
|
let current = node;
|
|
1995
2016
|
while (current && (current.type === 'member_expression' ||
|
|
@@ -1999,6 +2020,12 @@ function findCallsInCode(code, parser) {
|
|
|
1999
2020
|
return current?.type === 'identifier' ? current.text : undefined;
|
|
2000
2021
|
};
|
|
2001
2022
|
traverseTreeCached(tree.rootNode, node => {
|
|
2023
|
+
if (['variable_declarator', 'required_parameter', 'optional_parameter'].includes(node.type)) {
|
|
2024
|
+
const name = node.childForFieldName('name') || node.childForFieldName('pattern');
|
|
2025
|
+
if (name?.type === 'identifier' && tsArrayElement(node.childForFieldName('type'))) {
|
|
2026
|
+
arrayAnnotationNames.add(name.text);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2002
2029
|
if (node.type === 'namespace_import') {
|
|
2003
2030
|
const identifier = node.namedChild(0);
|
|
2004
2031
|
if (identifier?.type === 'identifier') namespaceAliases.add(identifier.text);
|
|
@@ -2107,7 +2134,7 @@ function findCallsInCode(code, parser) {
|
|
|
2107
2134
|
// false external edges and caller/callee disagreement.
|
|
2108
2135
|
const aliases = new Map(); // aliasName -> [{ target, declarationIndex, scopeStart, scopeEnd }]
|
|
2109
2136
|
const nonCallableNames = new Set(); // Track names assigned non-callable values
|
|
2110
|
-
const localVarTypes = new
|
|
2137
|
+
const localVarTypes = new ReceiverTypeMap(); // Track local variable types: varName -> typeName (for receiverType inference)
|
|
2111
2138
|
const localVarTypeQualifiers = new Map(); // qualifier provenance for new ns.Type()
|
|
2112
2139
|
// Names whose type came from a DECLARED annotation (TS `x: Foo` / typed
|
|
2113
2140
|
// params). The compiler enforces assignability for these, so reassignment
|
|
@@ -2361,6 +2388,79 @@ function findCallsInCode(code, parser) {
|
|
|
2361
2388
|
return false;
|
|
2362
2389
|
};
|
|
2363
2390
|
|
|
2391
|
+
// Resolve the actual lexical declaration for an indexed receiver. Keep
|
|
2392
|
+
// untyped bindings too: a shadow must stop an outer annotation from
|
|
2393
|
+
// leaking into a nested function or block. This lookup is cached per
|
|
2394
|
+
// spelling and used only for bracket receivers, not every ordinary call.
|
|
2395
|
+
const indexedBindings = new Map();
|
|
2396
|
+
const indexedBinding = (name, site) => {
|
|
2397
|
+
if (!indexedBindings.has(name)) {
|
|
2398
|
+
const records = [];
|
|
2399
|
+
const add = (pattern, declaration, scope) => {
|
|
2400
|
+
if (!scope || !_patternDeclaresName(pattern, name)) return;
|
|
2401
|
+
records.push({ declaration, scope, type: pattern?.type === 'identifier'
|
|
2402
|
+
? declaration.childForFieldName('type') : null });
|
|
2403
|
+
};
|
|
2404
|
+
traverseTree(tree.rootNode, current => {
|
|
2405
|
+
if (current.type === 'variable_declarator') {
|
|
2406
|
+
add(current.childForFieldName('name'), current, aliasScope(current));
|
|
2407
|
+
} else if (isFunctionNode(current)) {
|
|
2408
|
+
const parameters = current.childForFieldName('parameters');
|
|
2409
|
+
for (const parameter of parameters?.namedChildren || []) {
|
|
2410
|
+
const pattern = parameter.childForFieldName('pattern') ||
|
|
2411
|
+
parameter.childForFieldName('name') || parameter;
|
|
2412
|
+
add(pattern, parameter, current);
|
|
2413
|
+
}
|
|
2414
|
+
const lone = current.childForFieldName('parameter');
|
|
2415
|
+
if (lone) add(lone, lone, current);
|
|
2416
|
+
const fnName = current.childForFieldName('name');
|
|
2417
|
+
if (fnName?.text === name) add(fnName, current,
|
|
2418
|
+
current.type === 'function_declaration' ? current.parent : current);
|
|
2419
|
+
} else if (current.type === 'catch_clause') {
|
|
2420
|
+
add(current.childForFieldName('parameter'), current, current);
|
|
2421
|
+
}
|
|
2422
|
+
return true;
|
|
2423
|
+
});
|
|
2424
|
+
indexedBindings.set(name, records);
|
|
2425
|
+
}
|
|
2426
|
+
const matches = indexedBindings.get(name).filter(record =>
|
|
2427
|
+
record.scope.startIndex <= site.startIndex && record.scope.endIndex >= site.endIndex)
|
|
2428
|
+
.sort((a, b) => (a.scope.endIndex - a.scope.startIndex) - (b.scope.endIndex - b.scope.startIndex));
|
|
2429
|
+
const nearest = matches[0];
|
|
2430
|
+
if (!nearest || nearest.declaration.startIndex > site.startIndex ||
|
|
2431
|
+
(matches[1] && matches[1].scope.id === nearest.scope.id)) return null;
|
|
2432
|
+
return nearest;
|
|
2433
|
+
};
|
|
2434
|
+
const indexedArrayReceiver = object => {
|
|
2435
|
+
if (object?.type !== 'subscript_expression') return null;
|
|
2436
|
+
const root = object.childForFieldName('object');
|
|
2437
|
+
const offset = object.childForFieldName('index');
|
|
2438
|
+
if (root?.type !== 'identifier' || !arrayAnnotationNames.has(root.text) || !offset) return null;
|
|
2439
|
+
let numeric = offset.type === 'number';
|
|
2440
|
+
if (offset.type === 'identifier') {
|
|
2441
|
+
const indexBinding = indexedBinding(offset.text, object);
|
|
2442
|
+
const annotation = indexBinding?.type?.namedChild(0);
|
|
2443
|
+
numeric = annotation?.type === 'predefined_type' && annotation.text === 'number';
|
|
2444
|
+
if (!annotation && indexBinding?.declaration.childForFieldName('value')?.type === 'number') {
|
|
2445
|
+
numeric = localVarTypes.get(offset.text) === 'Number';
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (!numeric) return null;
|
|
2449
|
+
const binding = indexedBinding(root.text, object);
|
|
2450
|
+
const element = tsArrayElement(binding?.type);
|
|
2451
|
+
if (element && !element.qualifier) {
|
|
2452
|
+
for (let scope = binding.declaration.parent; scope; scope = scope.parent) {
|
|
2453
|
+
const parameters = scope.childForFieldName('type_parameters');
|
|
2454
|
+
if (parameters?.namedChildren.some(parameter =>
|
|
2455
|
+
(parameter.childForFieldName('name')?.text || parameter.text) === element.type)) return null;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return element && { ...element,
|
|
2459
|
+
evidence: { ...typeOrigin('annotation', binding.type), projection: 'array-element',
|
|
2460
|
+
container: root.text, elementType: element.node.text,
|
|
2461
|
+
index: { start: offset.startIndex, end: offset.endIndex, nodeType: offset.type } } };
|
|
2462
|
+
};
|
|
2463
|
+
|
|
2364
2464
|
// fix #203: does a declaration node declare `name` (including nested destructuring)?
|
|
2365
2465
|
const _declaresName = (declNode, name) => {
|
|
2366
2466
|
for (let i = 0; i < declNode.namedChildCount; i++) {
|
|
@@ -2549,7 +2649,7 @@ function findCallsInCode(code, parser) {
|
|
|
2549
2649
|
endLine: node.endPosition.row + 1
|
|
2550
2650
|
});
|
|
2551
2651
|
// Save localVarTypes so inner declarations don't leak to sibling functions
|
|
2552
|
-
localVarTypesStack.push(new
|
|
2652
|
+
localVarTypesStack.push(new ReceiverTypeMap(localVarTypes));
|
|
2553
2653
|
localVarTypeQualifiersStack.push(new Map(localVarTypeQualifiers));
|
|
2554
2654
|
declaredTypeVarsStack.push(new Set(declaredTypeVars));
|
|
2555
2655
|
}
|
|
@@ -2603,7 +2703,7 @@ function findCallsInCode(code, parser) {
|
|
|
2603
2703
|
// Infer type: const x = new Foo() / new pkg.Foo() → x is Foo
|
|
2604
2704
|
const ctorName = jsConstructorTypeName(initNode.childForFieldName('constructor'));
|
|
2605
2705
|
if (ctorName) {
|
|
2606
|
-
localVarTypes.set(nameNode.text, ctorName);
|
|
2706
|
+
localVarTypes.set(nameNode.text, ctorName, 'constructor', initNode);
|
|
2607
2707
|
const qualifier = jsConstructorTypeQualifier(
|
|
2608
2708
|
initNode.childForFieldName('constructor'));
|
|
2609
2709
|
if (qualifier) localVarTypeQualifiers.set(nameNode.text, qualifier);
|
|
@@ -2619,7 +2719,7 @@ function findCallsInCode(code, parser) {
|
|
|
2619
2719
|
? typeNode.namedChild(0) : typeNode;
|
|
2620
2720
|
const typeName = tsTypeName(typeId);
|
|
2621
2721
|
if (typeName) {
|
|
2622
|
-
localVarTypes.set(nameNode.text, typeName);
|
|
2722
|
+
localVarTypes.set(nameNode.text, typeName, 'annotation', typeNode);
|
|
2623
2723
|
declaredTypeVars.add(nameNode.text);
|
|
2624
2724
|
const annotationQualifier = tsTypeQualifier(typeId);
|
|
2625
2725
|
if (annotationQualifier) {
|
|
@@ -2632,7 +2732,7 @@ function findCallsInCode(code, parser) {
|
|
|
2632
2732
|
// Literal declaration types the variable (fix #262):
|
|
2633
2733
|
// `const lines = []` → Array. Annotation, when present,
|
|
2634
2734
|
// wins (the branch above).
|
|
2635
|
-
localVarTypes.set(nameNode.text, JS_LITERAL_ASSIGN_TYPES[initNode.type]);
|
|
2735
|
+
localVarTypes.set(nameNode.text, JS_LITERAL_ASSIGN_TYPES[initNode.type], 'literal', initNode);
|
|
2636
2736
|
}
|
|
2637
2737
|
}
|
|
2638
2738
|
}
|
|
@@ -2645,7 +2745,7 @@ function findCallsInCode(code, parser) {
|
|
|
2645
2745
|
const inner = typeNode.type === 'type_annotation' ? typeNode.namedChild(0) : typeNode;
|
|
2646
2746
|
const typeName = tsTypeName(inner);
|
|
2647
2747
|
if (typeName) {
|
|
2648
|
-
localVarTypes.set(pat.text, typeName);
|
|
2748
|
+
localVarTypes.set(pat.text, typeName, 'annotation', node);
|
|
2649
2749
|
declaredTypeVars.add(pat.text);
|
|
2650
2750
|
const annotationQualifier = tsTypeQualifier(inner);
|
|
2651
2751
|
if (annotationQualifier) {
|
|
@@ -2666,7 +2766,7 @@ function findCallsInCode(code, parser) {
|
|
|
2666
2766
|
nonCallableNames.add(left.text);
|
|
2667
2767
|
const ctorName = jsConstructorTypeName(right.childForFieldName('constructor'));
|
|
2668
2768
|
if (ctorName && !isConditionalReassignment(node)) {
|
|
2669
|
-
localVarTypes.set(left.text, ctorName);
|
|
2769
|
+
localVarTypes.set(left.text, ctorName, 'constructor', right);
|
|
2670
2770
|
const qualifier = jsConstructorTypeQualifier(
|
|
2671
2771
|
right.childForFieldName('constructor'));
|
|
2672
2772
|
if (qualifier) localVarTypeQualifiers.set(left.text, qualifier);
|
|
@@ -2678,7 +2778,7 @@ function findCallsInCode(code, parser) {
|
|
|
2678
2778
|
} else if (right && JS_LITERAL_ASSIGN_TYPES[right.type]) {
|
|
2679
2779
|
// Literal reassignment re-types the variable (fix #262)
|
|
2680
2780
|
if (!declaredTypeVars.has(left.text)) {
|
|
2681
|
-
localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type]);
|
|
2781
|
+
localVarTypes.set(left.text, JS_LITERAL_ASSIGN_TYPES[right.type], 'literal', right);
|
|
2682
2782
|
localVarTypeQualifiers.delete(left.text);
|
|
2683
2783
|
}
|
|
2684
2784
|
} else if (localVarTypes.has(left.text) && !declaredTypeVars.has(left.text)) {
|
|
@@ -2861,7 +2961,11 @@ function findCallsInCode(code, parser) {
|
|
|
2861
2961
|
isMethod: true,
|
|
2862
2962
|
boundCall: true,
|
|
2863
2963
|
receiver: boundReceiver,
|
|
2864
|
-
...(boundReceiverType && { receiverType: boundReceiverType
|
|
2964
|
+
...(boundReceiverType && { receiverType: boundReceiverType,
|
|
2965
|
+
...(prototypeOwner ? {
|
|
2966
|
+
receiverTypeSource: 'type-qualified',
|
|
2967
|
+
receiverTypeEvidence: typeOrigin('type-qualified', innerObj),
|
|
2968
|
+
} : localVarTypes.fields(innerObj?.text, boundReceiverType)) }),
|
|
2865
2969
|
...(innerObj?.type === 'identifier' &&
|
|
2866
2970
|
localVarTypeQualifiers.has(innerObj.text) && {
|
|
2867
2971
|
receiverTypeQualifier: localVarTypeQualifiers.get(innerObj.text),
|
|
@@ -2962,10 +3066,11 @@ function findCallsInCode(code, parser) {
|
|
|
2962
3066
|
const constructedReceiverQualifier = objNode?.type === 'new_expression'
|
|
2963
3067
|
? jsConstructorTypeQualifier(objNode.childForFieldName('constructor'))
|
|
2964
3068
|
: undefined;
|
|
2965
|
-
const
|
|
3069
|
+
const indexedReceiver = indexedArrayReceiver(objNode);
|
|
3070
|
+
const receiverType = indexedReceiver?.type || (receiver
|
|
2966
3071
|
? localVarTypes.get(receiver)
|
|
2967
3072
|
: (constructedReceiverType ||
|
|
2968
|
-
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined));
|
|
3073
|
+
(objNode ? JS_LITERAL_RECEIVER_TYPES[objNode.type] : undefined)));
|
|
2969
3074
|
// Module receiver (ns.helper()) — unless locally shadowed
|
|
2970
3075
|
// by a typed instance binding
|
|
2971
3076
|
const receiverModuleSpecifier = jsLiteralRequireModule(objNode);
|
|
@@ -3001,10 +3106,17 @@ function findCallsInCode(code, parser) {
|
|
|
3001
3106
|
callEnd: node.endIndex,
|
|
3002
3107
|
isMethod: true,
|
|
3003
3108
|
receiver,
|
|
3004
|
-
...(receiverType && { receiverType
|
|
3005
|
-
|
|
3109
|
+
...(receiverType && { receiverType,
|
|
3110
|
+
...(indexedReceiver ? { receiverTypeSource: 'annotation',
|
|
3111
|
+
receiverTypeEvidence: indexedReceiver.evidence }
|
|
3112
|
+
: receiver ? localVarTypes.fields(receiver, receiverType) : {
|
|
3113
|
+
receiverTypeSource: constructedReceiverType ? 'constructor' : 'literal',
|
|
3114
|
+
receiverTypeEvidence: typeOrigin(constructedReceiverType ? 'constructor' : 'literal', objNode),
|
|
3115
|
+
}),
|
|
3116
|
+
}),
|
|
3117
|
+
...((indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3006
3118
|
(receiver && localVarTypeQualifiers.get(receiver))) && {
|
|
3007
|
-
receiverTypeQualifier: constructedReceiverQualifier ||
|
|
3119
|
+
receiverTypeQualifier: indexedReceiver?.qualifier || constructedReceiverQualifier ||
|
|
3008
3120
|
localVarTypeQualifiers.get(receiver),
|
|
3009
3121
|
}),
|
|
3010
3122
|
...(receiverIsModule && { receiverIsModule: true }),
|
|
@@ -3087,7 +3199,7 @@ function findCallsInCode(code, parser) {
|
|
|
3087
3199
|
line: arg.startPosition.row + 1,
|
|
3088
3200
|
isMethod: true,
|
|
3089
3201
|
receiver: hofRecv,
|
|
3090
|
-
...(hofRecvType && { receiverType: hofRecvType }),
|
|
3202
|
+
...(hofRecvType && { receiverType: hofRecvType, ...localVarTypes.fields(hofRecv, hofRecvType) }),
|
|
3091
3203
|
isFunctionReference: true,
|
|
3092
3204
|
enclosingFunction
|
|
3093
3205
|
});
|
|
@@ -3135,7 +3247,7 @@ function findCallsInCode(code, parser) {
|
|
|
3135
3247
|
line: arg.startPosition.row + 1,
|
|
3136
3248
|
isMethod: true,
|
|
3137
3249
|
receiver: mvObj.text,
|
|
3138
|
-
...(mvType && { receiverType: mvType }),
|
|
3250
|
+
...(mvType && { receiverType: mvType, ...localVarTypes.fields(mvObj.text, mvType) }),
|
|
3139
3251
|
isFunctionReference: true,
|
|
3140
3252
|
enclosingFunction
|
|
3141
3253
|
});
|
|
@@ -3290,8 +3402,7 @@ function findCallsInCode(code, parser) {
|
|
|
3290
3402
|
// Restore localVarTypes to pre-function state
|
|
3291
3403
|
const saved = localVarTypesStack.pop();
|
|
3292
3404
|
if (saved) {
|
|
3293
|
-
localVarTypes.
|
|
3294
|
-
for (const [k, v] of saved) localVarTypes.set(k, v);
|
|
3405
|
+
localVarTypes.restore(saved);
|
|
3295
3406
|
}
|
|
3296
3407
|
const savedQualifiers = localVarTypeQualifiersStack.pop();
|
|
3297
3408
|
if (savedQualifiers) {
|