ucn 5.4.2 → 5.5.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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/deadcode.js
CHANGED
|
@@ -13,7 +13,9 @@ const { isTestFile } = require('./discovery');
|
|
|
13
13
|
const { isFrameworkEntrypoint } = require('./entrypoints');
|
|
14
14
|
const { splitParentList } = require('./graph-build');
|
|
15
15
|
const { isOverrideMarked, codeUnitCompare, lineInRanges, maskBlockComments, escapeRegExp } = require('./shared');
|
|
16
|
-
const { projectComputedDispatch,
|
|
16
|
+
const { projectComputedDispatch, projectReflectionSites } = require('./ast-analysis');
|
|
17
|
+
const { reflectionPatternReferences } = require('./reflection');
|
|
18
|
+
const { protocolMemberOf, protocolTypeOf, typedLiteralContract } = require('./contract-membership');
|
|
17
19
|
|
|
18
20
|
const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
|
|
19
21
|
|
|
@@ -215,12 +217,27 @@ function overridesOutOfTreeBase(index, symbol) {
|
|
|
215
217
|
if (owners.length <= 1) return true;
|
|
216
218
|
}
|
|
217
219
|
const mods = symbol.modifiers || [];
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
// Language-private spellings (`#x`, Python `__x`) never override a base
|
|
221
|
+
// member; a leading `_` is only a convention in languages that say so
|
|
222
|
+
// (fix #397: Node stream `_transform` hooks).
|
|
223
|
+
const privateName = langTraits(index.files.get(symbol.file)?.language)?.privateMemberName;
|
|
224
|
+
const publicByShape = !mods.includes('private') && !symbol.name.startsWith('#') &&
|
|
225
|
+
(typeof privateName === 'function' ? !privateName(symbol.name) : !symbol.name.startsWith('_'));
|
|
220
226
|
if (publicByShape && _classHasExternalBase(index, symbol)) return true;
|
|
221
227
|
return false;
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
/**
|
|
231
|
+
* A member of an object literal declared with an out-of-project type
|
|
232
|
+
* (`const traps: ProxyHandler<T> = { get() {} }`, fix #397): the type's
|
|
233
|
+
* consumer (the runtime, a library) calls it by name, so a zero in-project
|
|
234
|
+
* usage count is not evidence of deadness.
|
|
235
|
+
*/
|
|
236
|
+
function typedLiteralExternalMember(index, symbol) {
|
|
237
|
+
if (!symbol.registryContainerType || symbol.className) return false;
|
|
238
|
+
return !!typedLiteralContract(index, symbol)?.external;
|
|
239
|
+
}
|
|
240
|
+
|
|
224
241
|
// Go satisfies interfaces implicitly, so no `implements` edge exists for
|
|
225
242
|
// compiler/stdlib callbacks. A zero textual usage for one of these exact
|
|
226
243
|
// method shapes is not evidence of deadness (sort.Interface, http.Handler,
|
|
@@ -626,6 +643,15 @@ function symbolIsExported(index, symbol, fileEntry) {
|
|
|
626
643
|
if (fileEntry.exports.includes(name) || mods.includes('export') || mods.includes('public')) {
|
|
627
644
|
return true;
|
|
628
645
|
}
|
|
646
|
+
// fix #379: a C++ member that follows a macro invocation whose access
|
|
647
|
+
// effect the parser could not read (the macro is defined in another
|
|
648
|
+
// file) takes the access its project definitions spell; an external or
|
|
649
|
+
// configuration-dependent macro leaves it unknown, and an unknown-access
|
|
650
|
+
// member is treated as public API.
|
|
651
|
+
if (symbol.accessAfterMacro && symbol.className) {
|
|
652
|
+
const access = memberAccessAfterMacro(index, symbol.accessAfterMacro);
|
|
653
|
+
if (access === null || access === 'public') return true;
|
|
654
|
+
}
|
|
629
655
|
const traits = langTraits(fileEntry.language);
|
|
630
656
|
// Python's language-level public surface is every top-level non-underscore
|
|
631
657
|
// name when __all__ is absent. Apply the same rule here as `api`: deadcode
|
|
@@ -645,6 +671,22 @@ function symbolIsExported(index, symbol, fileEntry) {
|
|
|
645
671
|
if (traits?.exportVisibility === 'capitalization') {
|
|
646
672
|
return /^[A-Z]/.test(name);
|
|
647
673
|
}
|
|
674
|
+
// fix #367f: members of an interface/trait carry the container's
|
|
675
|
+
// visibility (Rust trait items cannot be marked `pub`; Java and C#
|
|
676
|
+
// interface members are implicitly public), so the members of an
|
|
677
|
+
// exported interface/trait are public API exactly as `api` lists them.
|
|
678
|
+
if (symbol.className && !mods.includes('private')) {
|
|
679
|
+
const container = (index.symbols.get(symbol.className) || []).find(c =>
|
|
680
|
+
c.file === symbol.file && (c.type === 'interface' || c.type === 'trait') &&
|
|
681
|
+
c.startLine <= symbol.startLine && symbol.startLine <= (c.endLine ?? c.startLine));
|
|
682
|
+
if (container) {
|
|
683
|
+
const cmods = container.modifiers || [];
|
|
684
|
+
if (fileEntry.exports.includes(container.name) || cmods.includes('export') ||
|
|
685
|
+
cmods.includes('public') || cmods.includes('pub')) {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
648
690
|
if (traits?.implicitlyPublicMembers && symbol.className &&
|
|
649
691
|
!mods.includes('private') && !name.startsWith('#') && !name.startsWith('_')) {
|
|
650
692
|
const classSyms = index.symbols.get(symbol.className) || [];
|
|
@@ -661,6 +703,21 @@ function symbolIsExported(index, symbol, fileEntry) {
|
|
|
661
703
|
return false;
|
|
662
704
|
}
|
|
663
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Access in effect after `macro` for a member whose lexical access before the
|
|
708
|
+
* invocation was `before`: the last access specifier every project
|
|
709
|
+
* definition of the macro spells (or `before` when none spells one). Null
|
|
710
|
+
* when the macro has no readable project definition or its definitions
|
|
711
|
+
* disagree.
|
|
712
|
+
*/
|
|
713
|
+
function memberAccessAfterMacro(index, { macro, before } = {}) {
|
|
714
|
+
const { macroAccessEffect } = require('../languages/c-family');
|
|
715
|
+
const definitions = (index.symbols.get(macro) || []).filter(d => d.type === 'macro');
|
|
716
|
+
if (definitions.length === 0 || definitions.some(d => typeof d.ppBody !== 'string')) return null;
|
|
717
|
+
const effects = new Set(definitions.map(d => macroAccessEffect(d.ppBody) || before || null));
|
|
718
|
+
return effects.size === 1 ? [...effects][0] : null;
|
|
719
|
+
}
|
|
720
|
+
|
|
664
721
|
function isPythonPackageFile(index, fileEntry) {
|
|
665
722
|
if (fileEntry?.language !== 'python' || !fileEntry.relativePath) return false;
|
|
666
723
|
let dir = path.dirname(path.join(index.root, fileEntry.relativePath));
|
|
@@ -674,40 +731,6 @@ function isPythonPackageFile(index, fileEntry) {
|
|
|
674
731
|
return false;
|
|
675
732
|
}
|
|
676
733
|
|
|
677
|
-
const JAVA_SERIALIZATION_CALLBACKS = new Set([
|
|
678
|
-
'readResolve', 'writeReplace', 'readObject', 'writeObject',
|
|
679
|
-
'readObjectNoData',
|
|
680
|
-
]);
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Java serialization invokes these callbacks reflectively, so there is no
|
|
684
|
-
* text call edge. Require a Serializable/Externalizable heritage path before
|
|
685
|
-
* suppressing a dead-code claim; name alone is not enough.
|
|
686
|
-
*/
|
|
687
|
-
function isJavaSerializationCallback(index, symbol, fileEntry) {
|
|
688
|
-
if (fileEntry?.language !== 'java' || !symbol.className ||
|
|
689
|
-
!JAVA_SERIALIZATION_CALLBACKS.has(symbol.name)) return false;
|
|
690
|
-
const head = value => String(value || '')
|
|
691
|
-
.replace(/<.*$/, '').split(/[.$]/).pop().trim();
|
|
692
|
-
const seen = new Set();
|
|
693
|
-
const serializable = className => {
|
|
694
|
-
const normalized = head(className);
|
|
695
|
-
if (!normalized || seen.has(normalized)) return false;
|
|
696
|
-
if (normalized === 'Serializable' || normalized === 'Externalizable') return true;
|
|
697
|
-
seen.add(normalized);
|
|
698
|
-
const defs = (index.symbols.get(normalized) || []).filter(candidate =>
|
|
699
|
-
['class', 'interface', 'record', 'enum'].includes(candidate.type));
|
|
700
|
-
return defs.some(definition => {
|
|
701
|
-
const implemented = Array.isArray(definition.implements)
|
|
702
|
-
? definition.implements : String(definition.implements || '').split(',');
|
|
703
|
-
const parents = [definition.extends, ...implemented]
|
|
704
|
-
.filter(Boolean).map(head);
|
|
705
|
-
return parents.some(serializable);
|
|
706
|
-
});
|
|
707
|
-
};
|
|
708
|
-
return serializable(symbol.className);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
734
|
/**
|
|
712
735
|
* Find dead code (unused functions/classes)
|
|
713
736
|
* @param {object} index - ProjectIndex instance
|
|
@@ -742,29 +765,48 @@ function deadcode(index, options = {}) {
|
|
|
742
765
|
const reflectionInfo = {
|
|
743
766
|
count: 0,
|
|
744
767
|
literalCount: 0,
|
|
768
|
+
patternCount: 0,
|
|
745
769
|
dynamicCount: 0,
|
|
746
770
|
fileCount: 0,
|
|
747
771
|
files: [],
|
|
748
772
|
names: [],
|
|
773
|
+
patterns: [],
|
|
774
|
+
withheld: 0,
|
|
775
|
+
withheldByPattern: [],
|
|
749
776
|
};
|
|
750
|
-
|
|
777
|
+
// Reflection is liveness evidence wherever it sits (a test or script
|
|
778
|
+
// outside --in still reaches the member); the disclosure counts cover the
|
|
779
|
+
// audited scope.
|
|
780
|
+
const reflectionPatterns = new Set();
|
|
781
|
+
for (const [reflectionFile, sites] of projectReflectionSites(index)) {
|
|
782
|
+
const fe = index.files.get(reflectionFile);
|
|
783
|
+
if (!fe || sites.length === 0) continue;
|
|
784
|
+
for (const site of sites) {
|
|
785
|
+
if (site.name) literalReflectionNames.add(site.name);
|
|
786
|
+
if (site.patterns && site.patterns.length > 0 && !site.name) {
|
|
787
|
+
// Exact alternatives of a multi-spelling site are literals.
|
|
788
|
+
for (const pattern of site.patterns) {
|
|
789
|
+
if (!pattern.includes('*')) literalReflectionNames.add(pattern);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
751
793
|
if (!index.matchesFilters(fe.relativePath, options)) continue;
|
|
752
|
-
let sites = [];
|
|
753
|
-
try {
|
|
754
|
-
sites = reflectionSites(index._readFile(reflectionFile), fe.language);
|
|
755
|
-
} catch { /* coverage diagnostics own unreadable-file reporting */ }
|
|
756
|
-
if (sites.length === 0) continue;
|
|
757
794
|
reflectionInfo.count += sites.length;
|
|
758
|
-
|
|
759
|
-
|
|
795
|
+
for (const site of sites) {
|
|
796
|
+
if (site.name) reflectionInfo.literalCount++;
|
|
797
|
+
else if (site.patterns && site.patterns.length > 0) reflectionInfo.patternCount++;
|
|
798
|
+
if (site.dynamic) reflectionInfo.dynamicCount++;
|
|
799
|
+
for (const pattern of site.patterns || []) {
|
|
800
|
+
if (pattern.includes('*')) reflectionPatterns.add(pattern);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
760
803
|
reflectionInfo.fileCount++;
|
|
761
804
|
if (reflectionInfo.files.length < 10) {
|
|
762
805
|
reflectionInfo.files.push(fe.relativePath);
|
|
763
806
|
}
|
|
764
|
-
for (const site of sites) {
|
|
765
|
-
if (site.name) literalReflectionNames.add(site.name);
|
|
766
|
-
}
|
|
767
807
|
}
|
|
808
|
+
reflectionInfo.patterns = [...reflectionPatterns].sort(codeUnitCompare);
|
|
809
|
+
const reflectionWithheldByPattern = new Map();
|
|
768
810
|
reflectionInfo.names = [...literalReflectionNames].sort(codeUnitCompare);
|
|
769
811
|
|
|
770
812
|
// Ensure callee index is built (lazy, reused across operations)
|
|
@@ -845,10 +887,48 @@ function deadcode(index, options = {}) {
|
|
|
845
887
|
potentiallyDeadNames.delete(reflectedName);
|
|
846
888
|
}
|
|
847
889
|
|
|
890
|
+
// C/C++ token-pasting macros (fix #362): names an expanded invocation
|
|
891
|
+
// produces in value position (`{ #name, handle_##name }` tables) are
|
|
892
|
+
// uses. A paste pattern whose produced names cannot be enumerated (a
|
|
893
|
+
// pasting macro with a blind invocation, or one never seen expanded)
|
|
894
|
+
// withholds every candidate the pattern could spell.
|
|
895
|
+
const macroPaste = { sites: 0, blind: null, withheld: 0, withheldNames: [], patterns: 0 };
|
|
896
|
+
const macroSuppressedNames = new Set();
|
|
897
|
+
if ([...index.files.values()].some(fe => langTraits(fe.language)?.textualIncludes)) {
|
|
898
|
+
const { macroExpansionSummary } = require('./macro-expansion');
|
|
899
|
+
const { matchesPastePattern } = require('../languages/c-preprocessor');
|
|
900
|
+
const summary = macroExpansionSummary(index);
|
|
901
|
+
macroPaste.sites = summary.sites;
|
|
902
|
+
macroPaste.blind = summary.blind;
|
|
903
|
+
for (const name of summary.refNames) {
|
|
904
|
+
potentiallyDeadNames.delete(name);
|
|
905
|
+
macroSuppressedNames.add(name);
|
|
906
|
+
}
|
|
907
|
+
const patterns = summary.patterns.filter(p => p.pattern.some(piece => piece && piece.length > 0));
|
|
908
|
+
macroPaste.patterns = patterns.length;
|
|
909
|
+
if (patterns.length > 0) {
|
|
910
|
+
for (const name of [...potentiallyDeadNames].sort(codeUnitCompare)) {
|
|
911
|
+
const cFamily = (index.symbols.get(name) || []).some(def =>
|
|
912
|
+
langTraits(index.files.get(def.file)?.language)?.textualIncludes);
|
|
913
|
+
if (!cFamily) continue;
|
|
914
|
+
const pattern = patterns.find(p => matchesPastePattern(p.pattern, name));
|
|
915
|
+
if (!pattern) continue;
|
|
916
|
+
potentiallyDeadNames.delete(name);
|
|
917
|
+
macroSuppressedNames.add(name);
|
|
918
|
+
macroPaste.withheld++;
|
|
919
|
+
if (macroPaste.withheldNames.length < 10) {
|
|
920
|
+
macroPaste.withheldNames.push({ name, macro: pattern.macro,
|
|
921
|
+
file: pattern.file, line: pattern.line });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
848
927
|
const coverage = scanDeadcodeCoverage(index, potentiallyDeadNames);
|
|
849
928
|
const coverageSuppressedNames = new Set([
|
|
850
929
|
...coverage.matchedNames,
|
|
851
930
|
...literalReflectionNames,
|
|
931
|
+
...macroSuppressedNames,
|
|
852
932
|
]);
|
|
853
933
|
if (coverage.claimsWithdrawn) {
|
|
854
934
|
// An unreadable file/directory can contain a use of any candidate.
|
|
@@ -1205,6 +1285,12 @@ function deadcode(index, options = {}) {
|
|
|
1205
1285
|
if (symbol.bodyScopedName) {
|
|
1206
1286
|
continue;
|
|
1207
1287
|
}
|
|
1288
|
+
// Generated by a project macro_rules! invocation (fix #374): the
|
|
1289
|
+
// definition is the macro's template or the invocation's
|
|
1290
|
+
// arguments, never a deletable declaration of its own.
|
|
1291
|
+
if (symbol.macroExpansion) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1208
1294
|
const fileEntry = index.files.get(symbol.file);
|
|
1209
1295
|
const lang = fileEntry?.language;
|
|
1210
1296
|
|
|
@@ -1267,7 +1353,11 @@ function deadcode(index, options = {}) {
|
|
|
1267
1353
|
if (langModule.isEntryPoint?.(symbol)) {
|
|
1268
1354
|
continue;
|
|
1269
1355
|
}
|
|
1270
|
-
|
|
1356
|
+
// Members the runtime/compiler invokes by name (trait
|
|
1357
|
+
// languageProtocolMember): Java serialization callbacks on
|
|
1358
|
+
// Serializable types, Python Enum hooks on Enum subclasses, C#
|
|
1359
|
+
// pattern-bound members, JS toJSON/then/[Symbol.*] (fix #363).
|
|
1360
|
+
if (protocolMemberOf(index, symbol) || protocolTypeOf(index, symbol)) {
|
|
1271
1361
|
excludedRuntimeContract++;
|
|
1272
1362
|
continue;
|
|
1273
1363
|
}
|
|
@@ -1422,6 +1512,7 @@ function deadcode(index, options = {}) {
|
|
|
1422
1512
|
const isExternalContract = classAuditSet.has(symbol.type)
|
|
1423
1513
|
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
1424
1514
|
: overridesOutOfTreeBase(index, symbol) ||
|
|
1515
|
+
typedLiteralExternalMember(index, symbol) ||
|
|
1425
1516
|
(lang === 'go' && goImplicitExternalContract(symbol));
|
|
1426
1517
|
if (isExternalContract && !options.includeExported) {
|
|
1427
1518
|
excludedExternalContract++;
|
|
@@ -1472,6 +1563,22 @@ function deadcode(index, options = {}) {
|
|
|
1472
1563
|
continue;
|
|
1473
1564
|
}
|
|
1474
1565
|
|
|
1566
|
+
// Reflection by name pattern (fix #363): a reflective access
|
|
1567
|
+
// whose name is built from literal fragments around a runtime
|
|
1568
|
+
// part (`getattr(self, "_get_%s_permissions" % src)`) can
|
|
1569
|
+
// reach every member the pattern spells on a receiver that may
|
|
1570
|
+
// hold this class. That is a runtime use, not deadness.
|
|
1571
|
+
const reflected = reflectionPatternReferences(index, symbol);
|
|
1572
|
+
if (reflected.length > 0) {
|
|
1573
|
+
const first = reflected[0];
|
|
1574
|
+
const pattern = first.site.receiver?.kind === 'module'
|
|
1575
|
+
? `${first.pattern}\u0000${first.relativePath}` : first.pattern;
|
|
1576
|
+
reflectionWithheldByPattern.set(pattern,
|
|
1577
|
+
(reflectionWithheldByPattern.get(pattern) || 0) + 1);
|
|
1578
|
+
reflectionInfo.withheld++;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1475
1582
|
results.push({
|
|
1476
1583
|
name: symbol.name,
|
|
1477
1584
|
type: symbol.type,
|
|
@@ -1508,7 +1615,22 @@ function deadcode(index, options = {}) {
|
|
|
1508
1615
|
results.excludedRuntimeContract = excludedRuntimeContract;
|
|
1509
1616
|
results.excludedDynamicDispatch = excludedDynamicDispatch;
|
|
1510
1617
|
results.computedDispatch = computedDispatchInfo;
|
|
1618
|
+
reflectionInfo.withheldByPattern = [...reflectionWithheldByPattern]
|
|
1619
|
+
.map(([key, count]) => {
|
|
1620
|
+
const [pattern, namespaceOf] = key.split('\u0000');
|
|
1621
|
+
return { pattern, count, ...(namespaceOf && { namespaceOf }) };
|
|
1622
|
+
})
|
|
1623
|
+
.sort((a, b) => b.count - a.count || codeUnitCompare(a.pattern, b.pattern));
|
|
1511
1624
|
results.reflection = reflectionInfo;
|
|
1625
|
+
if (macroPaste.sites > 0 || macroPaste.blind?.count > 0 || macroPaste.withheld > 0) {
|
|
1626
|
+
results.macroPaste = macroPaste;
|
|
1627
|
+
}
|
|
1628
|
+
// Project macro_rules! invocations that could not be expanded (fix #374).
|
|
1629
|
+
if ([...index.files.values()].some(fe => fe.rustMacroExpansion?.blind)) {
|
|
1630
|
+
const { rustMacroExpansionSummary } = require('./rust-macro-expansion');
|
|
1631
|
+
const summary = rustMacroExpansionSummary(index);
|
|
1632
|
+
if (summary.blind.count > 0) results.macroRulesBlind = summary.blind;
|
|
1633
|
+
}
|
|
1512
1634
|
results.coverage = {
|
|
1513
1635
|
complete: coverage.complete,
|
|
1514
1636
|
claimsWithdrawn: coverage.claimsWithdrawn,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/decorator-binding.js - Decorators that may bind a callable by its name
|
|
3
|
+
* (fix #392).
|
|
4
|
+
*
|
|
5
|
+
* Where a decorator replaces the callable (`decoratorsWrapCallables`: Python,
|
|
6
|
+
* JS/TS), it receives the function object, and with it the function's NAME:
|
|
7
|
+
* a pytest fixture is requested by it, a click command and a Flask endpoint
|
|
8
|
+
* are named after it, a registry may key on `fn.__name__`, a TS member
|
|
9
|
+
* decorator receives the property key. Renaming the definition and its
|
|
10
|
+
* references is then not the whole rename. A decorator is known to keep the
|
|
11
|
+
* name out of it when:
|
|
12
|
+
* - it resolves (through the file's import bindings or builtins) to one of
|
|
13
|
+
* the language's name-preserving decorators (trait `callableDecorators`:
|
|
14
|
+
* `transparent`, `asyncContextManagers`, `namePreserving`);
|
|
15
|
+
* - it is the member's own property accessor (`@size.setter` on `size`);
|
|
16
|
+
* - it resolves to a project function whose decorated-callable parameter is
|
|
17
|
+
* only called, returned, passed to `functools.wraps`/`update_wrapper`, or
|
|
18
|
+
* tested with `callable`/`isinstance` (Python), or whose name/context
|
|
19
|
+
* parameters are never read (JS/TS); a decorator factory `@f(args)` is
|
|
20
|
+
* proven through the decorator it returns.
|
|
21
|
+
* Any other decorator is reported, so the plan asks for review.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const { langTraits, getParser, safeParse } = require('../languages');
|
|
27
|
+
const { sameNode, traverseTree } = require('../languages/utils');
|
|
28
|
+
const { resolveDecorator } = require('./async-producers');
|
|
29
|
+
|
|
30
|
+
function namedChildrenOf(node) {
|
|
31
|
+
return node ? node.namedChildren || [] : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function treeOf(index, filePath) {
|
|
35
|
+
const entry = index.files.get(filePath);
|
|
36
|
+
if (!entry) return null;
|
|
37
|
+
let content;
|
|
38
|
+
try { content = index._readFile(filePath); } catch { return null; }
|
|
39
|
+
const tree = index._getParsedTree?.(filePath, content, entry.language);
|
|
40
|
+
if (tree) return tree;
|
|
41
|
+
const parser = getParser(entry.language);
|
|
42
|
+
return parser ? safeParse(parser, content) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The function node declared at `line` (1-based) with `name`. */
|
|
46
|
+
function functionNodeAt(tree, line, name) {
|
|
47
|
+
let found = null;
|
|
48
|
+
const row = line - 1;
|
|
49
|
+
traverseTree(tree.rootNode, node => {
|
|
50
|
+
if (found) return false;
|
|
51
|
+
if (node.endPosition.row < row || node.startPosition.row > row) return false;
|
|
52
|
+
const nameNode = node.childForFieldName?.('name');
|
|
53
|
+
if (nameNode && nameNode.text === name && nameNode.startPosition.row === row &&
|
|
54
|
+
/function|method|arrow/.test(node.type)) {
|
|
55
|
+
found = node;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (node.type === 'variable_declarator' && nameNode?.text === name &&
|
|
59
|
+
nameNode.startPosition.row === row) {
|
|
60
|
+
const value = node.childForFieldName('value');
|
|
61
|
+
if (value && /function|arrow/.test(value.type)) {
|
|
62
|
+
found = value;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function firstParamName(fnNode, family) {
|
|
72
|
+
const params = fnNode.childForFieldName('parameters') || fnNode.childForFieldName('parameter');
|
|
73
|
+
if (!params) return null;
|
|
74
|
+
if (params.type === 'identifier') return params.text;
|
|
75
|
+
for (const param of namedChildrenOf(params)) {
|
|
76
|
+
if (param.type === 'identifier') return param.text;
|
|
77
|
+
const nameNode = param.childForFieldName?.('name') || param.childForFieldName?.('pattern');
|
|
78
|
+
if (nameNode?.type === 'identifier') return nameNode.text;
|
|
79
|
+
if (family === 'python' && param.type === 'typed_parameter' &&
|
|
80
|
+
param.namedChild(0)?.type === 'identifier') return param.namedChild(0).text;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function paramNames(fnNode) {
|
|
87
|
+
const params = fnNode.childForFieldName('parameters') || fnNode.childForFieldName('parameter');
|
|
88
|
+
if (!params) return [];
|
|
89
|
+
if (params.type === 'identifier') return [params.text];
|
|
90
|
+
const names = [];
|
|
91
|
+
for (const param of namedChildrenOf(params)) {
|
|
92
|
+
if (param.type === 'identifier') { names.push(param.text); continue; }
|
|
93
|
+
const nameNode = param.childForFieldName?.('name') || param.childForFieldName?.('pattern');
|
|
94
|
+
names.push(nameNode?.type === 'identifier' ? nameNode.text : null);
|
|
95
|
+
}
|
|
96
|
+
return names;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Python: the decorated-callable parameter `p` of decorator `fnNode` is only
|
|
101
|
+
* called, returned, passed to functools.wraps/update_wrapper, or tested.
|
|
102
|
+
*/
|
|
103
|
+
function pythonParamKeepsName(fnNode, p, resolveName) {
|
|
104
|
+
const body = fnNode.childForFieldName('body');
|
|
105
|
+
if (!body) return false;
|
|
106
|
+
let proven = true;
|
|
107
|
+
traverseTree(body, node => {
|
|
108
|
+
if (!proven) return false;
|
|
109
|
+
if (node.type !== 'identifier' || node.text !== p) return true;
|
|
110
|
+
const parent = node.parent;
|
|
111
|
+
if (!parent) { proven = false; return false; }
|
|
112
|
+
// `return p`
|
|
113
|
+
if (parent.type === 'return_statement') return true;
|
|
114
|
+
// `p(...)`
|
|
115
|
+
if (parent.type === 'call' && sameNode(parent.childForFieldName('function'), node)) return true;
|
|
116
|
+
// `wraps(p)`, `update_wrapper(w, p)`, `callable(p)`, `isinstance(p, T)`
|
|
117
|
+
if (parent.type === 'argument_list' && parent.parent?.type === 'call') {
|
|
118
|
+
const callee = parent.parent.childForFieldName('function');
|
|
119
|
+
const key = callee ? resolveName(callee.text) : null;
|
|
120
|
+
if (key === 'functools.wraps' || key === 'functools.update_wrapper' ||
|
|
121
|
+
key === 'builtins.callable' || key === 'builtins.isinstance') return true;
|
|
122
|
+
}
|
|
123
|
+
proven = false;
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
return proven;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** JS/TS: every parameter after the decorated value is unread. */
|
|
130
|
+
function jsExtraParamsUnread(fnNode) {
|
|
131
|
+
const names = paramNames(fnNode).slice(1);
|
|
132
|
+
if (names.some(n => n === null)) return false;
|
|
133
|
+
if (names.length === 0) return true;
|
|
134
|
+
const body = fnNode.childForFieldName('body');
|
|
135
|
+
if (!body) return true;
|
|
136
|
+
let read = false;
|
|
137
|
+
traverseTree(body, node => {
|
|
138
|
+
if (read) return false;
|
|
139
|
+
if (node.type === 'identifier' && names.includes(node.text)) read = true;
|
|
140
|
+
return !read;
|
|
141
|
+
});
|
|
142
|
+
return !read;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The nested decorator a factory returns (`return inner` / `return lambda..`). */
|
|
146
|
+
function returnedDecorator(fnNode, family) {
|
|
147
|
+
const body = fnNode.childForFieldName('body');
|
|
148
|
+
if (!body) return null;
|
|
149
|
+
const nested = new Map();
|
|
150
|
+
const returns = [];
|
|
151
|
+
traverseTree(body, node => {
|
|
152
|
+
if (sameNode(node, body)) return true;
|
|
153
|
+
if (/^(function_definition|function_declaration)$/.test(node.type)) {
|
|
154
|
+
const n = node.childForFieldName('name');
|
|
155
|
+
if (n) nested.set(n.text, node);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (/function|arrow|lambda/.test(node.type)) return false;
|
|
159
|
+
if (node.type === 'variable_declarator') {
|
|
160
|
+
const n = node.childForFieldName('name');
|
|
161
|
+
const v = node.childForFieldName('value');
|
|
162
|
+
if (n && v && /function|arrow/.test(v.type)) nested.set(n.text, v);
|
|
163
|
+
}
|
|
164
|
+
if (node.type === 'return_statement') returns.push(node);
|
|
165
|
+
return true;
|
|
166
|
+
});
|
|
167
|
+
// JS arrow shorthand: `(x) => (target, key) => {}`
|
|
168
|
+
if (family === 'js' && body.type !== 'statement_block' && /function|arrow/.test(body.type)) return body;
|
|
169
|
+
if (returns.length !== 1) return null;
|
|
170
|
+
const value = returns[0].namedChild(0);
|
|
171
|
+
if (!value) return null;
|
|
172
|
+
if (value.type === 'identifier') return nested.get(value.text) || null;
|
|
173
|
+
if (/function|arrow/.test(value.type)) return value;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a decorator's head name to one project callable definition, or null.
|
|
179
|
+
*/
|
|
180
|
+
function projectDecoratorDef(index, def, head, rest) {
|
|
181
|
+
const { _nameBindingReaches, _moduleAttributeBindingReaches } = require('./callers');
|
|
182
|
+
const target = rest ? rest.slice(1) : head;
|
|
183
|
+
if (rest && rest.includes('.', 1)) return null;
|
|
184
|
+
const candidates = (index.symbols.get(target) || []).filter(candidate =>
|
|
185
|
+
!candidate.className && /function|method/.test(candidate.type || 'function'));
|
|
186
|
+
const reaching = candidates.filter(candidate => {
|
|
187
|
+
const targets = new Set([candidate.file]);
|
|
188
|
+
if (!rest) {
|
|
189
|
+
if (candidate.file === def.file) return true;
|
|
190
|
+
return _nameBindingReaches(index, def.file, head, targets) === 'yes';
|
|
191
|
+
}
|
|
192
|
+
return _moduleAttributeBindingReaches(index, def.file, head, target, targets) === 'yes';
|
|
193
|
+
});
|
|
194
|
+
return reaching.length === 1 ? reaching[0] : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function provenProjectDecorator(index, def, text, family, resolveName, memo) {
|
|
198
|
+
const call = text.indexOf('(');
|
|
199
|
+
const expr = (call >= 0 ? text.slice(0, call) : text).trim();
|
|
200
|
+
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/.test(expr)) return false;
|
|
201
|
+
const dot = expr.indexOf('.');
|
|
202
|
+
const head = dot < 0 ? expr : expr.slice(0, dot);
|
|
203
|
+
const rest = dot < 0 ? '' : expr.slice(dot);
|
|
204
|
+
const target = projectDecoratorDef(index, def, head, rest);
|
|
205
|
+
if (!target) return false;
|
|
206
|
+
const key = `${target.file}\0${target.startLine}\0${call >= 0 ? 1 : 0}`;
|
|
207
|
+
if (memo.has(key)) return memo.get(key);
|
|
208
|
+
memo.set(key, false); // recursion guard
|
|
209
|
+
const tree = treeOf(index, target.file);
|
|
210
|
+
let fnNode = tree ? functionNodeAt(tree, target.nameLine || target.startLine, target.name) : null;
|
|
211
|
+
if (fnNode && call >= 0) fnNode = returnedDecorator(fnNode, family);
|
|
212
|
+
let proven = false;
|
|
213
|
+
if (fnNode) {
|
|
214
|
+
if (family === 'python') {
|
|
215
|
+
const p = firstParamName(fnNode, family);
|
|
216
|
+
const targetEntry = index.files.get(target.file);
|
|
217
|
+
const vocab = langTraits(targetEntry?.language)?.callableDecorators || {};
|
|
218
|
+
const resolveInTarget = name => resolveDecorator(name, targetEntry, vocab)?.name || null;
|
|
219
|
+
proven = !!p && pythonParamKeepsName(fnNode, p, resolveInTarget);
|
|
220
|
+
} else {
|
|
221
|
+
proven = jsExtraParamsUnread(fnNode);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
memo.set(key, proven);
|
|
225
|
+
return proven;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Decorators of `def` not proven to keep its name out of what they do.
|
|
230
|
+
* Returns [] when the language's decorators never replace the callable.
|
|
231
|
+
*/
|
|
232
|
+
function nameBindingDecorators(index, def, memo = new Map()) {
|
|
233
|
+
const decorators = Array.isArray(def.decorators) ? def.decorators : [];
|
|
234
|
+
if (decorators.length === 0) return [];
|
|
235
|
+
const fileEntry = index.files.get(def.file);
|
|
236
|
+
const traits = fileEntry ? langTraits(fileEntry.language) : null;
|
|
237
|
+
if (!traits?.decoratorsWrapCallables) return [];
|
|
238
|
+
const family = fileEntry.language === 'python' ? 'python' : 'js';
|
|
239
|
+
const vocab = traits.callableDecorators || {};
|
|
240
|
+
const preserving = new Set([...(vocab.transparent || []), ...(vocab.asyncContextManagers || []),
|
|
241
|
+
...(vocab.namePreserving || [])]);
|
|
242
|
+
const resolveName = name => resolveDecorator(name, fileEntry, vocab)?.name || null;
|
|
243
|
+
const unproven = [];
|
|
244
|
+
for (const raw of decorators) {
|
|
245
|
+
const text = String(typeof raw === 'string' ? raw : raw?.name || '').replace(/^@/, '').trim();
|
|
246
|
+
if (!text) continue;
|
|
247
|
+
const resolved = resolveDecorator(text, fileEntry, vocab);
|
|
248
|
+
if (resolved) {
|
|
249
|
+
const key = resolved.name + (resolved.called ? '()' : '');
|
|
250
|
+
if (preserving.has(key) || preserving.has(resolved.name)) continue;
|
|
251
|
+
}
|
|
252
|
+
// The member's own property accessors keep its name by definition.
|
|
253
|
+
if (new RegExp(`^${def.name.replace(/[$]/g, '\\$')}\\.(setter|getter|deleter)$`).test(text)) continue;
|
|
254
|
+
if (provenProjectDecorator(index, def, text, family, resolveName, memo)) continue;
|
|
255
|
+
unproven.push(text);
|
|
256
|
+
}
|
|
257
|
+
return unproven;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { nameBindingDecorators };
|