ucn 5.5.1 → 5.5.3
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 +35 -9
- package/.claude/skills/ucn/references/commands.md +40 -18
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +32 -21
- package/cli/index.js +2 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/bridge.js +15 -65
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/client-receivers.js +189 -0
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +61 -16
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +2 -0
- package/core/output/reporting.js +12 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +270 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/request-helpers.js +63 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +3 -3
package/core/analysis.js
CHANGED
|
@@ -14,7 +14,7 @@ const { UcnError } = require('./errors');
|
|
|
14
14
|
const { parse } = require('./parser');
|
|
15
15
|
const { detectLanguage, langTraits } = require('../languages');
|
|
16
16
|
const { NON_CALLABLE_TYPES, addTestExclusions, countTextBlindspots,
|
|
17
|
-
codeUnitCompare, formatSymbolHandle } = require('./shared');
|
|
17
|
+
codeUnitCompare, formatSymbolHandle, isValueDefinition } = require('./shared');
|
|
18
18
|
const { isTestFile } = require('./discovery');
|
|
19
19
|
// Entry-point detection loads on first use (fix #375).
|
|
20
20
|
let entrypointsModule = null;
|
|
@@ -346,6 +346,23 @@ function callNotResolvedEntries(index, account, options = {}) {
|
|
|
346
346
|
* @param {object} options - { file, className, includeMethods, includeUncertain, exclude, minConfidence }
|
|
347
347
|
* @returns {object|null}
|
|
348
348
|
*/
|
|
349
|
+
/**
|
|
350
|
+
* The hint for a constructor handle with no callers whose declaring class
|
|
351
|
+
* is unresolved (fix #421): constructions name the class, and without its
|
|
352
|
+
* definition they cannot be bound to this constructor. Null otherwise: a
|
|
353
|
+
* constructor of a resolved class receives every construction selecting it.
|
|
354
|
+
*/
|
|
355
|
+
function unownedConstructorHint(index, def, callerCount) {
|
|
356
|
+
if (callerCount > 0 || !def?.className) return null;
|
|
357
|
+
const { isConstructor } = require('./constructor-calls');
|
|
358
|
+
if (!isConstructor(def, index) || require('./class-identity').ownerDefinitionOf(index, def) ||
|
|
359
|
+
!index.getCalleeFiles(def.className)) return null;
|
|
360
|
+
return {
|
|
361
|
+
type: 'hint',
|
|
362
|
+
message: `The class declaring this constructor was not resolved, so constructions of \`${def.className}\` are not bound to it; query \`${def.className}\` for the construction sites.`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
349
366
|
function context(index, name, options = {}) {
|
|
350
367
|
index._beginOp();
|
|
351
368
|
try {
|
|
@@ -591,6 +608,7 @@ function context(index, name, options = {}) {
|
|
|
591
608
|
...(ambiguityCandidates && { ambiguityCandidates }),
|
|
592
609
|
callees,
|
|
593
610
|
unverifiedCallees: rawCallees.unverifiedCallees || [],
|
|
611
|
+
...(rawCallees.initializerReviews?.length && { initializerReviews: rawCallees.initializerReviews }),
|
|
594
612
|
callerHistogram,
|
|
595
613
|
calleeHistogram,
|
|
596
614
|
meta: {
|
|
@@ -619,18 +637,11 @@ function context(index, name, options = {}) {
|
|
|
619
637
|
}
|
|
620
638
|
};
|
|
621
639
|
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
(def.type === 'constructor' || name === 'constructor' || name === '__init__') &&
|
|
628
|
-
index.getCalleeFiles(def.className)) {
|
|
629
|
-
warnings.push({
|
|
630
|
-
type: 'hint',
|
|
631
|
-
message: `Constructor invocations (\`new ${def.className}(...)\` / \`${def.className}(...)\`) are indexed under the class name — query \`${def.className}\` for the call sites.`,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
640
|
+
// A constructor handle lists the constructions that select it (fix
|
|
641
|
+
// #417). Only a constructor whose declaring class is unresolved cannot
|
|
642
|
+
// receive them; its class handle still has the sites (fix #421).
|
|
643
|
+
const unownedHint = unownedConstructorHint(index, def, callers.length);
|
|
644
|
+
if (unownedHint) warnings.push(unownedHint);
|
|
634
645
|
|
|
635
646
|
if (warnings.length > 0) {
|
|
636
647
|
result.warnings = warnings;
|
|
@@ -711,6 +722,7 @@ function smart(index, name, options = {}) {
|
|
|
711
722
|
dependencies,
|
|
712
723
|
types,
|
|
713
724
|
unverifiedCallees: smartUnverified,
|
|
725
|
+
...(callees.initializerReviews?.length && { initializerReviews: callees.initializerReviews }),
|
|
714
726
|
meta: {
|
|
715
727
|
complete: stats.uncertain === 0 && dynamicImports === 0 && smartUnverified.length === 0,
|
|
716
728
|
skipped: 0,
|
|
@@ -991,99 +1003,132 @@ function related(index, name, options = {}) {
|
|
|
991
1003
|
* @param {object} options - { file, className, exclude, top }
|
|
992
1004
|
* @returns {object|null}
|
|
993
1005
|
*/
|
|
994
|
-
// Kinds whose dependents
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
|
|
1006
|
+
// Kinds whose dependents include annotation/reference sites (fix #345):
|
|
1007
|
+
// every type declaration. A class or struct is also constructed: its
|
|
1008
|
+
// constructions are call sites of the caller band, every other token naming
|
|
1009
|
+
// it (annotations, bases, casts, qualifiers, imports) a type reference
|
|
1010
|
+
// (fix #428).
|
|
1011
|
+
const TYPE_REFERENCE_KINDS = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'record', 'union']);
|
|
1012
|
+
// Tokens that declare the type (its own name, its constructors' names).
|
|
1013
|
+
const TYPE_DECLARATION_ROLES = new Set(['decl', 'ctor', 'ctor-qualified', 'pin-binding']);
|
|
998
1014
|
|
|
999
1015
|
/**
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1016
|
+
* The type reference band of a type definition (fixes #345, #428): every
|
|
1017
|
+
* token naming the type on the name's ground lines, decided by the
|
|
1018
|
+
* type-rename lookup (core/type-references.js, the language's own name
|
|
1019
|
+
* lookup: lexical shadows, type parameters, imports, packages, namespaces,
|
|
1020
|
+
* include closures). A token the lookup proves names the type is confirmed,
|
|
1021
|
+
* one it cannot settle is unverified with its reason, one naming something
|
|
1022
|
+
* else is excluded with its reason. Tokens at call sites (constructions)
|
|
1023
|
+
* belong to the caller band and declaration tokens to the definitions; the
|
|
1024
|
+
* account partitions all of them.
|
|
1006
1025
|
*/
|
|
1007
1026
|
function findTypeReferences(index, name, def, options = {}) {
|
|
1008
|
-
if (!def || !TYPE_REFERENCE_KINDS.has(def.type)) return null;
|
|
1009
|
-
const {
|
|
1010
|
-
const {
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
const excluded = [];
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
(
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1027
|
+
if (!def || !TYPE_REFERENCE_KINDS.has(def.type) || def.isMethod) return null;
|
|
1028
|
+
const { TypeReferenceResolver, typeReferenceSites, csharpAttributeShortSites } = require('./type-references');
|
|
1029
|
+
const { computeGroundSet } = require('./account');
|
|
1030
|
+
const { getCachedCalls, _recordIsNoCall } = require('./callers');
|
|
1031
|
+
const resolver = new TypeReferenceResolver(index, def, name);
|
|
1032
|
+
resolver.collectRoles = true;
|
|
1033
|
+
const ground = computeGroundSet(index, name);
|
|
1034
|
+
const scopedGround = options.files ? { ...ground,
|
|
1035
|
+
perFile: new Map([...ground.perFile].filter(([file]) => options.files.has(file))) } : ground;
|
|
1036
|
+
const { files } = typeReferenceSites(index, def, name, scopedGround, resolver);
|
|
1037
|
+
const confirmed = [], unverified = [], excluded = [];
|
|
1038
|
+
let tokens = 0, lineCount = 0, textLines = 0, constructions = 0, declarations = 0;
|
|
1039
|
+
const scannedFiles = new Set();
|
|
1040
|
+
const roleKind = role => role === 'value' || role === 'shorthand' ? 'value' : role === 'import' ? 'import'
|
|
1041
|
+
: role === 'string-annotation' ? 'annotation-string' : role === 'doc-cref' ? 'doc-reference'
|
|
1042
|
+
: role === 'member' ? 'member' : 'type';
|
|
1043
|
+
for (const [absoluteFile, info] of [...files].sort((a, b) => codeUnitCompare(a[1].relativePath, b[1].relativePath))) {
|
|
1044
|
+
const file = info.relativePath;
|
|
1045
|
+
if (!index.matchesFilters(file, options) || options.files && !options.files.has(absoluteFile)) continue;
|
|
1046
|
+
// One split of the file and one grouping of its call records by line.
|
|
1047
|
+
let fileLines;
|
|
1048
|
+
try { fileLines = index._getFileLines(absoluteFile) || []; } catch { fileLines = []; }
|
|
1049
|
+
let callsByLine = null;
|
|
1050
|
+
for (const [line, slot] of [...info.lines].sort((a, b) => a[0] - b[0])) {
|
|
1051
|
+
const expression = String(fileLines[line - 1] || '').trim();
|
|
1052
|
+
if (slot.tokens === 0 && slot.reviews.length === 0) {
|
|
1053
|
+
if (slot.text && !slot.macroBody) { textLines++; continue; }
|
|
1054
|
+
// The name outside every parsed token and comment: code the
|
|
1055
|
+
// lookup cannot read (a macro body, an unparsed region).
|
|
1056
|
+
lineCount++;
|
|
1057
|
+
scannedFiles.add(absoluteFile);
|
|
1058
|
+
unverified.push({ file, line, expression, kind: 'unparsed',
|
|
1059
|
+
reason: slot.unparsedReason || (slot.macroBody ? 'macro-body' : 'unparsed-occurrence'), tier: 'unverified' });
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (slot.tokens > 0) { lineCount++; scannedFiles.add(absoluteFile); }
|
|
1063
|
+
tokens += slot.tokens;
|
|
1064
|
+
// Call-site tokens of the line: the construction records' name
|
|
1065
|
+
// columns; records without a column, by the token's AST position.
|
|
1066
|
+
if (!callsByLine) {
|
|
1067
|
+
callsByLine = new Map();
|
|
1068
|
+
for (const call of getCachedCalls(index, absoluteFile) || []) {
|
|
1069
|
+
if (call.name !== name && !(call.receiverIsTypeQualified && call.receiver === name)) continue;
|
|
1070
|
+
if (!callsByLine.has(call.line)) callsByLine.set(call.line, []);
|
|
1071
|
+
callsByLine.get(call.line).push(call);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
// A cast or a member initializer spelled like a call is no call.
|
|
1075
|
+
// A static member call through the type (`Key.get(..)`, Java and
|
|
1076
|
+
// C#) is a type-qualifier site of the caller band too.
|
|
1077
|
+
const lineCalls = (callsByLine.get(line) || []).filter(call => !_recordIsNoCall(index, absoluteFile, call));
|
|
1078
|
+
// A default or list initialization (`Foo y;`, `Foo z{}`, fix
|
|
1079
|
+
// #421) is recorded at its declarator and a qualified call at its
|
|
1080
|
+
// member: the type token is the nearest name token before it.
|
|
1081
|
+
const lineTokens = [...slot.edits, ...slot.reviews.map(review => review.column),
|
|
1082
|
+
...(slot.skipColumns || [])].filter(Number.isInteger).sort((a, b) => a - b);
|
|
1083
|
+
const callColumns = new Set(lineCalls.map(call => {
|
|
1084
|
+
const column = call.column ?? call.callSite?.column;
|
|
1085
|
+
const before = call.name !== name || ['default', 'list'].includes(call.constructorSyntax);
|
|
1086
|
+
if (!Number.isInteger(column) || !before || lineTokens.includes(column)) return column;
|
|
1087
|
+
return lineTokens.filter(token => token < column).pop();
|
|
1088
|
+
}).filter(Number.isInteger));
|
|
1089
|
+
const columnless = lineCalls.some(call => !Number.isInteger(call.column ?? call.callSite?.column));
|
|
1090
|
+
const atCall = column => callColumns.has(column) ||
|
|
1091
|
+
(columnless && (slot.constructionColumns || []).includes(column));
|
|
1092
|
+
const roleAt = column => slot.roles?.get(column);
|
|
1093
|
+
const site = (column, extra) => ({ file, line, ...(Number.isInteger(column) && { column }), expression, ...extra });
|
|
1094
|
+
for (const column of slot.edits) {
|
|
1095
|
+
const role = roleAt(column);
|
|
1096
|
+
if (TYPE_DECLARATION_ROLES.has(role)) { declarations++; continue; }
|
|
1097
|
+
if (atCall(column)) { constructions++; continue; }
|
|
1098
|
+
confirmed.push(site(column, { kind: roleKind(role), tier: 'confirmed' }));
|
|
1099
|
+
}
|
|
1100
|
+
for (const review of slot.reviews) {
|
|
1101
|
+
if (Number.isInteger(review.column) && atCall(review.column)) { constructions++; continue; }
|
|
1102
|
+
unverified.push(site(review.column, { kind: roleKind(roleAt(review.column)),
|
|
1103
|
+
reason: review.reason || 'unresolved', tier: 'unverified' }));
|
|
1104
|
+
}
|
|
1105
|
+
for (const column of slot.skipColumns || []) {
|
|
1106
|
+
if (atCall(column)) { constructions++; continue; }
|
|
1107
|
+
excluded.push(site(column, { reason: slot.skipReasons?.get(column) || 'other-declaration' }));
|
|
1108
|
+
}
|
|
1073
1109
|
}
|
|
1074
|
-
unverified.push({ ...site, reason: 'no-import-link' });
|
|
1075
1110
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1111
|
+
// C# applies `XAttribute` as `[X]` (fix #390): the short spellings.
|
|
1112
|
+
let attributeTokens = 0;
|
|
1113
|
+
for (const short of csharpAttributeShortSites(index, def, name)) {
|
|
1114
|
+
if (!index.matchesFilters(short.relativePath, options) || options.files && !options.files.has(short.file)) continue;
|
|
1115
|
+
attributeTokens++;
|
|
1116
|
+
const entry = { file: short.relativePath, line: short.line, column: short.column,
|
|
1117
|
+
expression: (index.getLineContent(short.file, short.line) || '').trim(), kind: 'attribute' };
|
|
1118
|
+
if (short.verdict === 'yes') confirmed.push({ ...entry, tier: 'confirmed' });
|
|
1119
|
+
else if (short.verdict === 'unknown') unverified.push({ ...entry, reason: short.reason || 'unresolved', tier: 'unverified' });
|
|
1120
|
+
else excluded.push({ ...entry, reason: short.reason || 'other-declaration' });
|
|
1079
1121
|
}
|
|
1080
|
-
const bySite = (a, b) => a.file
|
|
1122
|
+
const bySite = (a, b) => codeUnitCompare(a.file, b.file) || a.line - b.line || (a.column ?? -1) - (b.column ?? -1);
|
|
1081
1123
|
confirmed.sort(bySite); unverified.sort(bySite);
|
|
1082
1124
|
const byFile = new Map();
|
|
1083
|
-
for (const
|
|
1084
|
-
if (!byFile.has(
|
|
1085
|
-
byFile.get(
|
|
1125
|
+
for (const entry of confirmed) {
|
|
1126
|
+
if (!byFile.has(entry.file)) byFile.set(entry.file, []);
|
|
1127
|
+
byFile.get(entry.file).push(entry);
|
|
1086
1128
|
}
|
|
1129
|
+
const tokenSites = unverified.filter(entry => entry.kind !== 'unparsed').length;
|
|
1130
|
+
const allTokens = tokens + attributeTokens;
|
|
1131
|
+
const decided = confirmed.length + tokenSites + excluded.length + constructions + declarations;
|
|
1087
1132
|
return {
|
|
1088
1133
|
owner: def.type,
|
|
1089
1134
|
confirmedCount: confirmed.length,
|
|
@@ -1093,7 +1138,14 @@ function findTypeReferences(index, name, def, options = {}) {
|
|
|
1093
1138
|
unverifiedSites: unverified,
|
|
1094
1139
|
excluded: {
|
|
1095
1140
|
total: excluded.length,
|
|
1096
|
-
byReason: excluded.reduce((out,
|
|
1141
|
+
byReason: excluded.reduce((out, entry) => { out[entry.reason] = (out[entry.reason] || 0) + 1; return out; }, {}),
|
|
1142
|
+
},
|
|
1143
|
+
account: {
|
|
1144
|
+
tokens: allTokens, lines: lineCount, files: scannedFiles.size, textLines,
|
|
1145
|
+
confirmed: confirmed.length, unverified: tokenSites, unparsedLines: unverified.length - tokenSites,
|
|
1146
|
+
excluded: excluded.length, constructions, declarations,
|
|
1147
|
+
...(attributeTokens > 0 && { attributeTokens }),
|
|
1148
|
+
conserved: decided === allTokens,
|
|
1097
1149
|
},
|
|
1098
1150
|
};
|
|
1099
1151
|
}
|
|
@@ -1416,6 +1468,29 @@ function impact(index, name, options = {}) {
|
|
|
1416
1468
|
// dependents. Same design as the accessor band: a separate band, never
|
|
1417
1469
|
// fake caller edges (the caller oracle and the account stay call-shaped).
|
|
1418
1470
|
let typeReferences = findTypeReferences(index, name, def, options);
|
|
1471
|
+
const valueAccesses = isValueDefinition(def)
|
|
1472
|
+
? require('./value-references').findValueReferences(index, name, def, {
|
|
1473
|
+
includeTests: true, exclude: options.exclude,
|
|
1474
|
+
}) : null;
|
|
1475
|
+
|
|
1476
|
+
// A value token that is also a call is one dependency site (fix #420):
|
|
1477
|
+
// the value band lists every token, the call band only the calls.
|
|
1478
|
+
const valueOnlyCount = valueAccesses ? (() => {
|
|
1479
|
+
const calls = new Map();
|
|
1480
|
+
for (const site of filteredSites) {
|
|
1481
|
+
const key = `${site.file}\0${site.line}`;
|
|
1482
|
+
calls.set(key, (calls.get(key) || 0) + 1);
|
|
1483
|
+
}
|
|
1484
|
+
let count = 0;
|
|
1485
|
+
for (const group of valueAccesses.byFile) {
|
|
1486
|
+
for (const site of group.sites) {
|
|
1487
|
+
const key = `${site.file}\0${site.line}`;
|
|
1488
|
+
if (calls.get(key) > 0) calls.set(key, calls.get(key) - 1);
|
|
1489
|
+
else count++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return count;
|
|
1493
|
+
})() : 0;
|
|
1419
1494
|
|
|
1420
1495
|
// Apply top limit if specified (limits total call sites shown)
|
|
1421
1496
|
const totalBeforeLimit = filteredSites.length;
|
|
@@ -1464,6 +1539,8 @@ function impact(index, name, options = {}) {
|
|
|
1464
1539
|
...(propertyAccesses?.unverifiedSites || []).map(site => site.file),
|
|
1465
1540
|
...(typeReferences?.byFile || []).map(group => group.file),
|
|
1466
1541
|
...(typeReferences?.unverifiedSites || []).map(site => site.file),
|
|
1542
|
+
...(valueAccesses?.byFile || []).map(group => group.file),
|
|
1543
|
+
...(valueAccesses?.unverifiedSites || []).map(site => site.file),
|
|
1467
1544
|
]);
|
|
1468
1545
|
|
|
1469
1546
|
return {
|
|
@@ -1486,6 +1563,11 @@ function impact(index, name, options = {}) {
|
|
|
1486
1563
|
totalDependencySites: totalBeforeLimit + typeReferences.confirmedCount,
|
|
1487
1564
|
affectedFiles: affectedFiles.size,
|
|
1488
1565
|
}),
|
|
1566
|
+
...(valueAccesses && {
|
|
1567
|
+
valueAccesses,
|
|
1568
|
+
totalDependencySites: totalBeforeLimit + valueOnlyCount,
|
|
1569
|
+
affectedFiles: affectedFiles.size,
|
|
1570
|
+
}),
|
|
1489
1571
|
account: impactAccount,
|
|
1490
1572
|
hasEntrypoints: !!impactReachable && impactReachable.size > 0,
|
|
1491
1573
|
callerHistogram,
|
|
@@ -1572,7 +1654,7 @@ function about(index, name, options = {}) {
|
|
|
1572
1654
|
// ("Using <def>...") stays consistent with the count.
|
|
1573
1655
|
const visible = definitions.filter(d => {
|
|
1574
1656
|
if (d === primary || (d.relativePath === primary.relativePath && d.startLine === primary.startLine)) return true;
|
|
1575
|
-
const lang =
|
|
1657
|
+
const lang = index.languageOf(d.file);
|
|
1576
1658
|
if (isTestFile(d.relativePath, lang) || isTestPath(d.relativePath)) return false;
|
|
1577
1659
|
return true;
|
|
1578
1660
|
});
|
|
@@ -1831,6 +1913,7 @@ function about(index, name, options = {}) {
|
|
|
1831
1913
|
...(c.paramsStructured && { paramsStructured: c.paramsStructured }),
|
|
1832
1914
|
...(c.docstring && { docstring: c.docstring }),
|
|
1833
1915
|
...(c.sideEffects && c.sideEffects.length && { sideEffects: c.sideEffects }),
|
|
1916
|
+
...(c.initializerOf && { initializerOf: c.initializerOf }),
|
|
1834
1917
|
}));
|
|
1835
1918
|
}
|
|
1836
1919
|
|
|
@@ -1964,16 +2047,8 @@ function about(index, name, options = {}) {
|
|
|
1964
2047
|
completeness: detectCompleteness(index)
|
|
1965
2048
|
};
|
|
1966
2049
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
if (primary.className && (callers?.length ?? 0) === 0 &&
|
|
1970
|
-
(primary.type === 'constructor' || symbolName === 'constructor' || symbolName === '__init__') &&
|
|
1971
|
-
index.getCalleeFiles(primary.className)) {
|
|
1972
|
-
result.warnings = [...(result.warnings || []), {
|
|
1973
|
-
type: 'hint',
|
|
1974
|
-
message: `Constructor invocations (\`new ${primary.className}(...)\` / \`${primary.className}(...)\`) are indexed under the class name — query \`${primary.className}\` for the call sites.`,
|
|
1975
|
-
}];
|
|
1976
|
-
}
|
|
2050
|
+
const unownedHint = unownedConstructorHint(index, primary, callers?.length ?? 0);
|
|
2051
|
+
if (unownedHint) result.warnings = [...(result.warnings || []), unownedHint];
|
|
1977
2052
|
|
|
1978
2053
|
return result;
|
|
1979
2054
|
} finally { index._endOp(); }
|
|
@@ -2129,7 +2204,7 @@ function diffImpact(index, options = {}) {
|
|
|
2129
2204
|
let nonSourcePaths = 0;
|
|
2130
2205
|
|
|
2131
2206
|
for (const change of changes) {
|
|
2132
|
-
const lang =
|
|
2207
|
+
const lang = index.languageOf(change.filePath);
|
|
2133
2208
|
if (!lang || (change.untracked && !index.files.has(change.filePath))) {
|
|
2134
2209
|
nonSourcePaths++;
|
|
2135
2210
|
continue;
|
|
@@ -2174,6 +2249,7 @@ function diffImpact(index, options = {}) {
|
|
|
2174
2249
|
let oldCallables = null; // old symbols WITH ranges — deleted lines are old-file coordinates
|
|
2175
2250
|
let oldParsed = null;
|
|
2176
2251
|
let oldLines = null;
|
|
2252
|
+
let oldText = null;
|
|
2177
2253
|
if (change.deletedLines.length > 0 || change.addedLines.length > 0) {
|
|
2178
2254
|
const ref = staged ? 'HEAD' : base;
|
|
2179
2255
|
try {
|
|
@@ -2181,9 +2257,10 @@ function diffImpact(index, options = {}) {
|
|
|
2181
2257
|
'git', ['show', `${ref}:${change.gitRelativePath}`],
|
|
2182
2258
|
{ cwd: index.root, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
2183
2259
|
);
|
|
2184
|
-
const fileLang =
|
|
2260
|
+
const fileLang = index.languageOf(change.filePath);
|
|
2185
2261
|
if (fileLang) {
|
|
2186
2262
|
oldParsed = parse(oldContent, fileLang);
|
|
2263
|
+
oldText = oldContent;
|
|
2187
2264
|
oldLines = oldContent.split('\n');
|
|
2188
2265
|
oldSymbolIdentities = new Set();
|
|
2189
2266
|
oldCallables = extractCallableSymbols(oldParsed);
|
|
@@ -2198,17 +2275,61 @@ function diffImpact(index, options = {}) {
|
|
|
2198
2275
|
|
|
2199
2276
|
collectNonCallableChanges(index, change, oldParsed, fileEntry.symbols, symbolChanges, oldLines);
|
|
2200
2277
|
|
|
2278
|
+
// Several callables can share a changed line (`class Box {
|
|
2279
|
+
// constructor(v) {..} get w() {..} }`, fix #434): the line belongs
|
|
2280
|
+
// to each whose own source changed, a callable whose text equals its
|
|
2281
|
+
// base twin's is unchanged by it.
|
|
2282
|
+
let currentText = null;
|
|
2283
|
+
const spanText = (text, s) => text != null && s?.sourceSpan &&
|
|
2284
|
+
Number.isInteger(s.sourceSpan.start) && Number.isInteger(s.sourceSpan.end)
|
|
2285
|
+
? text.slice(s.sourceSpan.start, s.sourceSpan.end) : null;
|
|
2286
|
+
const unchangedMemo = new Map();
|
|
2287
|
+
const unchangedCallable = symbol => {
|
|
2288
|
+
if (unchangedMemo.has(symbol)) return unchangedMemo.get(symbol);
|
|
2289
|
+
let unchanged = false;
|
|
2290
|
+
if (oldCallables !== null && symbol.sourceSpan) {
|
|
2291
|
+
if (currentText === null) {
|
|
2292
|
+
try { currentText = index._readFile(change.filePath); } catch { currentText = undefined; }
|
|
2293
|
+
}
|
|
2294
|
+
const text = spanText(currentText, symbol);
|
|
2295
|
+
unchanged = text != null && oldCallables.some(old => old.name === symbol.name &&
|
|
2296
|
+
(old.className || '') === (symbol.className || '') && spanText(oldText, old) === text);
|
|
2297
|
+
}
|
|
2298
|
+
unchangedMemo.set(symbol, unchanged);
|
|
2299
|
+
return unchanged;
|
|
2300
|
+
};
|
|
2201
2301
|
for (const line of change.addedLines) {
|
|
2202
2302
|
// Arrow fields are indexed as fields but their bodies still need
|
|
2203
2303
|
// callable impact once declaration-only changes are separated out.
|
|
2204
|
-
const
|
|
2304
|
+
const primary = index.findEnclosingFunction(change.filePath, line, true) ||
|
|
2205
2305
|
fileEntry.symbols.find(s => isDiffCallable(s) && s.startLine <= line && s.endLine >= line);
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2306
|
+
let lineSymbols = primary ? [primary] : [];
|
|
2307
|
+
if (primary) {
|
|
2308
|
+
const disjoint = (a, b) => a.sourceSpan && b.sourceSpan &&
|
|
2309
|
+
(a.sourceSpan.end <= b.sourceSpan.start || b.sourceSpan.end <= a.sourceSpan.start);
|
|
2310
|
+
const siblings = fileEntry.symbols.filter(s => s !== primary && isDiffCallable(s) &&
|
|
2311
|
+
s.startLine <= line && s.endLine >= line && disjoint(s, primary) &&
|
|
2312
|
+
!fileEntry.symbols.some(inner => inner !== s && isDiffCallable(inner) && inner.sourceSpan && s.sourceSpan &&
|
|
2313
|
+
inner.sourceSpan.start >= s.sourceSpan.start && inner.sourceSpan.end <= s.sourceSpan.end &&
|
|
2314
|
+
inner.startLine <= line && inner.endLine >= line));
|
|
2315
|
+
// Only a line inside the callable's own source span: an
|
|
2316
|
+
// attribute or decorator line outside it changes the callable.
|
|
2317
|
+
const spans = s => s.sourceSpan?.startLine != null && s.sourceSpan.startLine <= line &&
|
|
2318
|
+
line <= s.sourceSpan.endLine;
|
|
2319
|
+
lineSymbols = [primary, ...siblings].filter(s => !(spans(s) && unchangedCallable(s)));
|
|
2320
|
+
}
|
|
2321
|
+
if (lineSymbols.length > 0) {
|
|
2322
|
+
for (const symbol of lineSymbols) {
|
|
2323
|
+
const key = `${symbol.name}:${symbol.startLine}`;
|
|
2324
|
+
if (!affectedSymbols.has(key)) {
|
|
2325
|
+
affectedSymbols.set(key, { symbol, addedLines: [], deletedLines: [] });
|
|
2326
|
+
}
|
|
2327
|
+
affectedSymbols.get(key).addedLines.push(line);
|
|
2210
2328
|
}
|
|
2211
|
-
|
|
2329
|
+
} else if (primary) {
|
|
2330
|
+
// Every callable on the line is unchanged: the line's other
|
|
2331
|
+
// tokens changed.
|
|
2332
|
+
continue;
|
|
2212
2333
|
} else {
|
|
2213
2334
|
// Module-level change
|
|
2214
2335
|
const existing = moduleLevelChanges.find(m => m.filePath === change.filePath);
|
|
@@ -2266,11 +2387,16 @@ function diffImpact(index, options = {}) {
|
|
|
2266
2387
|
for (const c of candidates) {
|
|
2267
2388
|
if (Math.abs(c.startLine - bestOld.startLine) < Math.abs(cur.startLine - bestOld.startLine)) cur = c;
|
|
2268
2389
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2390
|
+
// A callable whose own text is unchanged is not
|
|
2391
|
+
// what the deleted line changed (fix #434).
|
|
2392
|
+
if (!(bestOld.sourceSpan?.startLine != null && bestOld.sourceSpan.startLine <= line &&
|
|
2393
|
+
line <= bestOld.sourceSpan.endLine && unchangedCallable(cur))) {
|
|
2394
|
+
const key = `${cur.name}:${cur.startLine}`;
|
|
2395
|
+
if (!affectedSymbols.has(key)) {
|
|
2396
|
+
affectedSymbols.set(key, { symbol: cur, addedLines: [], deletedLines: [] });
|
|
2397
|
+
}
|
|
2398
|
+
affectedSymbols.get(key).deletedLines.push(line);
|
|
2272
2399
|
}
|
|
2273
|
-
affectedSymbols.get(key).deletedLines.push(line);
|
|
2274
2400
|
}
|
|
2275
2401
|
// Identity gone → the whole function was deleted; the
|
|
2276
2402
|
// deletion detection below reports it. Its body lines are
|
|
@@ -2553,8 +2679,10 @@ function diffImpact(index, options = {}) {
|
|
|
2553
2679
|
totalCallSites += evidence.totalCallSites || 0;
|
|
2554
2680
|
totalUnverifiedSites += evidence.unverifiedSites?.length || 0;
|
|
2555
2681
|
for (const group of evidence.byFile || []) callerFileSet.add(path.resolve(index.root, group.file));
|
|
2556
|
-
const refs = evidence.typeReferences || evidence.propertyAccesses;
|
|
2557
|
-
|
|
2682
|
+
const refs = evidence.typeReferences || evidence.propertyAccesses || evidence.valueAccesses;
|
|
2683
|
+
// Non-call sites only: a value band also lists the call tokens.
|
|
2684
|
+
totalDependencySites += refs && Number.isInteger(evidence.totalDependencySites)
|
|
2685
|
+
? Math.max(0, evidence.totalDependencySites - (evidence.totalCallSites || 0)) : refs?.confirmedCount || 0;
|
|
2558
2686
|
unverifiedDependencySites += refs?.unverifiedCount || 0;
|
|
2559
2687
|
for (const group of refs?.byFile || []) dependencyFiles.add(group.file);
|
|
2560
2688
|
for (const site of refs?.unverifiedSites || []) dependencyFiles.add(site.file);
|
|
@@ -2609,7 +2737,7 @@ function collectNonCallableChanges(index, change, oldParsed, currentSymbols, out
|
|
|
2609
2737
|
before.get(key).push(s);
|
|
2610
2738
|
}
|
|
2611
2739
|
const touches = (s, lines) => lines.some(line => line >= s.startLine && line <= (s.endLine || s.startLine));
|
|
2612
|
-
const language =
|
|
2740
|
+
const language = index.languageOf(change.filePath);
|
|
2613
2741
|
let currentSnapshots = new Map();
|
|
2614
2742
|
let oldSnapshots = new Map();
|
|
2615
2743
|
// Unsupported/recovered declaration shapes retain conservative range
|
|
@@ -2697,13 +2825,15 @@ function extractCallableSymbols(parsed) {
|
|
|
2697
2825
|
const signature = s => ({ params: s.params, paramsStructured: s.paramsStructured, generics: s.generics });
|
|
2698
2826
|
for (const fn of parsed.functions) {
|
|
2699
2827
|
const className = fn.className || (fn.receiver ? fn.receiver.replace(/^\*/, '') : '');
|
|
2700
|
-
symbols.push({ name: fn.name, className, startLine: fn.startLine, endLine: fn.endLine, ...signature(fn)
|
|
2828
|
+
symbols.push({ name: fn.name, className, startLine: fn.startLine, endLine: fn.endLine, ...signature(fn),
|
|
2829
|
+
...(fn.sourceSpan && { sourceSpan: fn.sourceSpan }) });
|
|
2701
2830
|
}
|
|
2702
2831
|
for (const cls of parsed.classes) {
|
|
2703
2832
|
if (cls.members) {
|
|
2704
2833
|
for (const m of cls.members) {
|
|
2705
2834
|
if (!isDiffCallable(m)) continue;
|
|
2706
|
-
symbols.push({ name: m.name, className: cls.name, startLine: m.startLine, endLine: m.endLine, ...signature(m)
|
|
2835
|
+
symbols.push({ name: m.name, className: cls.name, startLine: m.startLine, endLine: m.endLine, ...signature(m),
|
|
2836
|
+
...(m.sourceSpan && { sourceSpan: m.sourceSpan }) });
|
|
2707
2837
|
}
|
|
2708
2838
|
}
|
|
2709
2839
|
}
|
|
@@ -3568,4 +3698,6 @@ module.exports = {
|
|
|
3568
3698
|
// Shared with tracing.js (trace/blast/reverseTrace root accounts) — the
|
|
3569
3699
|
// tree commands compose the same text-ground account at their root hop.
|
|
3570
3700
|
composeAccount,
|
|
3701
|
+
// Test discovery shares impact's per-token type-reference evidence.
|
|
3702
|
+
findTypeReferences,
|
|
3571
3703
|
};
|
package/core/ast-analysis.js
CHANGED
|
@@ -37,7 +37,10 @@ const DECLARATION_NODES = {
|
|
|
37
37
|
trait: new Set(['trait_item']),
|
|
38
38
|
impl: new Set(['impl_item']),
|
|
39
39
|
record: new Set(['record_declaration']),
|
|
40
|
-
|
|
40
|
+
// An instance attribute a method assigns on its receiver (`this.v = v`,
|
|
41
|
+
// `self.v = v`) is declared by that assignment (fix #434).
|
|
42
|
+
field: new Set(['field_definition', 'public_field_definition', 'property_signature', 'field_declaration', 'variable_declarator',
|
|
43
|
+
'assignment_expression', 'assignment']),
|
|
41
44
|
state: new Set(['variable_declarator', 'assignment', 'init_declarator', 'const_item', 'static_item', 'const_spec', 'var_spec']),
|
|
42
45
|
};
|
|
43
46
|
const FUNCTION_EXPRESSIONS = new Set([
|
|
@@ -72,7 +75,9 @@ function declarationSnapshots(content, language, symbols) {
|
|
|
72
75
|
if (!node) return false;
|
|
73
76
|
if (node.text === name) return true;
|
|
74
77
|
return hasName(node.childForFieldName('name') || node.childForFieldName('declarator') ||
|
|
75
|
-
(node.type === 'generic_type' && node.childForFieldName('type'))
|
|
78
|
+
(node.type === 'generic_type' && node.childForFieldName('type')) ||
|
|
79
|
+
(['member_expression', 'attribute'].includes(node.type) &&
|
|
80
|
+
(node.childForFieldName('property') || node.childForFieldName('attribute'))), name);
|
|
76
81
|
};
|
|
77
82
|
for (const symbol of symbols) {
|
|
78
83
|
const kinds = DECLARATION_NODES[symbol.type];
|
|
@@ -400,7 +405,11 @@ function projectComputedDispatch(index) {
|
|
|
400
405
|
const byFile = new Map();
|
|
401
406
|
for (const [filePath, fileEntry] of index.files) {
|
|
402
407
|
try {
|
|
403
|
-
|
|
408
|
+
// Extracted at index time from the file's literal parse (fix
|
|
409
|
+
// #436); an entry without them is read here.
|
|
410
|
+
const sites = Array.isArray(fileEntry.computedDispatchSites)
|
|
411
|
+
? fileEntry.computedDispatchSites.map(site => ({ ...site }))
|
|
412
|
+
: computedDispatchSites(index._readFile(filePath), fileEntry.language);
|
|
404
413
|
if (sites.length > 0) byFile.set(filePath, sites);
|
|
405
414
|
} catch (_) {
|
|
406
415
|
// Unreadable files are reported through the existing parse/read
|