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/cli/index.js +2 -2
- package/core/analysis.js +24 -5
- package/core/cache.js +114 -60
- package/core/callers.js +71 -26
- package/core/deadcode.js +31 -2
- package/core/graph-build.js +224 -0
- package/core/graph.js +7 -10
- package/core/parallel-build.js +10 -7
- package/core/project.js +106 -222
- package/core/registry.js +5 -0
- package/core/search.js +9 -5
- package/core/tracing.js +15 -6
- package/languages/go.js +8 -8
- package/package.json +2 -2
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
594
|
+
if (targets && targets.size > 0) filesWithImports++;
|
|
598
595
|
}
|
|
599
596
|
|
|
600
597
|
return {
|
package/core/parallel-build.js
CHANGED
|
@@ -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,
|