ucn 3.8.21 → 3.8.23

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
@@ -212,6 +212,28 @@ function deadcode(index, options = {}) {
212
212
  }
213
213
  }
214
214
 
215
+ // Pre-filter exported symbols from the scan set when not auditing exports.
216
+ // Go exports ~63K capitalized names on K8s — scanning these in Phase 2 only to
217
+ // skip them in Phase 3 wastes O(63K × 11K files) = ~700M comparisons.
218
+ if (!options.includeExported) {
219
+ const narrowed = new Set();
220
+ for (const name of potentiallyDeadNames) {
221
+ const syms = index.symbols.get(name) || [];
222
+ // Keep the name only if at least one definition is NOT exported
223
+ const allExported = syms.every(s => {
224
+ const fe = index.files.get(s.file);
225
+ const lang = fe?.language;
226
+ if (!fe) return false;
227
+ return fe.exports.includes(name) ||
228
+ (s.modifiers || []).includes('export') ||
229
+ (s.modifiers || []).includes('public') ||
230
+ (langTraits(lang)?.exportVisibility === 'capitalization' && /^[A-Z]/.test(name));
231
+ });
232
+ if (!allExported) narrowed.add(name);
233
+ }
234
+ potentiallyDeadNames = narrowed;
235
+ }
236
+
215
237
  // When --file is provided, pre-filter to only names of symbols in the target scope.
216
238
  // The text scan below is O(potentiallyDeadNames × files) — narrowing the name set
217
239
  // avoids scanning all files for names that will be filtered out at the result stage.
@@ -236,9 +258,16 @@ function deadcode(index, options = {}) {
236
258
  for (const [filePath, fileEntry] of index.files) {
237
259
  try {
238
260
  const content = index._readFile(filePath);
239
- const lines = content.split('\n');
261
+ // Fast pre-filter: extract identifiers from file, intersect with target names.
262
+ // One regex pass over content (O(content)) vs O(names × content) substring searches.
263
+ const fileIdentifiers = new Set(content.match(/\b[a-zA-Z_]\w*\b/g));
264
+ const namesInFile = [];
240
265
  for (const name of potentiallyDeadNames) {
241
- if (!content.includes(name)) continue;
266
+ if (fileIdentifiers.has(name)) namesInFile.push(name);
267
+ }
268
+ if (namesInFile.length === 0) continue;
269
+ const lines = content.split('\n');
270
+ for (const name of namesInFile) {
242
271
  const nameLen = name.length;
243
272
  for (let i = 0; i < lines.length; i++) {
244
273
  const line = lines[i];
@@ -0,0 +1,224 @@
1
+ /**
2
+ * core/graph-build.js - Import/export and inheritance graph construction
3
+ *
4
+ * Extracted from project.js. All functions take an `index` (ProjectIndex)
5
+ * as the first argument instead of using `this`.
6
+ */
7
+
8
+ const path = require('path');
9
+ const { resolveImport } = require('./imports');
10
+ const { langTraits } = require('../languages');
11
+
12
+ /**
13
+ * Build directory→files index for O(1) same-package lookups.
14
+ * Replaces O(N) full-index scans in findCallers and countSymbolUsages.
15
+ */
16
+ function buildDirIndex(index) {
17
+ index.dirToFiles = new Map();
18
+ for (const filePath of index.files.keys()) {
19
+ const dir = path.dirname(filePath);
20
+ let list = index.dirToFiles.get(dir);
21
+ if (!list) {
22
+ list = [];
23
+ index.dirToFiles.set(dir, list);
24
+ }
25
+ list.push(filePath);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Resolve a Java package import to a project file.
31
+ * Handles regular imports, static imports (strips member name), and wildcards (strips .*).
32
+ * Progressively strips trailing segments to find the class file.
33
+ */
34
+ function _resolveJavaPackageImport(index, importModule, javaFileIndex) {
35
+ const isWildcard = importModule.endsWith('.*');
36
+ // Strip wildcard suffix (e.g., "com.pkg.Class.*" -> "com.pkg.Class")
37
+ const mod = isWildcard ? importModule.slice(0, -2) : importModule;
38
+ const segments = mod.split('.');
39
+
40
+ // Try progressively shorter paths: full path, then strip last segment, etc.
41
+ // This handles static imports where path includes member name after class
42
+ if (javaFileIndex) {
43
+ // Fast path: use pre-built filename→files index (O(candidates) vs O(all files))
44
+ for (let i = segments.length; i > 0; i--) {
45
+ const className = segments[i - 1];
46
+ const candidates = javaFileIndex.get(className);
47
+ if (candidates) {
48
+ const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
49
+ for (const absPath of candidates) {
50
+ if (absPath.endsWith(fileSuffix)) {
51
+ return absPath;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ } else {
57
+ // Fallback: scan all files (used by imports() method outside buildImportGraph)
58
+ for (let i = segments.length; i > 0; i--) {
59
+ const fileSuffix = '/' + segments.slice(0, i).join('/') + '.java';
60
+ for (const absPath of index.files.keys()) {
61
+ if (absPath.endsWith(fileSuffix)) {
62
+ return absPath;
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // For wildcard imports (com.pkg.model.*), the package may be a directory
69
+ // containing .java files. Check if any file lives under this package path.
70
+ if (isWildcard) {
71
+ const dirSuffix = '/' + segments.join('/') + '/';
72
+ for (const absPath of index.files.keys()) {
73
+ if (absPath.includes(dirSuffix)) {
74
+ return absPath;
75
+ }
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Build import/export relationship graphs
84
+ */
85
+ function buildImportGraph(index) {
86
+ index.importGraph.clear();
87
+ index.exportGraph.clear();
88
+
89
+ // Pre-build directory→files map for Go package linking (O(1) lookup vs O(n) scan)
90
+ const dirToGoFiles = new Map();
91
+ // Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
92
+ const javaFileIndex = new Map();
93
+ for (const [fp, fe] of index.files) {
94
+ if (langTraits(fe.language)?.packageScope === 'directory') {
95
+ const dir = path.dirname(fp);
96
+ if (!dirToGoFiles.has(dir)) dirToGoFiles.set(dir, []);
97
+ dirToGoFiles.get(dir).push(fp);
98
+ } else if (fe.language === 'java') {
99
+ const name = path.basename(fp, '.java');
100
+ if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
101
+ javaFileIndex.get(name).push(fp);
102
+ }
103
+ }
104
+
105
+ for (const [filePath, fileEntry] of index.files) {
106
+ const importedFiles = new Set();
107
+ const seenModules = new Set();
108
+
109
+ for (const importModule of fileEntry.imports) {
110
+ // Skip null modules (e.g., dynamic include! macros in Rust)
111
+ if (!importModule) continue;
112
+
113
+ // Deduplicate: same module imported multiple times in one file
114
+ // (e.g., lazy imports inside different functions)
115
+ if (seenModules.has(importModule)) continue;
116
+ seenModules.add(importModule);
117
+
118
+ let resolved = resolveImport(importModule, filePath, {
119
+ aliases: index.config.aliases,
120
+ language: fileEntry.language,
121
+ root: index.root
122
+ });
123
+
124
+ // Java package imports: resolve by progressive suffix matching
125
+ // Handles regular, static (com.pkg.Class.method), and wildcard (com.pkg.Class.*) imports
126
+ if (!resolved && fileEntry.language === 'java' && !importModule.startsWith('.')) {
127
+ resolved = _resolveJavaPackageImport(index, importModule, javaFileIndex);
128
+ }
129
+
130
+ if (resolved && index.files.has(resolved)) {
131
+ // For Go, a package import means all files in that directory are dependencies
132
+ // (Go packages span multiple files in the same directory)
133
+ const filesToLink = [resolved];
134
+ if (langTraits(fileEntry.language)?.packageScope === 'directory') {
135
+ const pkgDir = path.dirname(resolved);
136
+ const dirFiles = dirToGoFiles.get(pkgDir) || [];
137
+ const importerIsTest = filePath.endsWith('_test.go');
138
+ for (const fp of dirFiles) {
139
+ if (fp !== resolved) {
140
+ if (!importerIsTest && fp.endsWith('_test.go')) continue;
141
+ filesToLink.push(fp);
142
+ }
143
+ }
144
+ }
145
+
146
+ for (const linkedFile of filesToLink) {
147
+ importedFiles.add(linkedFile);
148
+ if (!index.exportGraph.has(linkedFile)) {
149
+ index.exportGraph.set(linkedFile, new Set());
150
+ }
151
+ index.exportGraph.get(linkedFile).add(filePath);
152
+ }
153
+ }
154
+ }
155
+
156
+ index.importGraph.set(filePath, importedFiles);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Build inheritance relationship graphs
162
+ */
163
+ function buildInheritanceGraph(index) {
164
+ index.extendsGraph.clear();
165
+ index.extendedByGraph.clear();
166
+
167
+ // Collect all class/interface/struct names for alias resolution
168
+ const classNames = new Set();
169
+ for (const [, fileEntry] of index.files) {
170
+ for (const symbol of fileEntry.symbols) {
171
+ if (['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
172
+ classNames.add(symbol.name);
173
+ }
174
+ }
175
+ }
176
+
177
+ for (const [filePath, fileEntry] of index.files) {
178
+ for (const symbol of fileEntry.symbols) {
179
+ if (!['class', 'interface', 'struct', 'trait', 'record'].includes(symbol.type)) {
180
+ continue;
181
+ }
182
+
183
+ if (symbol.extends) {
184
+ // Parse comma-separated parents (Python MRO: "Flyable, Swimmable")
185
+ const parents = symbol.extends.split(',').map(s => s.trim()).filter(Boolean);
186
+
187
+ // Resolve aliased parent names via import aliases
188
+ // e.g., const { BaseHandler: Handler } = require('./base')
189
+ // class Child extends Handler → resolve Handler to BaseHandler
190
+ const resolvedParents = parents.map(parent => {
191
+ if (classNames.has(parent)) return parent;
192
+ if (fileEntry.importAliases) {
193
+ const alias = fileEntry.importAliases.find(a => a.local === parent);
194
+ if (alias && classNames.has(alias.original)) return alias.original;
195
+ }
196
+ return parent;
197
+ });
198
+
199
+ // Store with file scope to avoid collisions when same class name
200
+ // appears in multiple files (F-002 fix)
201
+ if (!index.extendsGraph.has(symbol.name)) {
202
+ index.extendsGraph.set(symbol.name, []);
203
+ }
204
+ index.extendsGraph.get(symbol.name).push({
205
+ file: filePath,
206
+ parents: resolvedParents
207
+ });
208
+
209
+ for (const parent of resolvedParents) {
210
+ if (!index.extendedByGraph.has(parent)) {
211
+ index.extendedByGraph.set(parent, []);
212
+ }
213
+ index.extendedByGraph.get(parent).push({
214
+ name: symbol.name,
215
+ type: symbol.type,
216
+ file: filePath
217
+ });
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, _resolveJavaPackageImport };
package/core/graph.js CHANGED
@@ -119,9 +119,9 @@ function exporters(index, filePath) {
119
119
 
120
120
  const targetPath = resolved;
121
121
 
122
- const importers = index.exportGraph.get(targetPath) || [];
122
+ const importers = index.exportGraph.get(targetPath) || new Set();
123
123
 
124
- return importers.map(importerPath => {
124
+ return [...importers].map(importerPath => {
125
125
  const fileEntry = index.files.get(importerPath);
126
126
 
127
127
  // Find the import line
@@ -470,13 +470,10 @@ function graph(index, filePath, options = {}) {
470
470
  if (depth >= maxDepth) return;
471
471
 
472
472
  const neighbors = dir === 'imports'
473
- ? (index.importGraph.get(file) || [])
474
- : (index.exportGraph.get(file) || []);
473
+ ? (index.importGraph.get(file) || new Set())
474
+ : (index.exportGraph.get(file) || new Set());
475
475
 
476
- // Deduplicate neighbors (same file may be imported multiple times, e.g. Java inner classes)
477
- const uniqueNeighbors = [...new Set(neighbors)];
478
-
479
- for (const neighbor of uniqueNeighbors) {
476
+ for (const neighbor of neighbors) {
480
477
  edges.push({ from: file, to: neighbor });
481
478
  traverse(neighbor, depth + 1);
482
479
  }
@@ -543,7 +540,7 @@ function circularDeps(index, options = {}) {
543
540
  color.set(file, GRAY);
544
541
  stack.push(file);
545
542
 
546
- const neighbors = [...new Set(index.importGraph.get(file) || [])];
543
+ const neighbors = index.importGraph.get(file) || new Set();
547
544
 
548
545
  for (const neighbor of neighbors) {
549
546
  if (neighbor === file) continue; // Skip self-imports (not a cycle)
@@ -594,7 +591,7 @@ function circularDeps(index, options = {}) {
594
591
  // Count files that participate in import graph (have edges)
595
592
  let filesWithImports = 0;
596
593
  for (const [, targets] of index.importGraph) {
597
- if (targets && targets.length > 0) filesWithImports++;
594
+ if (targets && targets.size > 0) filesWithImports++;
598
595
  }
599
596
 
600
597
  return {
@@ -40,12 +40,6 @@ function parallelBuild(index, files, options = {}) {
40
40
  console.error(`Parallel build: ${workerCount} workers for ${files.length} files`);
41
41
  }
42
42
 
43
- // Prepare existing hash data for skip-if-unchanged checks in workers
44
- const existingHashes = Object.create(null);
45
- for (const [fp, entry] of index.files) {
46
- existingHashes[fp] = { mtime: entry.mtime, size: entry.size, hash: entry.hash };
47
- }
48
-
49
43
  // Partition files round-robin for balanced work distribution
50
44
  const chunks = Array.from({ length: workerCount }, () => []);
51
45
  for (let i = 0; i < files.length; i++) {
@@ -63,11 +57,20 @@ function parallelBuild(index, files, options = {}) {
63
57
  const { port1, port2 } = new MessageChannel();
64
58
  ports.push(port1);
65
59
 
60
+ // Build per-worker hash subset (each worker only needs hashes for its chunk)
61
+ const workerHashes = Object.create(null);
62
+ for (const fp of chunks[i]) {
63
+ const entry = index.files.get(fp);
64
+ if (entry) {
65
+ workerHashes[fp] = { mtime: entry.mtime, size: entry.size, hash: entry.hash };
66
+ }
67
+ }
68
+
66
69
  const worker = new Worker(path.join(__dirname, 'build-worker.js'), {
67
70
  workerData: {
68
71
  files: chunks[i],
69
72
  rootDir: index.root,
70
- existingHashes,
73
+ existingHashes: workerHashes,
71
74
  signal: sab,
72
75
  workerIndex: i,
73
76
  port: port2,