ucn 5.6.0 → 5.6.1

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/core/deadcode.js CHANGED
@@ -19,6 +19,9 @@ const { protocolMemberOf, protocolTypeOf, typedLiteralContract, rustProjectTrait
19
19
  hierarchySlotClosure, goContractClosure, structuralContractsRequiring,
20
20
  requirementIn, resetContractIndex } = require('./contract-membership');
21
21
  const { projectKind, downstreamExposureBlocks } = require('./deletion');
22
+ const { pythonIdentity, registrationUnknown, pythonSourceName, pythonNormalizedNames,
23
+ pythonProjectIdentityAffected } = require('./python-binding-identity');
24
+ const { runtimeEntrypoints, runtimeTargetReaches } = require('./runtime-entrypoints');
22
25
 
23
26
  const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
24
27
 
@@ -32,12 +35,6 @@ const CLASS_AUDIT_KINDS = ['class', 'struct', 'interface', 'trait', 'record', 'e
32
35
  // the callable with an external runtime. They therefore remain eligible for a
33
36
  // name/usage-based dead-code proof. Every other unknown decorator/annotation
34
37
  // stays conservative: it may be the only evidence of framework registration.
35
- const _NON_REGISTERING_DECORATORS = new Set([
36
- 'property',
37
- 'classmethod',
38
- 'staticmethod',
39
- ]);
40
-
41
38
  function _decoratorName(value) {
42
39
  const raw = typeof value === 'string'
43
40
  ? value
@@ -46,7 +43,33 @@ function _decoratorName(value) {
46
43
  return String(raw).trim().replace(/^@/, '').split('(')[0].trim();
47
44
  }
48
45
 
49
- function hasUnknownRegistrationDecorator(symbol) {
46
+ const PYTHON_DECLARATIONS = new WeakMap();
47
+
48
+ function pythonDeclaration(index, symbol) {
49
+ try {
50
+ const operation = index._opTreeCache;
51
+ let byFile = operation && PYTHON_DECLARATIONS.get(operation);
52
+ if (!byFile) {
53
+ byFile = new Map();
54
+ if (operation) PYTHON_DECLARATIONS.set(operation, byFile);
55
+ }
56
+ let byName = byFile.get(symbol.file);
57
+ if (!byName) {
58
+ byName = new Map();
59
+ const tree = index._getParsedTree(symbol.file, index._readFile(symbol.file), 'python');
60
+ for (const node of tree.rootNode.descendantsOfType(['function_definition', 'class_definition'])) {
61
+ const name = node.childForFieldName('name')?.text;
62
+ if (!byName.has(name)) byName.set(name, []);
63
+ byName.get(name).push(node);
64
+ }
65
+ byFile.set(symbol.file, byName);
66
+ }
67
+ return (byName.get(symbol.name) || []).find(node =>
68
+ node.startPosition.row + 1 >= symbol.startLine && node.endPosition.row + 1 <= symbol.endLine);
69
+ } catch { return null; }
70
+ }
71
+
72
+ function hasUnknownRegistrationDecorator(symbol, index) {
50
73
  const values = [
51
74
  symbol.decorators,
52
75
  symbol.decoratorsWithArgs,
@@ -54,15 +77,15 @@ function hasUnknownRegistrationDecorator(symbol) {
54
77
  symbol.attributesWithArgs,
55
78
  ].flatMap(items => Array.isArray(items) ? items : []);
56
79
 
80
+ if (values.length === 0) return false;
81
+ if (index?.files.get(symbol.file)?.language === 'python') {
82
+ if (typeof symbol.registrationUnknown === 'boolean') return symbol.registrationUnknown || pythonProjectIdentityAffected(index, 'builtins.*');
83
+ const declaration = pythonDeclaration(index, symbol);
84
+ return declaration ? registrationUnknown(declaration) || pythonProjectIdentityAffected(index, 'builtins.*') : true;
85
+ }
57
86
  return values.some(value => {
58
87
  const qualified = _decoratorName(value);
59
88
  if (!qualified) return false;
60
- const bare = qualified.split(/\.|::/).pop();
61
- if (_NON_REGISTERING_DECORATORS.has(bare)) return false;
62
- // `@name.getter`, `@name.setter`, and `@name.deleter` install an
63
- // accessor on an already local property; they are not external
64
- // framework registration boundaries.
65
- if (['getter', 'setter', 'deleter'].includes(bare)) return false;
66
89
  return true;
67
90
  });
68
91
  }
@@ -93,9 +116,20 @@ const _UNIVERSAL_ROOTS = new Set(['object', 'Object']);
93
116
  const _PY_NON_DISPATCHING_BASES = new Set(['Generic']);
94
117
 
95
118
  /** True when a base name resolves to NO in-project class/struct/interface/trait/record (an out-of-tree type). */
96
- function _baseIsExternal(index, bare, lang, fromDef) {
97
- if (!bare || _UNIVERSAL_ROOTS.has(bare)) return false;
98
- if (lang === 'python' && _PY_NON_DISPATCHING_BASES.has(bare)) return false;
119
+ function _baseIsExternal(index, bare, lang, fromDef, raw) {
120
+ if (!bare) return false;
121
+ if (lang === 'python' && fromDef) {
122
+ const declaration = pythonDeclaration(index, fromDef);
123
+ let base = declaration?.childForFieldName('superclasses')?.namedChildren.find(node =>
124
+ node.text === raw || (node.type === 'subscript' && node.childForFieldName('value')?.text === raw));
125
+ while (base?.type === 'subscript') base = base.childForFieldName('value');
126
+ const identity = pythonIdentity(base);
127
+ if (identity === 'builtins.object' || identity === 'typing.Generic') return pythonProjectIdentityAffected(index, identity);
128
+ // A source import can reuse a builtin's spelling without its semantics.
129
+ if (identity && (_UNIVERSAL_ROOTS.has(bare) || _PY_NON_DISPATCHING_BASES.has(bare))) return true;
130
+ } else if (_UNIVERSAL_ROOTS.has(bare) &&
131
+ !(index.files.get(fromDef?.file)?.importBindings || []).some(binding => (binding.alias || binding.name) === bare) &&
132
+ !(index.symbols.get(bare) || []).some(def => def.file === fromDef?.file)) return false;
99
133
  const defs = index.symbols.get(bare);
100
134
  return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type) &&
101
135
  // `class EnvironBuilder(werkzeug.test.EnvironBuilder)` must not
@@ -152,7 +186,7 @@ function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
152
186
  const bare = _bareBaseName(raw);
153
187
  if (!bare || seen.has(bare)) continue;
154
188
  seen.add(bare);
155
- if (_baseIsExternal(index, bare, lang, def)) return true;
189
+ if (_baseIsExternal(index, bare, lang, def, raw)) return true;
156
190
  for (const pd of index.symbols.get(bare) || []) {
157
191
  if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
158
192
  }
@@ -349,7 +383,7 @@ function contractFactsOf(index, declared, lang) {
349
383
  typedLiteralExternalMember(index, declared) ||
350
384
  implicitStandardContract(declared, lang)),
351
385
  projectContract: requiredByProjectContract(index, declared),
352
- registered: !!(isFrameworkEntrypoint(declared, index) || hasUnknownRegistrationDecorator(declared)),
386
+ registered: !!(isFrameworkEntrypoint(declared, index) || hasUnknownRegistrationDecorator(declared, index)),
353
387
  };
354
388
  }
355
389
 
@@ -885,6 +919,7 @@ function deadcode(index, options = {}) {
885
919
  // reports every outcome (detail: the audit row of a claim, the sites of
886
920
  // a use). Neither changes what is claimed.
887
921
  const onlyNames = options.names instanceof Set ? options.names : null;
922
+ const packagingEntrypoints = runtimeEntrypoints(index);
888
923
  const explain = typeof options.explain === 'function' ? options.explain : null;
889
924
  const withhold = (symbol, reason, detail) => { if (explain) explain(symbol, reason, detail); };
890
925
  // Assessment only: names whose every call site the caller has shown to
@@ -990,8 +1025,8 @@ function deadcode(index, options = {}) {
990
1025
  if (!fe || sites.length === 0) continue;
991
1026
  for (const site of sites) {
992
1027
  if (site.dynamic) dynamicReflectionFiles.add(reflectionFile);
993
- if (site.name) literalReflectionNames.add(site.name);
994
- if (site.patterns && site.patterns.length > 0 && !site.name) {
1028
+ if (site.name && !site.runtimeTarget) literalReflectionNames.add(site.name);
1029
+ if (site.patterns && site.patterns.length > 0 && !site.name && !site.runtimeTarget) {
995
1030
  // Exact alternatives of a multi-spelling site are literals.
996
1031
  for (const pattern of site.patterns) {
997
1032
  if (!pattern.includes('*')) literalReflectionNames.add(pattern);
@@ -1283,6 +1318,18 @@ function deadcode(index, options = {}) {
1283
1318
  // name inside /* ... */ counted as usage — commented-out code
1284
1319
  // silently kept its symbols "alive" and hid true dead claims.
1285
1320
  const lines = maskBlockComments(content, fileEntry.language).split('\n');
1321
+ let stringTree;
1322
+ const multilinePython = fileEntry.language === 'python' && (content.includes('"""') || content.includes("'''"));
1323
+ const inMultilineString = (line, column) => {
1324
+ if (!multilinePython) return false;
1325
+ stringTree ||= index._getParsedTree(filePath, content, fileEntry.language);
1326
+ let node = stringTree.rootNode.descendantForPosition({ row: line, column });
1327
+ for (; node; node = node.parent) {
1328
+ if (node.type === 'interpolation') return false;
1329
+ if (node.type === 'string') return true;
1330
+ }
1331
+ return false;
1332
+ };
1286
1333
  for (const name of namesInFile) {
1287
1334
  const nameLen = name.length;
1288
1335
  for (let i = 0; i < lines.length; i++) {
@@ -1318,7 +1365,7 @@ function deadcode(index, options = {}) {
1318
1365
  const rustAttributePath = fileEntry.language === 'rust' &&
1319
1366
  /^\s*#\[\s*serde\s*\(/.test(line) &&
1320
1367
  new RegExp(`\\b(?:serialize_with|deserialize_with|default|with)\\s*=\\s*["'][^"']*\\b${escapeRegExp(name)}\\b`).test(line);
1321
- if (isInsideString(line, pos, fileEntry.language) &&
1368
+ if ((isInsideString(line, pos, fileEntry.language) || inMultilineString(i, pos)) &&
1322
1369
  !(fileEntry.language === 'python' && classKindNames.has(name)) &&
1323
1370
  !rustAttributePath) continue;
1324
1371
  // Property/field access (preceded by '.'), not a
@@ -1639,7 +1686,18 @@ function deadcode(index, options = {}) {
1639
1686
  // call-pattern-based registration (Express routes, Gin handlers, etc.)
1640
1687
  // These functions are invoked by frameworks, not by user code.
1641
1688
  const hasFrameworkEntrypoint = isFrameworkEntrypoint(symbol, index);
1642
- const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol);
1689
+ const hasDecoratorMetadata = hasUnknownRegistrationDecorator(symbol, index);
1690
+ if (lang === 'python' && packagingEntrypoints.some(entry => entry.unknown || runtimeTargetReaches(index, symbol, entry.target))) {
1691
+ withhold(symbol, 'runtime-entrypoint');
1692
+ continue;
1693
+ }
1694
+
1695
+ if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity && symbol.registrationUnknown === false &&
1696
+ (symbol.decorators || []).length && pythonProjectIdentityAffected(index, 'builtins.*')) {
1697
+ excludedDecorated++;
1698
+ withhold(symbol, 'builtin-namespace-effect');
1699
+ continue;
1700
+ }
1643
1701
 
1644
1702
  // Unknown decorators are conservative runtime-registration
1645
1703
  // boundaries. A decorator necessarily consumes/replaces the
@@ -1653,6 +1711,19 @@ function deadcode(index, options = {}) {
1653
1711
  continue;
1654
1712
  }
1655
1713
 
1714
+ // The caller graph retains source spellings. Python can bind a
1715
+ // different written identifier to this runtime name via NFKC;
1716
+ // until that graph models the equivalence, do not clear it. The
1717
+ // inventory is AST-only and shared by every symbol in this op.
1718
+ if (langTraits(lang)?.reflectionApi?.pythonBindingIdentity &&
1719
+ (pythonSourceName(symbol.name) !== symbol.name ||
1720
+ (symbol.runtimeOwnerPath || [symbol.className]).filter(Boolean)
1721
+ .some(owner => pythonSourceName(owner) !== owner) ||
1722
+ pythonNormalizedNames(index).has(pythonSourceName(symbol.name)))) {
1723
+ withhold(symbol, 'identifier-normalization');
1724
+ continue;
1725
+ }
1726
+
1656
1727
  const isExported = symbolIsExported(index, symbol, fileEntry);
1657
1728
 
1658
1729
  // `target.onmessage = function ...` / `target.onopen = (...) =>`
@@ -1707,7 +1778,7 @@ function deadcode(index, options = {}) {
1707
1778
  continue;
1708
1779
  }
1709
1780
  if (members.some(m => isFrameworkEntrypoint(m, index) ||
1710
- hasUnknownRegistrationDecorator(m))) {
1781
+ hasUnknownRegistrationDecorator(m, index))) {
1711
1782
  if (!options.includeDecorated) {
1712
1783
  excludedDecorated++;
1713
1784
  withhold(symbol, 'registered');
package/core/deletion.js CHANGED
@@ -13,11 +13,10 @@
13
13
  * no-static-dependents the name has no use in project source outside its
14
14
  * own declarations, and no shield applies: nothing
15
15
  * the engine can see depends on it;
16
- * has-static-dependents a call or other use of the name exists OUTSIDE
17
- * the declaration. The first such site is given.
18
- * Evidence is by name, as in the audit: with two
19
- * declarations of one name it does not say which
20
- * of them the use reaches;
16
+ * has-static-dependents a confirmed call or an unambiguous use exists
17
+ * OUTSIDE the declaration. The first such site is
18
+ * given. Unresolved name-only evidence remains
19
+ * review and never borrows another target's site;
21
20
  * review no use outside the declaration was found, but a
22
21
  * zero is not evidence here: a shield applies
23
22
  * (consumers outside the repository, a contract, a
@@ -68,7 +67,10 @@ const DEPENDENT_REASONS = new Set(['has-callers', 'has-usages', 'member-in-use']
68
67
  /** Why the pipeline did or did not claim a declaration, in one sentence. */
69
68
  const REASON_TEXT = Object.freeze({
70
69
  'claimed': 'the name has no use in project source outside its own declarations, and no shield applies',
71
- 'has-callers': 'a call of this name exists outside the declaration',
70
+ 'has-callers': 'a call is proven to reach this declaration from outside it',
71
+ 'unresolved-use': 'uses of the name exist, but none is proven to reach this declaration',
72
+ 'identifier-normalization': 'Python normalizes a source identifier spelling that the caller graph does not model',
73
+ 'builtin-namespace-effect': 'project source can change the builtin namespace that this declaration relies on',
72
74
  'has-usages': 'the name is used outside the declaration',
73
75
  'member-in-use': 'a member of this type is called outside the declaration',
74
76
  'used-only-within-itself': 'every use found is inside the declaration itself, and the audit does not clear a name that is in use',
@@ -80,6 +82,7 @@ const REASON_TEXT = Object.freeze({
80
82
  'runtime-contract': 'invoked by the language runtime, the compiler or an assignment, not by name',
81
83
  'registered': 'decorated or registered with a framework, which may invoke it without naming it',
82
84
  'entry-point': 'an entry point the runtime or a test runner invokes',
85
+ 'runtime-entrypoint': 'packaging metadata registers this declaration, or its runtime entrypoints cannot be resolved',
83
86
  'constructor': 'a constructor: part of its type\'s allocation protocol, assessed with the type',
84
87
  'reflection': 'reflection in the project can reach this name',
85
88
  'dynamic-dispatch': 'member of a registry that is called through a computed key',
@@ -177,7 +180,21 @@ function callSiteOutside(index, declaration, indexed, names) {
177
180
  function useOutsideDeclaration(index, declaration, indexed, deadcode, options) {
178
181
  const name = declaration.name;
179
182
  const call = callSiteOutside(index, declaration, indexed, [name]);
180
- if (call) return { reason: 'has-callers', site: call };
183
+ if (!indexed) {
184
+ const definite = definiteDependentsOf(index, declaration).find(site => site.kind === 'call');
185
+ if (definite) return { reason: 'has-callers', site: { file: definite.file, line: definite.line } };
186
+ }
187
+ const duplicateName = (index.symbols.get(name) || []).some(def => !sameDeclaration(def, declaration));
188
+ if (call || duplicateName) {
189
+ const callers = index.findCallers(name, { targetDefinitions: [declaration], collectAccount: true,
190
+ includeMethods: true, maxResults: Infinity });
191
+ const proven = callers.filter(c => c.tier !== 'unverified' &&
192
+ !withinDeclaration(declaration, indexed, c.file || path.join(index.root, c.relativePath), c.line))
193
+ .map(c => ({ file: c.relativePath || relativeOf(index, c.file), line: c.line })).sort(compareSites);
194
+ if (proven.length) return { reason: 'has-callers', site: proven[0] };
195
+ // A callee index is a name inventory, not selected-target evidence.
196
+ // Its conservative audit veto survives without a misleading site.
197
+ }
181
198
  let found = null;
182
199
  deadcode(index, {
183
200
  names: new Set([name]),
@@ -196,13 +213,16 @@ function useOutsideDeclaration(index, declaration, indexed, deadcode, options) {
196
213
  } else if (reason === 'has-usages') {
197
214
  const sites = ((detail && detail.usages) || [])
198
215
  .filter(usage => !withinDeclaration(declaration, indexed, usage.file, usage.line))
216
+ .filter(usage => !(index.getCachedCalls(usage.file) || []).some(candidate =>
217
+ candidate.line === usage.line && !candidate.isFunctionReference &&
218
+ (candidate.name === name || candidate.resolvedName === name)))
199
219
  .map(usage => ({ file: relativeOf(index, usage.file), line: usage.line }))
200
220
  .sort(compareSites);
201
- if (sites.length > 0) found = { reason, site: sites[0] };
221
+ if (sites.length > 0) found = duplicateName ? { reason: 'unresolved-use' } : { reason, site: sites[0] };
202
222
  }
203
223
  },
204
224
  });
205
- return found;
225
+ return found || (call ? { reason: 'unresolved-use' } : null);
206
226
  }
207
227
 
208
228
  /**
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ // A data-only TOML reader for the PEP 621 entrypoint subtree. Other values
4
+ // remain opaque; malformed or nonliteral entrypoint values return unknown.
5
+ function entrypointToml(source) {
6
+ const tokens = [];
7
+ let line = 1;
8
+ let offset = 0;
9
+ while (offset < source.length) {
10
+ const ch = source[offset];
11
+ if (ch === '\n') { tokens.push({ kind: 'newline', line }); offset++; line++; continue; }
12
+ if (/\s/.test(ch)) { offset++; continue; }
13
+ if (ch === '#') { while (offset < source.length && source[offset] !== '\n') offset++; continue; }
14
+ if ('[]=.,{}'.includes(ch)) { tokens.push({ kind: ch, line }); offset++; continue; }
15
+ if (ch === '"' || ch === "'") {
16
+ const startLine = line;
17
+ const triple = source.slice(offset, offset + 3) === ch.repeat(3);
18
+ const delimiter = triple ? ch.repeat(3) : ch;
19
+ offset += delimiter.length;
20
+ let value = '';
21
+ let valid = true;
22
+ let closed = false;
23
+ while (offset < source.length) {
24
+ if (source.slice(offset, offset + delimiter.length) === delimiter) {
25
+ offset += delimiter.length; closed = true; break;
26
+ }
27
+ const current = source[offset++];
28
+ if (current === '\n') line++;
29
+ if (current !== '\\' || ch === "'") { value += current; continue; }
30
+ const escaped = source[offset++];
31
+ const simple = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
32
+ if (Object.hasOwn(simple, escaped)) value += simple[escaped];
33
+ else if (escaped === 'u' || escaped === 'U') {
34
+ const width = escaped === 'u' ? 4 : 8;
35
+ const digits = source.slice(offset, offset + width);
36
+ if (!/^[0-9a-fA-F]+$/.test(digits) || digits.length !== width || parseInt(digits, 16) > 0x10ffff) valid = false;
37
+ else value += String.fromCodePoint(parseInt(digits, 16));
38
+ offset += width;
39
+ } else if (triple && /\s/.test(escaped || '')) {
40
+ if (escaped === '\n') line++;
41
+ while (offset < source.length && /\s/.test(source[offset])) {
42
+ if (source[offset++] === '\n') line++;
43
+ }
44
+ } else valid = false;
45
+ }
46
+ tokens.push({ kind: 'string', value: valid && closed ? value.replace(/^\n/, '') : null, line: startLine });
47
+ continue;
48
+ }
49
+ const start = offset;
50
+ while (offset < source.length && !/[\s[\]=.,{}#]/.test(source[offset])) offset++;
51
+ if (offset === start) offset++;
52
+ tokens.push({ kind: 'bare', value: source.slice(start, offset), line });
53
+ }
54
+ const rows = [];
55
+ let at = 0;
56
+ let section = [];
57
+ const keys = () => {
58
+ const parts = [];
59
+ while (tokens[at] && ['bare', 'string'].includes(tokens[at].kind)) {
60
+ parts.push(tokens[at++].value);
61
+ if (tokens[at]?.kind !== '.') break;
62
+ at++;
63
+ }
64
+ return parts;
65
+ };
66
+ const entryPath = parts => parts[0] === 'project' &&
67
+ (['scripts', 'gui-scripts'].includes(parts[1]) || parts[1] === 'entry-points');
68
+ const readValue = (parts, depth = 0) => {
69
+ const token = tokens[at++];
70
+ if (!token || depth > 32) { if (entryPath(parts)) rows.push({ unknown: true, line: token?.line }); return; }
71
+ if (token.kind === '{') {
72
+ while (tokens[at] && tokens[at].kind !== '}') {
73
+ if (['newline', ','].includes(tokens[at].kind)) { at++; continue; }
74
+ const nested = keys();
75
+ if (!nested.length || tokens[at++]?.kind !== '=') { if (entryPath(parts)) rows.push({ unknown: true, line: token.line }); break; }
76
+ readValue([...parts, ...nested], depth + 1);
77
+ }
78
+ if (tokens[at]?.kind === '}') at++;
79
+ } else if (token.kind === '[') {
80
+ let balance = 1;
81
+ while (tokens[at] && balance) {
82
+ const item = tokens[at++];
83
+ if (item.kind === '[') balance++;
84
+ if (item.kind === ']') balance--;
85
+ if (parts.join('.') === 'project.dynamic' && item.kind === 'string' &&
86
+ ['scripts', 'gui-scripts', 'entry-points'].includes(item.value)) rows.push({ unknown: true, line: item.line });
87
+ }
88
+ if (entryPath(parts)) rows.push({ unknown: true, line: token.line });
89
+ } else if (entryPath(parts)) {
90
+ rows.push(token.kind === 'string' && token.value != null
91
+ ? { value: token.value, line: token.line } : { unknown: true, line: token.line });
92
+ }
93
+ };
94
+ while (at < tokens.length) {
95
+ if (tokens[at].kind === 'newline') { at++; continue; }
96
+ if (tokens[at].kind === '[') {
97
+ at++;
98
+ section = keys();
99
+ while (tokens[at] && ![']', 'newline'].includes(tokens[at].kind)) at++;
100
+ if (tokens[at]?.kind === ']') at++;
101
+ continue;
102
+ }
103
+ const parts = keys();
104
+ if (parts.length && tokens[at]?.kind === '=') { at++; readValue([...section, ...parts]); }
105
+ else while (tokens[at] && tokens[at].kind !== 'newline') at++;
106
+ }
107
+ return rows;
108
+ }
109
+
110
+ module.exports = { entrypointToml };
package/core/index-ir.js CHANGED
@@ -185,7 +185,7 @@ const OPTIONAL_SYMBOL_FIELDS = Object.freeze([
185
185
  'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'typeParameterConstraints', 'extends', 'implements', 'indent', 'isNested',
186
186
  'enclosingType', 'isMethod', 'receiver', 'memberType', 'fieldType', 'embedded',
187
187
  'aliasOf', 'aliasQualifier', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
188
- 'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
188
+ 'annotationsWithArgs', 'attributesWithArgs', 'registrationUnknown', 'runtimeOwnerPath', 'nameLine', 'traitImpl',
189
189
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'assignedObject', 'selfNamed', 'bodyScopedName',
190
190
  'registryMember', 'registryContainer', 'registryContainerType', 'objectLiteralLine', 'namespace', 'isConstructor',
191
191
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface', 'primaryParams',
@@ -230,7 +230,7 @@ function materializeSymbol(fileEntry, item) {
230
230
  // Most false feature flags are omitted for compactness, but
231
231
  // functionLike=false is the semantic distinction between an
232
232
  // object-like macro and a callable macro (UCN5-170).
233
- if (item[field] === false && field !== 'functionLike') continue;
233
+ if (item[field] === false && field !== 'functionLike' && field !== 'registrationUnknown') continue;
234
234
  symbol[field] = item[field];
235
235
  }
236
236
  return symbol;
package/core/ir.js CHANGED
@@ -66,7 +66,7 @@ function normalizeSymbol(symbol, family, language, kind, owner = null) {
66
66
  'isAsync', 'isGenerator', 'generics', 'ownerGenerics', 'genericBounds', 'typeParameterConstraints', 'extends', 'implements', 'indent',
67
67
  'isNested', 'enclosingType', 'isMethod', 'memberType', 'fieldType', 'embedded',
68
68
  'aliasOf', 'aliasQualifier', 'aliasMembers', 'aliasTypeText', 'aliasTypeParameters', 'aliasTypeDefaults', 'derefTarget', 'decorators', 'decoratorsWithArgs',
69
- 'annotationsWithArgs', 'attributesWithArgs', 'nameLine', 'traitImpl',
69
+ 'annotationsWithArgs', 'attributesWithArgs', 'registrationUnknown', 'runtimeOwnerPath', 'nameLine', 'traitImpl',
70
70
  'traitName', 'isSignature', 'memberAssigned', 'assignedReceiver', 'assignedObject', 'selfNamed', 'bodyScopedName',
71
71
  'registryMember', 'registryContainer', 'registryContainerType', 'objectLiteralLine', 'isConstructor',
72
72
  'isExtensionMethod', 'extensionReceiver', 'explicitInterface', 'primaryParams',
@@ -0,0 +1,116 @@
1
+ /** Validate changed calls against the project declarations they can reach. */
2
+ 'use strict';
3
+
4
+ const { getCachedCalls } = require('./callers');
5
+ const { verifyCallSites } = require('./verify');
6
+ const { langTraits } = require('../languages');
7
+ const { positivelyFits } = require('./call-fit');
8
+
9
+ /** One reader and result cache for a check, including all changed symbols. */
10
+ function outgoingChecker(index, judge, addedLinesByFile) {
11
+ const files = new Map();
12
+ const checked = new Map();
13
+ const read = file => {
14
+ if (!files.has(file)) files.set(file, {
15
+ calls: getCachedCalls(index, file) || [], lines: index._getFileLines(file),
16
+ });
17
+ return files.get(file);
18
+ };
19
+ return (definition, change, added) => {
20
+ const mismatches = [];
21
+ const unsettled = [];
22
+ if (!definition) return { mismatches, unsettled };
23
+ const data = read(definition.file);
24
+ const changedRanges = !added ? change?.changedCalls : null;
25
+ const lines = addedLinesByFile.get(definition.relativePath) || new Set();
26
+ const records = data.calls.filter(call => !call.isFunctionReference &&
27
+ call.line >= definition.startLine && call.line <= definition.endLine &&
28
+ (changedRanges ? changedRanges.some(range =>
29
+ range.start <= (call.callStart ?? call.callSite?.start) &&
30
+ range.end >= (call.callEnd ?? call.callSite?.end ?? call.callStart ?? call.callSite?.start))
31
+ : added || lines.has(call.line)));
32
+ if (records.length === 0) return { mismatches, unsettled };
33
+ const byStart = new Map(records.map(call => [call.callStart ?? call.callSite?.start, call]));
34
+ const callees = index.findCallees(definition, {
35
+ collectAccount: true, includeMethods: true, siteStarts: new Set(byStart.keys()),
36
+ });
37
+ const relative = definition.relativePath;
38
+ const positiveFamilies = new Set();
39
+ const possibleFamilies = new Set();
40
+ const issues = [];
41
+ const familyOf = (callee, site) => {
42
+ // The class's public constructor overloads are the call
43
+ // interface; an initializer edge also projects their body.
44
+ const owner = callee.initializerOf || (callee.type === 'class' ? callee.name : null);
45
+ return JSON.stringify([site.start, callee.file, owner || callee.name,
46
+ owner ? null : callee.className, callee.namespace, callee.enclosingType]);
47
+ };
48
+ const uncertain = (site, name, reason) => unsettled.push({ file: relative, line: site.line, column: site.column, name, reason });
49
+ for (const callee of callees) {
50
+ if (callee.functionReference) continue;
51
+ for (const site of callee.siteProvenance || []) {
52
+ const record = byStart.get(site.start);
53
+ if (!record) continue;
54
+ const family = familyOf(callee, site);
55
+ const key = JSON.stringify([definition.file, site.start, callee.file, callee.startLine,
56
+ callee.sourceSpan?.start, callee.sourceSpan?.end, callee.name, callee.className,
57
+ callee.type, callee.paramsStructured]);
58
+ let result = checked.get(key);
59
+ if (!result) {
60
+ result = verifyCallSites(index, callee, [{
61
+ ...record, file: definition.file, relativePath: relative,
62
+ content: data.lines[site.line - 1] || '', calledAs: record.name,
63
+ provenance: site.provenance,
64
+ callerFile: definition.file, callerStartLine: definition.startLine,
65
+ }]);
66
+ checked.set(key, result);
67
+ }
68
+ for (const mismatch of result.mismatchDetails || []) {
69
+ const counts = String(mismatch.expected || '').match(/\d+/g);
70
+ const suppressed = judge.suppressed(mismatch, {
71
+ kind: 'arity', name: callee.name,
72
+ most: counts ? Number(counts[counts.length - 1]) : undefined,
73
+ span: mismatch.span, problem: mismatch.problem, indirect: mismatch.indirect,
74
+ });
75
+ // JavaScript deliberately permits omitted and extra
76
+ // arguments. A declared count is not runtime proof.
77
+ const flexible = langTraits(index.files.get(definition.file)?.language)?.lenientCallArity === true;
78
+ if ((callee.decorators || []).length) uncertain(site, callee.name, 'decorated-call-interface');
79
+ else if (flexible) uncertain(site, callee.name, 'call-arity-not-enforced');
80
+ else issues.push({ family, site, callee, mismatch: {
81
+ ...mismatch, column: site.column, target: callee.name, ...(suppressed && { unchecked: true }),
82
+ } });
83
+ }
84
+ if (result.valid > 0 || result.uncertain > 0) possibleFamilies.add(family);
85
+ for (const row of result.uncertainDetails || []) issues.push({ family, site, callee, reason: row.reason });
86
+ if (result.valid > 0) {
87
+ const preserved = changedRanges?.some(range => range.stableInvocation &&
88
+ range.start <= site.start && range.end >= (record.callEnd ?? record.callSite?.end ?? site.start));
89
+ if (positivelyFits(index, callee, definition, record, result, preserved)) positiveFamilies.add(family);
90
+ else issues.push({ family, site, callee, reason: 'argument-compatibility-unverified' });
91
+ }
92
+ }
93
+ }
94
+ // Overload declarations describe one call interface. A positively
95
+ // fitting alternative settles it; a merely possible alternative
96
+ // prevents a different signature's arity from claiming a break.
97
+ for (const issue of issues) {
98
+ if (positiveFamilies.has(issue.family)) continue;
99
+ if (issue.mismatch && !possibleFamilies.has(issue.family)) mismatches.push(issue.mismatch);
100
+ else uncertain(issue.site, issue.callee.name, issue.reason || 'overload-compatibility-unverified');
101
+ }
102
+ for (const entry of [...(callees.unverifiedCallees || []), ...(callees.initializerReviews || [])]) {
103
+ // External packages are outside this fit check. A candidate
104
+ // project declaration, however, cannot silently become READY.
105
+ if (!(entry.ownerCount > 0 || entry.initializerOf || index.symbols.has(entry.name))) continue;
106
+ for (const site of entry.siteProvenance || []) {
107
+ if (byStart.has(site.start)) uncertain(site, entry.name, entry.reason || 'outgoing-target-unresolved');
108
+ }
109
+ }
110
+ const unique = rows => [...new Map(rows.map(row => [JSON.stringify(row), row])).values()]
111
+ .sort((a, b) => a.line - b.line || (a.column || 0) - (b.column || 0) || ((a.name || a.target || '') < (b.name || b.target || '') ? -1 : (a.name || a.target || '') > (b.name || b.target || '') ? 1 : 0));
112
+ return { mismatches: unique(mismatches), unsettled: unique(unsettled) };
113
+ };
114
+ }
115
+
116
+ module.exports = { outgoingChecker };
@@ -10,8 +10,8 @@
10
10
 
11
11
  const SECTIONS = [
12
12
  ['blocked', 'BLOCKED', 'a break was observed'],
13
- ['review', 'REVIEW', 'can reach existing code; not settled by the engine'],
14
- ['ready', 'READY', 'nothing that existed depends on the change'],
13
+ ['review', 'REVIEW', 'change exposure or call requirements not settled'],
14
+ ['ready', 'READY', 'no unresolved exposure in the modeled checks'],
15
15
  ];
16
16
  // Occurrences of a name are context, not a worklist: a head is enough.
17
17
  const NAME_SITE_LIMIT = 10;
@@ -50,6 +50,9 @@ function itemLines(it) {
50
50
  const expression = m.expression ? ` ${String(m.expression).trim()}` : '';
51
51
  lines.push(` ↳ ${where}${expression} ${problem}${m.unchecked ? ' (not checked by the toolchain)' : ''}`);
52
52
  }
53
+ for (const site of it.outgoingReview || []) {
54
+ lines.push(` ↳ ${site.file}:${site.line}${site.column != null ? `:${site.column + 1}` : ''} ${site.name}: ${site.reason}`);
55
+ }
53
56
  const sites = it.sites || [];
54
57
  const definite = it.verdict === 'blocked';
55
58
  const shown = definite ? sites : sites.slice(0, NAME_SITE_LIMIT);
@@ -218,6 +218,8 @@ function formatDeadcode(results, options = {}) {
218
218
  !results.excludedDynamicDispatch && !results.excludedRequiredSlot &&
219
219
  results.projectKind !== 'application' && !results.computedDispatch?.count &&
220
220
  !results.reflection?.withheld && !results.reflection?.dynamicCount && !results.reflection?.literalCount &&
221
+ !results.reflection?.withheldByTable?.length && !results.macroPaste?.withheld &&
222
+ !results.macroPaste?.blind?.count && !results.macroRulesBlind?.count &&
221
223
  results.coverage?.complete !== false) {
222
224
  return 'No dead code found.';
223
225
  }
@@ -233,6 +235,8 @@ function formatDeadcode(results, options = {}) {
233
235
  } else {
234
236
  lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
235
237
  }
238
+ } else {
239
+ lines.push('Dead code: 0 unused symbol(s)\n');
236
240
  }
237
241
 
238
242
  let currentFile = null;
@@ -438,6 +438,9 @@ function formatTests(tests, name) {
438
438
  }
439
439
  if (testFile.run) lines.push(` run: ${selectorText(testFile.run)}`);
440
440
  else if (testFile.role === 'tests' && testFile.runWithheld) lines.push(` no command: ${testFile.runWithheld}`);
441
+ for (const unit of testFile.units || []) {
442
+ if (unit.selector) lines.push(` run test: ${selectorText(unit.selector)}`);
443
+ }
441
444
  lines.push('');
442
445
  }
443
446
  }
@@ -619,7 +619,7 @@ function formatAffectedTests(result, options = {}) {
619
619
  }
620
620
  const pat = result.possiblyAffectedTests || [];
621
621
  if (pat.length > 0) {
622
- lines.push(` Additional test files (${pat.length}):`);
622
+ lines.push(` Test files with possible links (${pat.length}):`);
623
623
  const MAX_POSSIBLE = options.all ? Infinity : 10;
624
624
  for (const tf of pat.slice(0, MAX_POSSIBLE)) {
625
625
  lines.push(` ${tf.file} (links: ${linkList(tf.linkedFunctions, options)})`);