ucn 4.1.2 → 4.1.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/core/cache.js +5 -1
- package/core/deadcode.js +83 -22
- package/languages/java.js +18 -8
- package/package.json +1 -1
package/core/cache.js
CHANGED
|
@@ -124,7 +124,11 @@ const UCN_VERSION = require('../package.json').version;
|
|
|
124
124
|
// v47 (fix #269): Python absolute imports resolve through the PEP-517 src
|
|
125
125
|
// layout (`import click` → src/click/__init__.py) — persisted
|
|
126
126
|
// importGraph/exportGraph/moduleResolved content changed.
|
|
127
|
-
|
|
127
|
+
// v48 (fix #270): Java interface symbols record their extends clause (the
|
|
128
|
+
// grammar's extends_interfaces child carries no `extends` field, so the
|
|
129
|
+
// field lookup silently returned nothing) — cached interface symbols lack
|
|
130
|
+
// the supertypes the deadcode heritage walk reads.
|
|
131
|
+
const CACHE_FORMAT_VERSION = 48;
|
|
128
132
|
|
|
129
133
|
/**
|
|
130
134
|
* Save index to cache file
|
package/core/deadcode.js
CHANGED
|
@@ -53,32 +53,90 @@ function _baseIsExternal(index, bare, lang) {
|
|
|
53
53
|
return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const supers = Array.isArray(classDef.extends) ? classDef.extends : splitParentList(classDef.extends);
|
|
60
|
-
return supers.some(raw => _baseIsExternal(index, _bareBaseName(raw), lang));
|
|
61
|
-
}
|
|
56
|
+
// Bounded heritage-closure depth (fix #270) — matches the engine's other
|
|
57
|
+
// inheritance walks.
|
|
58
|
+
const _HERITAGE_WALK_DEPTH = 8;
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
|
-
* Does
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
61
|
+
* Does this CLASS DEF reach at least one base that is NOT in the project
|
|
62
|
+
* index, walking `extends` chains transitively THROUGH resolved project types
|
|
63
|
+
* (fix #270)? One level was not enough — fastify-measured: CustomLoggerImpl
|
|
64
|
+
* implements CustomLogger (project) extends FastifyBaseLogger (project,
|
|
65
|
+
* .d.ts) extends Pick<BaseLogger, ...'silent'> (pino — external). The member
|
|
66
|
+
* surface the class must provide comes from the OUT-OF-TREE end of the chain,
|
|
67
|
+
* invisible without tsc, so a zero-usage impl member (`silent`) is contract
|
|
68
|
+
* surface — deleting it breaks the build. Same dispatch physics as one level;
|
|
69
|
+
* only the verdict moved from "direct parent external" to "heritage closure
|
|
70
|
+
* reaches external".
|
|
69
71
|
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
72
|
+
* `followImplements` additionally walks the def's OWN `implements` clause
|
|
73
|
+
* (first hop only — the measured family; deeper implements hops are
|
|
74
|
+
* classified-deferred). Method claims pass true only when the member sits
|
|
75
|
+
* INSIDE the def's source range: TS/Java members live in the class body, so
|
|
76
|
+
* the implements contract constrains them. Rust surfaces trait impls as
|
|
77
|
+
* `implements` on the struct (rust.js), but inherent methods live in separate
|
|
78
|
+
* `impl X` blocks OUTSIDE the struct's range — an unrelated `impl Display
|
|
79
|
+
* for X` must never shield a genuinely-dead inherent method (the original
|
|
80
|
+
* reason implements was not consulted here at all; trait-impl members
|
|
81
|
+
* themselves carry traitImpl/@Override markers → Rule A). Class-kind claims
|
|
82
|
+
* pass false: deleting the whole class removes its implements clause with it.
|
|
83
|
+
*/
|
|
84
|
+
function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
let frontier = [classDef];
|
|
87
|
+
for (let depth = 0; depth < _HERITAGE_WALK_DEPTH && frontier.length; depth++) {
|
|
88
|
+
const next = [];
|
|
89
|
+
for (const def of frontier) {
|
|
90
|
+
// Copy array-shaped extends — the implements push below must
|
|
91
|
+
// never mutate the symbol's own heritage data in the index.
|
|
92
|
+
const parents = def.extends
|
|
93
|
+
? (Array.isArray(def.extends) ? [...def.extends] : splitParentList(def.extends))
|
|
94
|
+
: [];
|
|
95
|
+
if (depth === 0 && followImplements && Array.isArray(def.implements)) {
|
|
96
|
+
parents.push(...def.implements);
|
|
97
|
+
}
|
|
98
|
+
for (const raw of parents) {
|
|
99
|
+
const bare = _bareBaseName(raw);
|
|
100
|
+
if (!bare || seen.has(bare)) continue;
|
|
101
|
+
seen.add(bare);
|
|
102
|
+
if (_baseIsExternal(index, bare, lang)) return true;
|
|
103
|
+
for (const pd of index.symbols.get(bare) || []) {
|
|
104
|
+
if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
frontier = next;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Does the method's enclosing class reach at least one base that is NOT in
|
|
115
|
+
* the project index through its heritage closure? An out-of-tree base is a
|
|
116
|
+
* framework/library type UCN can't see; via inheritance it may dispatch into
|
|
117
|
+
* a public method of the subclass polymorphically (Starlette →
|
|
118
|
+
* build_middleware_stack), by name convention (Pydantic → bytes_schema), or
|
|
119
|
+
* REQUIRE the member outright (fastify → CustomLoggerImpl.silent, via an
|
|
120
|
+
* implements chain ending in pino). The class def is matched in the method's
|
|
121
|
+
* own file; `implements` is walked only when the member sits inside the class
|
|
122
|
+
* def's own range (see _heritageReachesExternalBase — the Rust guard).
|
|
76
123
|
*/
|
|
77
124
|
function _classHasExternalBase(index, symbol) {
|
|
78
125
|
const lang = index.files.get(symbol.file)?.language;
|
|
126
|
+
// The implements hop shields only contract-SATISFIABLE members: an
|
|
127
|
+
// interface implementation must be public, so in explicit-visibility
|
|
128
|
+
// languages a package-private/non-pub member provably cannot implement
|
|
129
|
+
// any interface member (javac rejects it) — `implements Runnable` never
|
|
130
|
+
// shields a package-private helper. Implicit-public languages satisfy
|
|
131
|
+
// contracts with unmarked members; the caller's public-by-shape check
|
|
132
|
+
// already screened those. Compiler physics, not a heuristic.
|
|
133
|
+
const contractSatisfiable = langTraits(lang)?.implicitlyPublicMembers ||
|
|
134
|
+
(symbol.modifiers || []).includes('public');
|
|
79
135
|
const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
|
|
80
136
|
c.file === symbol.file && _CLASS_KINDS.includes(c.type));
|
|
81
|
-
return classDefs.some(cd =>
|
|
137
|
+
return classDefs.some(cd => _heritageReachesExternalBase(index, cd, lang,
|
|
138
|
+
contractSatisfiable &&
|
|
139
|
+
symbol.startLine >= cd.startLine && symbol.startLine <= cd.endLine));
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
/**
|
|
@@ -92,9 +150,12 @@ function _classHasExternalBase(index, symbol) {
|
|
|
92
150
|
* Rust `impl Trait for X`) AND a single project-wide method owner of the
|
|
93
151
|
* name (no in-project supertype defines it → the contract is external —
|
|
94
152
|
* the #210 ownerCount===1 rule).
|
|
95
|
-
* (B) a public-by-shape method whose class
|
|
96
|
-
*
|
|
97
|
-
*
|
|
153
|
+
* (B) a public-by-shape method whose class reaches an unresolved base
|
|
154
|
+
* through its heritage closure (extends chains walked transitively,
|
|
155
|
+
* plus the class's own `implements` clause when the member sits in the
|
|
156
|
+
* class body — fix #270). Private/underscore members are never
|
|
157
|
+
* external-contract surface, so a genuinely-dead one stays claimable
|
|
158
|
+
* (the fix #211 shape predicate).
|
|
98
159
|
* Data-driven, not language-keyed: classes without an `extends` clause (Go
|
|
99
160
|
* embedding, Rust structs / inherent impls) never trip (B), and Rust trait
|
|
100
161
|
* impls trip (A) via traitImpl — so new languages inherit correct behavior.
|
|
@@ -950,7 +1011,7 @@ function deadcode(index, options = {}) {
|
|
|
950
1011
|
// by module path, pytest plugins by registration) — zero
|
|
951
1012
|
// in-project references is not evidence of deadness there.
|
|
952
1013
|
const isExternalContract = classAuditSet.has(symbol.type)
|
|
953
|
-
?
|
|
1014
|
+
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
954
1015
|
: overridesOutOfTreeBase(index, symbol);
|
|
955
1016
|
if (isExternalContract && !options.includeExported) {
|
|
956
1017
|
excludedExternalContract++;
|
package/languages/java.js
CHANGED
|
@@ -556,16 +556,26 @@ function extractImplements(classNode) {
|
|
|
556
556
|
* Extract extends from interface
|
|
557
557
|
*/
|
|
558
558
|
function extractInterfaceExtends(interfaceNode) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
559
|
+
// The grammar exposes interface extends as an `extends_interfaces` child
|
|
560
|
+
// wrapping a type_list, not as an `extends` field — the field lookup
|
|
561
|
+
// returned null and interfaces silently never recorded their supertypes
|
|
562
|
+
// (fix #270: the deadcode heritage walk needs them).
|
|
563
|
+
const interfaces = [];
|
|
564
|
+
for (let i = 0; i < interfaceNode.namedChildCount; i++) {
|
|
565
|
+
const child = interfaceNode.namedChild(i);
|
|
566
|
+
if (child.type !== 'extends_interfaces') continue;
|
|
567
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
568
|
+
const entry = child.namedChild(j);
|
|
569
|
+
if (entry.type === 'type_list') {
|
|
570
|
+
for (let k = 0; k < entry.namedChildCount; k++) {
|
|
571
|
+
interfaces.push(entry.namedChild(k).text);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
interfaces.push(entry.text);
|
|
575
|
+
}
|
|
565
576
|
}
|
|
566
|
-
return interfaces;
|
|
567
577
|
}
|
|
568
|
-
return
|
|
578
|
+
return interfaces;
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|