ucn 4.2.3 → 5.0.2
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 +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/graph-build.js
CHANGED
|
@@ -7,7 +7,59 @@
|
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { resolveImport } = require('./imports');
|
|
10
|
-
const { langTraits } = require('../languages');
|
|
10
|
+
const { langTraits, getParser, safeParse } = require('../languages');
|
|
11
|
+
|
|
12
|
+
function _javaPackageTypes(index) {
|
|
13
|
+
const packages = new Map();
|
|
14
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
15
|
+
if (fileEntry.language !== 'java') continue;
|
|
16
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
17
|
+
if (!symbol.namespace || !['class', 'interface', 'record', 'enum'].includes(symbol.type)) continue;
|
|
18
|
+
let names = packages.get(symbol.namespace);
|
|
19
|
+
if (!names) { names = new Map(); packages.set(symbol.namespace, names); }
|
|
20
|
+
let files = names.get(symbol.name);
|
|
21
|
+
if (!files) { files = new Set(); names.set(symbol.name, files); }
|
|
22
|
+
files.add(filePath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return packages;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Collect same-package Java type references from AST roles that denote types. */
|
|
29
|
+
function _javaSamePackageDependencies(index, filePath, fileEntry, packageTypes) {
|
|
30
|
+
const packageName = (fileEntry.symbols || []).find(symbol => symbol.namespace)?.namespace;
|
|
31
|
+
const candidates = packageTypes.get(packageName);
|
|
32
|
+
if (!packageName || !candidates || candidates.size === 0) return [];
|
|
33
|
+
let tree;
|
|
34
|
+
try {
|
|
35
|
+
tree = safeParse(getParser('java'), index._readFile(filePath));
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const referenced = new Set();
|
|
40
|
+
const walk = node => {
|
|
41
|
+
if (node.type === 'type_identifier' && candidates.has(node.text)) {
|
|
42
|
+
referenced.add(node.text);
|
|
43
|
+
} else if (node.type === 'identifier' && candidates.has(node.text)) {
|
|
44
|
+
const parent = node.parent;
|
|
45
|
+
if (parent && ((parent.type === 'method_invocation' &&
|
|
46
|
+
parent.childForFieldName('object')?.id === node.id) ||
|
|
47
|
+
(parent.type === 'field_access' &&
|
|
48
|
+
parent.childForFieldName('object')?.id === node.id))) {
|
|
49
|
+
referenced.add(node.text);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
|
|
53
|
+
};
|
|
54
|
+
walk(tree.rootNode);
|
|
55
|
+
const out = new Set();
|
|
56
|
+
for (const name of referenced) {
|
|
57
|
+
for (const target of candidates.get(name) || []) {
|
|
58
|
+
if (target !== filePath) out.add(target);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...out];
|
|
62
|
+
}
|
|
11
63
|
|
|
12
64
|
/**
|
|
13
65
|
* Build directory→files index for O(1) same-package lookups.
|
|
@@ -89,6 +141,33 @@ function _resolveJavaPackageImport(index, importModule, javaFileIndex, opts = {}
|
|
|
89
141
|
return opts.all ? [] : null;
|
|
90
142
|
}
|
|
91
143
|
|
|
144
|
+
function _buildCSharpNamespaceIndex(index) {
|
|
145
|
+
const namespaces = new Map();
|
|
146
|
+
const add = (key, file) => {
|
|
147
|
+
if (!key) return;
|
|
148
|
+
if (!namespaces.has(key)) namespaces.set(key, []);
|
|
149
|
+
if (!namespaces.get(key).includes(file)) namespaces.get(key).push(file);
|
|
150
|
+
};
|
|
151
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
152
|
+
if (fileEntry.language !== 'csharp') continue;
|
|
153
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
154
|
+
if (!['class', 'interface', 'struct', 'record', 'enum'].includes(symbol.type)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
add(symbol.namespace, filePath);
|
|
158
|
+
if (symbol.namespace) add(`${symbol.namespace}.${symbol.name}`, filePath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return namespaces;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _resolveCSharpUsing(index, importModule, namespaceIndex = null, opts = {}) {
|
|
165
|
+
const map = namespaceIndex || _buildCSharpNamespaceIndex(index);
|
|
166
|
+
const matches = map.get(importModule) || [];
|
|
167
|
+
if (opts.all) return [...matches];
|
|
168
|
+
return matches[0] || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
92
171
|
/**
|
|
93
172
|
* Build import/export relationship graphs
|
|
94
173
|
*/
|
|
@@ -100,6 +179,9 @@ function buildImportGraph(index) {
|
|
|
100
179
|
const dirToGoFiles = new Map();
|
|
101
180
|
// Pre-build filename→files map for Java import resolution (O(1) vs O(n) scan)
|
|
102
181
|
const javaFileIndex = new Map();
|
|
182
|
+
const javaPackageTypes = _javaPackageTypes(index);
|
|
183
|
+
const csharpNamespaceIndex = _buildCSharpNamespaceIndex(index);
|
|
184
|
+
const csharpGlobalImports = new Set();
|
|
103
185
|
for (const [fp, fe] of index.files) {
|
|
104
186
|
if (langTraits(fe.language)?.packageScope === 'directory') {
|
|
105
187
|
const dir = path.dirname(fp);
|
|
@@ -109,6 +191,10 @@ function buildImportGraph(index) {
|
|
|
109
191
|
const name = path.basename(fp, '.java');
|
|
110
192
|
if (!javaFileIndex.has(name)) javaFileIndex.set(name, []);
|
|
111
193
|
javaFileIndex.get(name).push(fp);
|
|
194
|
+
} else if (fe.language === 'csharp') {
|
|
195
|
+
for (const moduleName of (fe.globalImports || [])) {
|
|
196
|
+
if (moduleName) csharpGlobalImports.add(moduleName);
|
|
197
|
+
}
|
|
112
198
|
}
|
|
113
199
|
}
|
|
114
200
|
|
|
@@ -123,7 +209,10 @@ function buildImportGraph(index) {
|
|
|
123
209
|
// names is not evidence about THIS name's module).
|
|
124
210
|
const moduleResolved = {};
|
|
125
211
|
|
|
126
|
-
|
|
212
|
+
const effectiveImports = fileEntry.language === 'csharp'
|
|
213
|
+
? [...(fileEntry.imports || []), ...csharpGlobalImports]
|
|
214
|
+
: (fileEntry.imports || []);
|
|
215
|
+
for (const importModule of effectiveImports) {
|
|
127
216
|
// Skip null modules (e.g., dynamic include! macros in Rust)
|
|
128
217
|
if (!importModule) continue;
|
|
129
218
|
|
|
@@ -134,6 +223,7 @@ function buildImportGraph(index) {
|
|
|
134
223
|
|
|
135
224
|
let resolved = resolveImport(importModule, filePath, {
|
|
136
225
|
aliases: index.config.aliases,
|
|
226
|
+
includePaths: index.config.includePaths,
|
|
137
227
|
language: fileEntry.language,
|
|
138
228
|
root: index.root
|
|
139
229
|
});
|
|
@@ -156,18 +246,31 @@ function buildImportGraph(index) {
|
|
|
156
246
|
}
|
|
157
247
|
}
|
|
158
248
|
|
|
249
|
+
let csharpFiles = null;
|
|
250
|
+
if (!resolved && fileEntry.language === 'csharp') {
|
|
251
|
+
const all = _resolveCSharpUsing(
|
|
252
|
+
index, importModule, csharpNamespaceIndex, { all: true });
|
|
253
|
+
if (all.length > 0) {
|
|
254
|
+
resolved = all[0];
|
|
255
|
+
csharpFiles = all;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
159
259
|
if (resolved && index.files.has(resolved)) {
|
|
160
260
|
moduleResolved[importModule] = path.relative(index.root, resolved);
|
|
161
261
|
// For Go, a package import means all files in that directory are dependencies
|
|
162
262
|
// (Go packages span multiple files in the same directory)
|
|
163
|
-
const filesToLink = javaWildcardFiles
|
|
263
|
+
const filesToLink = javaWildcardFiles
|
|
264
|
+
? [...javaWildcardFiles]
|
|
265
|
+
: csharpFiles ? [...csharpFiles] : [resolved];
|
|
164
266
|
if (langTraits(fileEntry.language)?.packageScope === 'directory') {
|
|
165
267
|
const pkgDir = path.dirname(resolved);
|
|
166
268
|
const dirFiles = dirToGoFiles.get(pkgDir) || [];
|
|
167
|
-
const importerIsTest = filePath.endsWith('_test.go');
|
|
168
269
|
for (const fp of dirFiles) {
|
|
169
|
-
if (fp !== resolved) {
|
|
170
|
-
|
|
270
|
+
if (fp !== resolved && fp !== filePath) {
|
|
271
|
+
// Test files are compilation inputs, never part of
|
|
272
|
+
// an importable Go package surface.
|
|
273
|
+
if (fp.endsWith('_test.go')) continue;
|
|
171
274
|
filesToLink.push(fp);
|
|
172
275
|
}
|
|
173
276
|
}
|
|
@@ -183,6 +286,19 @@ function buildImportGraph(index) {
|
|
|
183
286
|
}
|
|
184
287
|
}
|
|
185
288
|
|
|
289
|
+
// Java same-package visibility needs no import declaration. Add only
|
|
290
|
+
// AST-proven type/static-qualifier references, not a package clique.
|
|
291
|
+
if (fileEntry.language === 'java') {
|
|
292
|
+
for (const linkedFile of _javaSamePackageDependencies(
|
|
293
|
+
index, filePath, fileEntry, javaPackageTypes)) {
|
|
294
|
+
importedFiles.add(linkedFile);
|
|
295
|
+
if (!index.exportGraph.has(linkedFile)) {
|
|
296
|
+
index.exportGraph.set(linkedFile, new Set());
|
|
297
|
+
}
|
|
298
|
+
index.exportGraph.get(linkedFile).add(filePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
186
302
|
// From-import submodules (fix #224): `from . import jobs` binds
|
|
187
303
|
// jobs.py as a plain NAME — the parser can't know (a from-import name
|
|
188
304
|
// may be a symbol), the resolver can. Resolve the composed dotted
|
|
@@ -260,6 +376,38 @@ function buildInheritanceGraph(index) {
|
|
|
260
376
|
const alias = fileEntry.importAliases.find(a => a.local === parent);
|
|
261
377
|
if (alias && classNames.has(alias.original)) return alias.original;
|
|
262
378
|
}
|
|
379
|
+
// Nominal languages commonly spell a project base with a
|
|
380
|
+
// namespace/package qualifier (`detail::buffer<T>`,
|
|
381
|
+
// `demo.Base`). The symbol table is keyed by the terminal
|
|
382
|
+
// type name, so retaining the qualifier disconnects the
|
|
383
|
+
// inheritance graph and can turn an inherited method call
|
|
384
|
+
// into a false receiver mismatch. Strip it only when an
|
|
385
|
+
// indexed type has the matching compiler owner; suffix
|
|
386
|
+
// matching accommodates parsers which retain either the
|
|
387
|
+
// full namespace or only its innermost component.
|
|
388
|
+
if (langTraits(fileEntry.language)?.typeSystem === 'nominal' &&
|
|
389
|
+
(parent.includes('::') || parent.includes('.'))) {
|
|
390
|
+
const separator = parent.includes('::') ? '::' : '.';
|
|
391
|
+
const pieces = parent.split(separator).filter(Boolean);
|
|
392
|
+
const terminal = pieces.pop();
|
|
393
|
+
const qualifier = pieces.join(separator);
|
|
394
|
+
const typeKinds = new Set([
|
|
395
|
+
'class', 'interface', 'struct', 'trait', 'record',
|
|
396
|
+
]);
|
|
397
|
+
const matches = (index.symbols.get(terminal) || [])
|
|
398
|
+
.filter(definition => {
|
|
399
|
+
if (!typeKinds.has(definition.type)) return false;
|
|
400
|
+
const owner = String(definition.namespace || '');
|
|
401
|
+
return owner === qualifier ||
|
|
402
|
+
owner.endsWith(`${separator}${qualifier}`) ||
|
|
403
|
+
qualifier.endsWith(`${separator}${owner}`);
|
|
404
|
+
});
|
|
405
|
+
if (matches.length > 0 &&
|
|
406
|
+
new Set(matches.map(definition =>
|
|
407
|
+
definition.namespace || '')).size === 1) {
|
|
408
|
+
return terminal;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
263
411
|
// Qualified structural parent: `class CustomCommand(
|
|
264
412
|
// click.Command)`. The symbol table owns bare class names,
|
|
265
413
|
// so keeping `click.Command` makes the subclass invisible
|
|
@@ -347,4 +495,11 @@ function splitParentList(clause) {
|
|
|
347
495
|
.filter(Boolean);
|
|
348
496
|
}
|
|
349
497
|
|
|
350
|
-
module.exports = {
|
|
498
|
+
module.exports = {
|
|
499
|
+
buildDirIndex,
|
|
500
|
+
buildImportGraph,
|
|
501
|
+
buildInheritanceGraph,
|
|
502
|
+
splitParentList,
|
|
503
|
+
_resolveJavaPackageImport,
|
|
504
|
+
_resolveCSharpUsing,
|
|
505
|
+
};
|
package/core/graph.js
CHANGED
|
@@ -79,12 +79,16 @@ function imports(index, filePath) {
|
|
|
79
79
|
if (!resolvedPath && fileEntry.language === 'java' && !imp.module.startsWith('.')) {
|
|
80
80
|
resolvedPath = index._resolveJavaPackageImport(imp.module);
|
|
81
81
|
}
|
|
82
|
+
if (!resolvedPath && fileEntry.language === 'csharp') {
|
|
83
|
+
resolvedPath = index._resolveCSharpUsing(imp.module);
|
|
84
|
+
}
|
|
82
85
|
|
|
83
86
|
return {
|
|
84
87
|
module: imp.module,
|
|
85
88
|
names: imp.names,
|
|
86
89
|
type: imp.type,
|
|
87
90
|
resolved: resolvedPath ? path.relative(index.root, resolvedPath) : null,
|
|
91
|
+
indexed: !!(resolvedPath && index.files.has(resolvedPath)),
|
|
88
92
|
isExternal: !resolvedPath,
|
|
89
93
|
// A string-literal dynamic import (import('./x'), importlib.import_module("x"))
|
|
90
94
|
// is still mechanically dynamic even when the path resolves —
|
|
@@ -107,6 +111,35 @@ function imports(index, filePath) {
|
|
|
107
111
|
*/
|
|
108
112
|
function symbolIsExported(symbol, fileEntry, exportedNames) {
|
|
109
113
|
const modifiers = symbol.modifiers || [];
|
|
114
|
+
// Python's language-level public surface is convention based when the
|
|
115
|
+
// module does not declare __all__: top-level non-underscore names are
|
|
116
|
+
// public. Keep explicit __all__ authoritative when present.
|
|
117
|
+
if (fileEntry.language === 'python' &&
|
|
118
|
+
!(fileEntry.moduleAssignedNames || []).includes('__all__') &&
|
|
119
|
+
!symbol.className && !symbol.isMethod) {
|
|
120
|
+
return !!symbol.name && !symbol.name.startsWith('_');
|
|
121
|
+
}
|
|
122
|
+
if ((fileEntry.language === 'csharp' || fileEntry.language === 'java') &&
|
|
123
|
+
(symbol.className || symbol.enclosingType)) {
|
|
124
|
+
const typeKinds = new Set([
|
|
125
|
+
'class', 'struct', 'interface', 'record', 'enum', 'type',
|
|
126
|
+
]);
|
|
127
|
+
const typeIsPublic = (name, namespace, seen = new Set()) => {
|
|
128
|
+
const identity = `${namespace || ''}\0${name}`;
|
|
129
|
+
if (seen.has(identity)) return false;
|
|
130
|
+
seen.add(identity);
|
|
131
|
+
return (fileEntry.symbols || []).some(candidate => {
|
|
132
|
+
if (!typeKinds.has(candidate.type) || candidate.name !== name ||
|
|
133
|
+
(candidate.namespace || null) !== (namespace || null) ||
|
|
134
|
+
!(candidate.modifiers || []).includes('public')) return false;
|
|
135
|
+
return !candidate.enclosingType || typeIsPublic(
|
|
136
|
+
candidate.enclosingType, candidate.namespace,
|
|
137
|
+
new Set(seen));
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
if (!typeIsPublic(symbol.className || symbol.enclosingType,
|
|
141
|
+
symbol.namespace)) return false;
|
|
142
|
+
}
|
|
110
143
|
if (modifiers.includes('export') || modifiers.includes('public')) return true;
|
|
111
144
|
if (modifiers.some(m => typeof m === 'string' && /^pub\b/.test(m))) return true;
|
|
112
145
|
if (langTraits(fileEntry.language)?.exportVisibility === 'capitalization' &&
|
|
@@ -516,6 +549,8 @@ function fileExports(index, filePath, _visited) {
|
|
|
516
549
|
*/
|
|
517
550
|
function api(index, filePath, options = {}) {
|
|
518
551
|
const results = [];
|
|
552
|
+
let scopedFiles = 0;
|
|
553
|
+
let pythonImplicitFiles = 0;
|
|
519
554
|
|
|
520
555
|
let fileIterator;
|
|
521
556
|
if (filePath) {
|
|
@@ -540,95 +575,33 @@ function api(index, filePath, options = {}) {
|
|
|
540
575
|
fileIterator = index.files.entries();
|
|
541
576
|
}
|
|
542
577
|
|
|
543
|
-
for (const [, fileEntry] of fileIterator) {
|
|
578
|
+
for (const [absPath, fileEntry] of fileIterator) {
|
|
544
579
|
if (!fileEntry) continue;
|
|
580
|
+
if (options.in &&
|
|
581
|
+
!index.matchesFilters(fileEntry.relativePath, { in: options.in })) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
545
584
|
|
|
546
585
|
// Skip test files by default (test classes aren't part of public API)
|
|
547
586
|
if (!options.includeTests && isTestFile(fileEntry.relativePath, fileEntry.language)) {
|
|
548
587
|
continue;
|
|
549
588
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
for (const symbol of fileEntry.symbols) {
|
|
554
|
-
if (symbol.type === 'impl') continue;
|
|
555
|
-
if (symbolIsExported(symbol, fileEntry, exportedNames)) {
|
|
556
|
-
results.push({
|
|
557
|
-
name: symbol.name,
|
|
558
|
-
type: symbol.type,
|
|
559
|
-
file: fileEntry.relativePath,
|
|
560
|
-
startLine: symbol.startLine,
|
|
561
|
-
endLine: symbol.endLine,
|
|
562
|
-
...(symbol.className && { className: symbol.className }),
|
|
563
|
-
params: symbol.params,
|
|
564
|
-
returnType: symbol.returnType,
|
|
565
|
-
signature: formatExportSignature(index, symbol)
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Add variable exports (export const/let/var) not matched to symbols
|
|
571
|
-
if (fileEntry.exportDetails) {
|
|
572
|
-
const matchedNames = new Set(results.filter(r => r.file === fileEntry.relativePath).map(r => r.name));
|
|
573
|
-
for (const exp of fileEntry.exportDetails) {
|
|
574
|
-
if (exp.isVariable && !matchedNames.has(exp.name)) {
|
|
575
|
-
const sig = `${exp.declKind} ${exp.name}${exp.typeAnnotation ? ': ' + exp.typeAnnotation : ''}`;
|
|
576
|
-
results.push({
|
|
577
|
-
name: exp.name,
|
|
578
|
-
type: 'variable',
|
|
579
|
-
file: fileEntry.relativePath,
|
|
580
|
-
startLine: exp.line,
|
|
581
|
-
endLine: exp.line,
|
|
582
|
-
params: undefined,
|
|
583
|
-
returnType: exp.typeAnnotation || null,
|
|
584
|
-
signature: sig
|
|
585
|
-
});
|
|
586
|
-
matchedNames.add(exp.name);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// The fix #245 fileExports discipline, api side (fix #251 — the
|
|
590
|
-
// two commands diverged on the same file): consumers import the
|
|
591
|
-
// ALIAS, and clause-exported names with no indexed symbol
|
|
592
|
-
// (class/function expressions) are still API surface.
|
|
593
|
-
for (const exp of fileEntry.exportDetails) {
|
|
594
|
-
if (!exp || !exp.name || exp.module) continue;
|
|
595
|
-
if (exp.alias && exp.alias !== exp.name) {
|
|
596
|
-
const entry = results.find(r =>
|
|
597
|
-
r.file === fileEntry.relativePath && r.name === exp.name && !r.sourceName);
|
|
598
|
-
if (entry) {
|
|
599
|
-
entry.sourceName = exp.name;
|
|
600
|
-
entry.name = exp.alias;
|
|
601
|
-
if (entry.signature) {
|
|
602
|
-
entry.signature = entry.signature.replace(exp.name, exp.alias);
|
|
603
|
-
}
|
|
604
|
-
matchedNames.add(exp.alias);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
const shown = exp.alias || exp.name;
|
|
609
|
-
if (!matchedNames.has(shown) && !matchedNames.has(exp.name) &&
|
|
610
|
-
exportedNames.has(exp.name)) {
|
|
611
|
-
results.push({
|
|
612
|
-
name: shown,
|
|
613
|
-
...(exp.alias && exp.alias !== exp.name && { sourceName: exp.name }),
|
|
614
|
-
type: 'export',
|
|
615
|
-
file: fileEntry.relativePath,
|
|
616
|
-
startLine: exp.line || 1,
|
|
617
|
-
endLine: exp.line || 1,
|
|
618
|
-
params: undefined,
|
|
619
|
-
returnType: null,
|
|
620
|
-
signature: shown,
|
|
621
|
-
});
|
|
622
|
-
matchedNames.add(shown);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
589
|
+
scopedFiles++;
|
|
590
|
+
if (fileEntry.language === 'python' && fileEntry.exports.length === 0) {
|
|
591
|
+
pythonImplicitFiles++;
|
|
625
592
|
}
|
|
593
|
+
const exports = fileExports(index, absPath);
|
|
594
|
+
if (Array.isArray(exports)) results.push(...exports);
|
|
626
595
|
}
|
|
627
596
|
|
|
628
597
|
// Rule 11: (file, line) ordering regardless of parse order — file mode
|
|
629
598
|
// used to emit symbols in extraction order (fix #251).
|
|
630
599
|
results.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
631
600
|
(a.startLine - b.startLine) || codeUnitCompare(a.name, b.name));
|
|
601
|
+
Object.defineProperty(results, 'apiInfo', {
|
|
602
|
+
value: { scopedFiles, pythonImplicitFiles },
|
|
603
|
+
enumerable: false, writable: true, configurable: true,
|
|
604
|
+
});
|
|
632
605
|
return results;
|
|
633
606
|
}
|
|
634
607
|
|
|
@@ -852,4 +825,7 @@ function circularDeps(index, options = {}) {
|
|
|
852
825
|
}
|
|
853
826
|
}
|
|
854
827
|
|
|
855
|
-
module.exports = {
|
|
828
|
+
module.exports = {
|
|
829
|
+
imports, exporters, fileExports, api, graph, circularDeps,
|
|
830
|
+
symbolIsExported,
|
|
831
|
+
};
|
package/core/imports.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { getParser,
|
|
10
|
+
const { getParser, getLanguageAdapter } = require('../languages');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Extract imports from file content using AST
|
|
@@ -20,7 +20,7 @@ function extractImports(content, language) {
|
|
|
20
20
|
// Use JS language module for TS/TSX (same import syntax), but the actual language's parser
|
|
21
21
|
const moduleLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
22
22
|
|
|
23
|
-
const langModule =
|
|
23
|
+
const langModule = getLanguageAdapter(moduleLang);
|
|
24
24
|
if (langModule && typeof langModule.findImportsInCode === 'function') {
|
|
25
25
|
try {
|
|
26
26
|
const parser = getParser(language);
|
|
@@ -45,7 +45,7 @@ function extractExports(content, language) {
|
|
|
45
45
|
// Use JS language module for TS/TSX (same export syntax), but the actual language's parser
|
|
46
46
|
const moduleLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
47
47
|
|
|
48
|
-
const langModule =
|
|
48
|
+
const langModule = getLanguageAdapter(moduleLang);
|
|
49
49
|
if (langModule && typeof langModule.findExportsInCode === 'function') {
|
|
50
50
|
try {
|
|
51
51
|
const parser = getParser(language);
|
|
@@ -180,8 +180,38 @@ function resolveImport(importPath, fromFile, config = {}) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// Relative imports
|
|
183
|
+
const extensions = config.extensions || getExtensions(config.language);
|
|
183
184
|
const resolved = path.resolve(fromDir, normalizedPath);
|
|
184
|
-
|
|
185
|
+
const direct = resolveFilePath(resolved, extensions);
|
|
186
|
+
if (direct) return direct;
|
|
187
|
+
|
|
188
|
+
// C/C++ quoted includes may be rooted at compiler -I/-iquote paths rather
|
|
189
|
+
// than the importing file. compile_commands.json is the authoritative
|
|
190
|
+
// build metadata when present; unresolved system includes remain external.
|
|
191
|
+
if (config.language === 'c' || config.language === 'cpp') {
|
|
192
|
+
const { includeDirectoriesForFile } = require('./compilation-database');
|
|
193
|
+
const includeName = normalizedPath.replace(/^\.\//, '');
|
|
194
|
+
const includeDirs = includeDirectoriesForFile(fromFile, config.root);
|
|
195
|
+
for (const configured of config.includePaths || []) {
|
|
196
|
+
if (typeof configured !== 'string' || !configured.trim()) continue;
|
|
197
|
+
includeDirs.push(path.isAbsolute(configured)
|
|
198
|
+
? configured
|
|
199
|
+
: path.resolve(config.root || fromDir, configured));
|
|
200
|
+
}
|
|
201
|
+
// Header-only/source-distribution projects commonly omit a generated
|
|
202
|
+
// compile_commands.json but still use the conventional public
|
|
203
|
+
// `include/` root (`#include "fmt/format.h"`). These are project-owned
|
|
204
|
+
// files, not external packages. Try explicit compiler metadata first,
|
|
205
|
+
// then deterministic project roots; never search arbitrary parents.
|
|
206
|
+
if (config.root) {
|
|
207
|
+
includeDirs.push(config.root, path.join(config.root, 'include'));
|
|
208
|
+
}
|
|
209
|
+
for (const includeDir of [...new Set(includeDirs)]) {
|
|
210
|
+
const candidate = resolveFilePath(path.resolve(includeDir, includeName), extensions);
|
|
211
|
+
if (candidate) return candidate;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
185
215
|
}
|
|
186
216
|
|
|
187
217
|
// Cache for Go module paths
|
|
@@ -484,8 +514,17 @@ function resolveRustImport(importPath, fromFile, projectRoot) {
|
|
|
484
514
|
const cargo = findCargoRoot(fromDir);
|
|
485
515
|
if (cargo && cargo.packageName && firstSeg === cargo.packageName) {
|
|
486
516
|
const topDir = path.relative(cargo.root, fromFile).split(path.sep)[0];
|
|
517
|
+
const sourceRelative = path.relative(cargo.srcDir, fromFile);
|
|
518
|
+
const sourceHead = sourceRelative.split(path.sep)[0];
|
|
519
|
+
// A package's binaries are separate crates from its library even
|
|
520
|
+
// though both live below src/. `use package_name::Type` in
|
|
521
|
+
// src/main.rs or src/bin/** therefore names the lib target, not a
|
|
522
|
+
// child module of the binary. Treat these like examples/tests so
|
|
523
|
+
// the import graph can pin re-exported library types exactly.
|
|
524
|
+
const separateBinary = sourceRelative === 'main.rs' ||
|
|
525
|
+
sourceHead === 'bin';
|
|
487
526
|
const externalTarget = topDir === 'tests' || topDir === 'benches' || topDir === 'examples' ||
|
|
488
|
-
!fromFile.startsWith(cargo.srcDir + path.sep);
|
|
527
|
+
separateBinary || !fromFile.startsWith(cargo.srcDir + path.sep);
|
|
489
528
|
if (externalTarget) {
|
|
490
529
|
const restSegs = importPath.split('::').slice(1);
|
|
491
530
|
const resolved = restSegs.length > 0 ? resolveRustModulePath(cargo.srcDir, restSegs) : null;
|
|
@@ -593,7 +632,13 @@ function _findPackageJson(fromDir, stopDir) {
|
|
|
593
632
|
try {
|
|
594
633
|
if (fs.existsSync(candidate)) {
|
|
595
634
|
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
596
|
-
info = {
|
|
635
|
+
info = {
|
|
636
|
+
dir: current,
|
|
637
|
+
name: pkg.name,
|
|
638
|
+
exports: pkg.exports,
|
|
639
|
+
main: pkg.main,
|
|
640
|
+
source: pkg.source,
|
|
641
|
+
};
|
|
597
642
|
}
|
|
598
643
|
} catch { /* unreadable or invalid JSON */ }
|
|
599
644
|
_pkgCache.set(current, info);
|
|
@@ -666,6 +711,14 @@ function resolveSelfReference(importPath, fromDir, config) {
|
|
|
666
711
|
if (hit) return hit;
|
|
667
712
|
}
|
|
668
713
|
}
|
|
714
|
+
// Monorepo packages commonly export build artifacts that do not
|
|
715
|
+
// exist in a source checkout while declaring the development entry
|
|
716
|
+
// explicitly (`"source": "src/index.ts"`). For a package's own bare
|
|
717
|
+
// import, that source field is the authoritative local module.
|
|
718
|
+
if (subpath === '.' && typeof pkg.source === 'string') {
|
|
719
|
+
const source = resolveFilePath(path.resolve(pkg.dir, pkg.source), extensions);
|
|
720
|
+
if (source) return source;
|
|
721
|
+
}
|
|
669
722
|
return null;
|
|
670
723
|
}
|
|
671
724
|
// No exports map: bare name -> main/index; subpath -> direct file
|
|
@@ -730,6 +783,12 @@ function getExtensions(language) {
|
|
|
730
783
|
return ['.java'];
|
|
731
784
|
case 'rust':
|
|
732
785
|
return ['.rs'];
|
|
786
|
+
case 'c':
|
|
787
|
+
return ['.c', '.h'];
|
|
788
|
+
case 'cpp':
|
|
789
|
+
return ['.cc', '.cpp', '.cxx', '.c++', '.hpp', '.hh', '.hxx', '.h++', '.h'];
|
|
790
|
+
case 'csharp':
|
|
791
|
+
return ['.cs', '.csx'];
|
|
733
792
|
default:
|
|
734
793
|
return ['.js', '.ts'];
|
|
735
794
|
}
|