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 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
- const CACHE_FORMAT_VERSION = 47;
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
- /** Does this CLASS DEF extend at least one base that is not in the project index? */
57
- function _classDefHasExternalBase(index, classDef, lang) {
58
- if (!classDef.extends) return false;
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 the method's enclosing class EXTEND at least one base that is NOT in the
65
- * project index? An out-of-tree base is a framework/library type UCN can't see;
66
- * via inheritance it may dispatch into a public method of the subclass
67
- * polymorphically (Starlette → build_middleware_stack) or by name convention
68
- * (Pydantic → bytes_schema). The class def is matched in the method's own file.
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
- * `implements` is deliberately NOT consulted: implementing an external
71
- * interface/trait makes only the INTERFACE'S OWN methods a contract (those
72
- * carry traitImpl/@Override markers handled by Rule A), not the class's
73
- * unrelated inherent methods. Counting it would wrongly shield, e.g., a Rust
74
- * struct's genuinely-dead inherent method just because the struct also
75
- * `impl Display for`s.
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 => _classDefHasExternalBase(index, cd, lang));
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 EXTENDS an unresolved base.
96
- * Private/underscore members are never external-contract surface, so a
97
- * genuinely-dead one stays claimable (the fix #211 shape predicate).
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
- ? _classDefHasExternalBase(index, symbol, lang)
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
- const extendsNode = interfaceNode.childForFieldName('extends');
560
- if (extendsNode) {
561
- const interfaces = [];
562
- for (let i = 0; i < extendsNode.namedChildCount; i++) {
563
- const iface = extendsNode.namedChild(i);
564
- interfaces.push(iface.text);
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.2",
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",