codebase-onboarder 0.3.1 → 0.4.1
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/README.md +66 -2
- package/cli/commands.js +323 -12
- package/cli/main.js +39 -3
- package/cli/ui.js +18 -0
- package/package.json +1 -1
- package/server/daemon.js +195 -0
- package/server/index.js +59 -35
- package/server/layout.js +73 -0
- package/server/logger.js +329 -15
- package/server/pidfile.js +38 -1
- package/server/startup.js +261 -0
- package/shared/analyzer/graph.js +6 -1
- package/shared/analyzer/languages/csharp.js +81 -6
- package/shared/analyzer/languages/java.js +79 -4
- package/shared/analyzer/languages/rust.js +104 -2
- package/shared/analyzer/scan.js +37 -6
|
@@ -1,19 +1,40 @@
|
|
|
1
1
|
// C# analyzer
|
|
2
|
-
|
|
2
|
+
//
|
|
3
|
+
// C# is namespace-rooted exactly as Java is package-rooted: a file's path is its
|
|
4
|
+
// namespace spelled with slashes, under a project root. The difference that
|
|
5
|
+
// matters is the import. Java's `import a.b.C;` names a *class*, so one path
|
|
6
|
+
// answers it; C#'s `using A.B;` names a *namespace*, and the classes inside it
|
|
7
|
+
// are not knowable from the import string. So a namespace import resolves to a
|
|
8
|
+
// directory and the scanner links to every file in it — the same shape Go uses
|
|
9
|
+
// for packages.
|
|
10
|
+
import { blankComments, lineCounter } from '../util.js';
|
|
3
11
|
|
|
4
12
|
export const extensions = ['.cs'];
|
|
5
13
|
|
|
14
|
+
const NAMESPACE_RE = /^\s*namespace\s+([A-Za-z0-9_.]+)\s*[;{]/m;
|
|
15
|
+
|
|
6
16
|
export function analyze(source, path) {
|
|
7
17
|
const clean = blankComments(source);
|
|
8
18
|
const lineAt = lineCounter(clean);
|
|
9
19
|
|
|
20
|
+
// The file's own namespace. Captured first because every import is resolved
|
|
21
|
+
// relative to it — this declaration is the only place the root is written down.
|
|
22
|
+
// Block-scoped namespaces (`namespace A.B { }`) are the norm, and the regex
|
|
23
|
+
// accepts both the `;` and `{` forms.
|
|
24
|
+
const namespace = (clean.match(NAMESPACE_RE) || [])[1] || '';
|
|
25
|
+
|
|
10
26
|
const imports = [];
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
// `using static A.B.C;` names a type (and optionally a member of it); plain
|
|
28
|
+
// `using A.B;` names a namespace. The distinction decides whether we look for
|
|
29
|
+
// a file or a directory.
|
|
30
|
+
for (const m of clean.matchAll(/\busing\s+(static\s+)?([A-Za-z0-9_.]+)\s*;/g)) {
|
|
31
|
+
imports.push({ spec: m[2], static: !!m[1], namespace, line: lineAt(m.index) });
|
|
13
32
|
}
|
|
33
|
+
// `using X = Y;` aliases and global usings are out of scope; a spec that fails
|
|
34
|
+
// to resolve is reported as unresolved rather than guessed at.
|
|
14
35
|
|
|
15
36
|
const classes = [];
|
|
16
|
-
for (const m of clean.matchAll(/(\[[^\]]+\]\s*)*\b(?:public\s+|private\s+|protected\s+|internal\s+)*(?:abstract\s+|sealed\s+)
|
|
37
|
+
for (const m of clean.matchAll(/(\[[^\]]+\]\s*)*\b(?:public\s+|private\s+|protected\s+|internal\s+)*(?:abstract\s+|sealed\s+|static\s+|partial\s+)*(?:class|interface|struct|record|enum)\s+([a-zA-Z0-9_]+)/g)) {
|
|
17
38
|
const attributes = [];
|
|
18
39
|
if (m[1]) {
|
|
19
40
|
for (const am of m[1].matchAll(/\[([A-Za-z0-9_]+)/g)) {
|
|
@@ -30,10 +51,64 @@ export function analyze(source, path) {
|
|
|
30
51
|
exports: classes.map(c => ({ name: c.name, kind: 'class' })),
|
|
31
52
|
functions: [],
|
|
32
53
|
classes,
|
|
33
|
-
hasMain
|
|
54
|
+
hasMain,
|
|
55
|
+
namespace,
|
|
34
56
|
};
|
|
35
57
|
}
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
// A C# `using` is resolved by *path suffix*, not by a root prefix. See
|
|
60
|
+
// `resolveImport` below for why that distinction matters — it is the whole
|
|
61
|
+
// difference between C# and Java, and getting it backwards drops every import.
|
|
62
|
+
//
|
|
63
|
+
// `sourceRootFor` is kept and exported because it is the answer for the layouts
|
|
64
|
+
// where a project *does* mirror the full namespace (`Acme/Services/User.cs`).
|
|
65
|
+
// It is a fallback, not the main path.
|
|
66
|
+
export function sourceRootFor(fromPath, namespace) {
|
|
67
|
+
if (!namespace) return null;
|
|
68
|
+
const suffix = namespace.replace(/\./g, '/');
|
|
69
|
+
const normalized = String(fromPath).replace(/\\/g, '/');
|
|
70
|
+
const nested = normalized.lastIndexOf('/' + suffix + '/');
|
|
71
|
+
if (nested >= 0) return normalized.slice(0, nested + 1);
|
|
72
|
+
if (normalized === suffix || normalized.startsWith(suffix + '/')) return '';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveImport(spec, fromPath, has, context = {}, meta = {}) {
|
|
77
|
+
const parts = spec.split('.').filter(Boolean);
|
|
78
|
+
const findDir = (segments) => (segments.length ? context.findDirEndingWith?.(segments.join('/')) : null);
|
|
79
|
+
if (!parts.length) return { unresolved: spec };
|
|
80
|
+
|
|
81
|
+
// A C# `using` is ambiguous from the string alone: in `using Acme.Models;`,
|
|
82
|
+
// `Models` might be a namespace (link its directory) or a type (link the
|
|
83
|
+
// file). Both readings are tried, most specific first.
|
|
84
|
+
//
|
|
85
|
+
// The other half of the puzzle is that C# resolves by *path suffix*, not by a
|
|
86
|
+
// root prefix the way Java does. `namespace Acme.Services` conventionally
|
|
87
|
+
// lives in `src/Services/`: the folders mirror the namespace, but the leading
|
|
88
|
+
// `Acme` — the project or company root — is not a directory. So the namespace
|
|
89
|
+
// segments are matched against the *tail* of a real directory path, which is
|
|
90
|
+
// what `findDirEndingWith` answers. Stripping the whole namespace instead, the
|
|
91
|
+
// Java approach, matches nothing here and silently drops every import.
|
|
92
|
+
|
|
93
|
+
// Reading 1: the last segment is a type. Find the directory holding its
|
|
94
|
+
// namespace, then the file named after it.
|
|
95
|
+
const namespaceParts = parts.slice(0, -1);
|
|
96
|
+
for (let take = namespaceParts.length; take > 0; take -= 1) {
|
|
97
|
+
const dir = findDir(namespaceParts.slice(namespaceParts.length - take));
|
|
98
|
+
if (!dir) continue;
|
|
99
|
+
const exact = dir + '/' + parts[parts.length - 1] + '.cs';
|
|
100
|
+
if (has(exact)) return { path: exact };
|
|
101
|
+
if (meta.static) return { unresolved: spec }; // `using static A.B.C;` is always a type
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Reading 2: the whole spec is a namespace, so link its directory.
|
|
105
|
+
for (let take = parts.length; take > 0; take -= 1) {
|
|
106
|
+
const dir = findDir(parts.slice(parts.length - take));
|
|
107
|
+
if (dir) return { packageDir: dir };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The BCL (`System`, `System.Collections.Generic`) and every NuGet package land
|
|
111
|
+
// here. Staying unresolved is the honest answer: a file we invent would be a
|
|
112
|
+
// node in the graph that does not exist.
|
|
38
113
|
return { unresolved: spec };
|
|
39
114
|
}
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
// Java analyzer
|
|
2
|
+
//
|
|
3
|
+
// Java is package-rooted: a source file's path *is* its package spelled with
|
|
4
|
+
// slashes, under a source root (`src/main/java` in Maven, `src/main/java` in
|
|
5
|
+
// Gradle, or a plain directory in a one-off project). That single fact is what
|
|
6
|
+
// turns an import into a file, and it is why Java can have a real import graph
|
|
7
|
+
// at all — the C# analyzer sitting next to this one cannot, because a C# file
|
|
8
|
+
// carries no path↔namespace correspondence to invert.
|
|
2
9
|
import { blankComments, lineCounter, uniqueBy } from '../util.js';
|
|
3
10
|
|
|
4
11
|
export const extensions = ['.java'];
|
|
5
12
|
|
|
13
|
+
const PACKAGE_RE = /^[ \t]*package\s+([a-zA-Z0-9_.]+)\s*;/m;
|
|
14
|
+
|
|
6
15
|
export function analyze(source, path) {
|
|
7
16
|
const clean = blankComments(source);
|
|
8
17
|
const lineAt = lineCounter(clean);
|
|
9
18
|
|
|
19
|
+
// The file's own package, captured first because every import in the file is
|
|
20
|
+
// resolved relative to it: `resolveImport` has to know where the source root
|
|
21
|
+
// ends and the package begins, and this declaration is the only place that is
|
|
22
|
+
// written down. A file with no `package` is in the default package, which is a
|
|
23
|
+
// real (if unfashionable) case rather than an error.
|
|
24
|
+
const packageName = (clean.match(PACKAGE_RE) || [])[1] || '';
|
|
25
|
+
|
|
10
26
|
const imports = [];
|
|
11
27
|
for (const m of clean.matchAll(/\bimport\s+(static\s+)?([a-zA-Z0-9_.]+)/g)) {
|
|
12
|
-
|
|
28
|
+
// `packageName` rides along on the import so the resolver can do its job
|
|
29
|
+
// from the import object alone — which is the fifth argument scan.js passes.
|
|
30
|
+
imports.push({ spec: m[2], static: !!m[1], packageName, line: lineAt(m.index) });
|
|
13
31
|
}
|
|
14
32
|
|
|
15
33
|
const classes = [];
|
|
@@ -26,14 +44,71 @@ export function analyze(source, path) {
|
|
|
26
44
|
const hasMain = /public\s+static\s+void\s+main\s*\(/.test(clean);
|
|
27
45
|
|
|
28
46
|
return {
|
|
29
|
-
imports,
|
|
47
|
+
imports: uniqueBy(imports, (i) => i.spec + (i.static ? ' static' : '')),
|
|
30
48
|
exports: classes.map(c => ({ name: c.name, kind: 'class' })),
|
|
31
49
|
functions: [],
|
|
32
50
|
classes,
|
|
33
|
-
hasMain
|
|
51
|
+
hasMain,
|
|
52
|
+
packageName,
|
|
34
53
|
};
|
|
35
54
|
}
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
// The source root: the part of this file's path that sits *above* its package.
|
|
57
|
+
// `src/main/java/com/example/app/AppController.java` with package
|
|
58
|
+
// `com.example.app` gives `src/main/java/` — and that prefix is exactly what
|
|
59
|
+
// turns `com.example.model.User` into a path worth looking for.
|
|
60
|
+
//
|
|
61
|
+
// Returns '' when the file declares no package: with nothing to strip there is
|
|
62
|
+
// no way to know where the root is, and guessing would mean inventing files.
|
|
63
|
+
export function sourceRootFor(fromPath, packageName) {
|
|
64
|
+
if (!packageName) return null;
|
|
65
|
+
const suffix = packageName.replace(/\./g, '/');
|
|
66
|
+
const normalized = String(fromPath).replace(/\\/g, '/');
|
|
67
|
+
// Two shapes, and both are real. With a source root the package segment is
|
|
68
|
+
// preceded by a directory (`src/main/java/com/acme/…`); in a project with no
|
|
69
|
+
// root at all the path *begins* with the package (`com/acme/…`) and the root
|
|
70
|
+
// is the empty string. That is not the same as "no root" — it is the repo top,
|
|
71
|
+
// which is a perfectly good answer.
|
|
72
|
+
const nested = normalized.lastIndexOf('/' + suffix + '/');
|
|
73
|
+
if (nested >= 0) return normalized.slice(0, nested + 1);
|
|
74
|
+
if (normalized === suffix || normalized.startsWith(suffix + '/')) return '';
|
|
75
|
+
// The package is not in the path at all (a generated source dir, a file moved
|
|
76
|
+
// out of its package). No root can be inferred, and inventing one would put a
|
|
77
|
+
// node in the graph that does not exist.
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Candidate file paths for an import, longest spec first.
|
|
82
|
+
//
|
|
83
|
+
// The longest is the obvious one (`model.User` → `…/model/User.java`). The
|
|
84
|
+
// shorter ones exist because Java lets you import a *nested* type
|
|
85
|
+
// (`com.example.Outer.Inner` lives in `Outer.java`) and a *static member*
|
|
86
|
+
// (`com.example.Constants.MAX` lives in `Constants.java`). Rather than
|
|
87
|
+
// special-case the syntax, walk back a segment at a time and take the first
|
|
88
|
+
// that exists. The static flag only decides where the walk starts, so the
|
|
89
|
+
// common case does not pay for the rare one.
|
|
90
|
+
function candidatesFor(spec, root, { static: isStatic = false } = {}) {
|
|
91
|
+
const parts = spec.split('.').filter(Boolean);
|
|
92
|
+
const out = [];
|
|
93
|
+
const start = isStatic && parts.length > 1 ? 1 : 0;
|
|
94
|
+
for (let end = parts.length; end > start; end -= 1) {
|
|
95
|
+
out.push(root + parts.slice(0, end).join('/') + '.java');
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveImport(spec, fromPath, has, context = {}, meta = {}) {
|
|
101
|
+
const root = sourceRootFor(fromPath, meta.packageName);
|
|
102
|
+
// `null` means "no root could be inferred". `''` means the root is the repo
|
|
103
|
+
// top, which is the correct answer for a project laid out with no source root
|
|
104
|
+
// at all — treating that as "no root" silently dropped every flat-layout import.
|
|
105
|
+
if (root === null) return { unresolved: spec };
|
|
106
|
+
for (const candidate of candidatesFor(spec, root, { static: meta.static })) {
|
|
107
|
+
if (has(candidate)) return { path: candidate };
|
|
108
|
+
}
|
|
109
|
+
// Nothing in the repo matched. The JDK and every third-party library land
|
|
110
|
+
// here, and staying `unresolved` is the honest answer: inventing a file would
|
|
111
|
+
// put a node in the graph that does not exist. scan.js reports these by name
|
|
112
|
+
// so the reader knows exactly what the analyzer could not place.
|
|
38
113
|
return { unresolved: spec };
|
|
39
114
|
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
// Rust analyzer
|
|
2
|
+
//
|
|
3
|
+
// Rust has no `import` in the Java sense: a crate is a module tree, and `use`
|
|
4
|
+
// names a path *into* that tree. The mapping is still real, though, and it is
|
|
5
|
+
// what gives a Rust repo a graph:
|
|
6
|
+
//
|
|
7
|
+
// crate root src/lib.rs, src/main.rs, or crates/<name>/src/lib.rs
|
|
8
|
+
// module `a` src/a.rs or src/a/mod.rs
|
|
9
|
+
// `crate::a::T` T is declared in src/a.rs (or src/a/mod.rs)
|
|
10
|
+
//
|
|
11
|
+
// So a `use` resolves to the *module file* that owns the item, and the leading
|
|
12
|
+
// `crate` is just the crate root. `self::` and `super::` are relative to the
|
|
13
|
+
// current file's module directory. Anything outside the crate — `std`, a
|
|
14
|
+
// dependency from Cargo.toml — stays unresolved, because inventing a file would
|
|
15
|
+
// be a node in the graph that does not exist.
|
|
2
16
|
import { blankComments, lineCounter, uniqueBy } from '../util.js';
|
|
3
17
|
|
|
4
18
|
export const extensions = ['.rs'];
|
|
@@ -8,7 +22,9 @@ export function analyze(source, path) {
|
|
|
8
22
|
const lineAt = lineCounter(clean);
|
|
9
23
|
|
|
10
24
|
const imports = [];
|
|
11
|
-
|
|
25
|
+
// `use foo::bar;` and `mod foo;` are both dependencies on a file, and both are
|
|
26
|
+
// worth an edge. `pub use` is a re-export — still a real dependency.
|
|
27
|
+
for (const m of clean.matchAll(/\b(?:pub\s+)?(?:use|mod)\s+([a-zA-Z0-9_:]+)/g)) {
|
|
12
28
|
imports.push({ spec: m[1], line: lineAt(m.index) });
|
|
13
29
|
}
|
|
14
30
|
|
|
@@ -37,6 +53,92 @@ export function analyze(source, path) {
|
|
|
37
53
|
};
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
const dirOfPath = (p) => {
|
|
57
|
+
const at = String(p).lastIndexOf('/');
|
|
58
|
+
return at < 0 ? '' : String(p).slice(0, at + 1);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// The crate root: the `src/` directory, wherever it sits. `src/lib.rs` → `src/`;
|
|
62
|
+
// `crates/foo/src/main.rs` → `crates/foo/src/`. A crate with no `src/` (a flat
|
|
63
|
+
// `lib.rs` in the repo root) is rooted at that file's own directory, which is the
|
|
64
|
+
// same rule with one fewer component to find.
|
|
65
|
+
export function crateRootFor(fromPath) {
|
|
66
|
+
const normalized = String(fromPath).replace(/\\/g, '/');
|
|
67
|
+
const segments = normalized.split('/');
|
|
68
|
+
const at = segments.lastIndexOf('src');
|
|
69
|
+
if (at < 0) return dirOfPath(normalized);
|
|
70
|
+
return segments.slice(0, at + 1).join('/') + '/';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The directory that holds the *children* of the module this file defines.
|
|
74
|
+
//
|
|
75
|
+
// This is not simply the dirname, and getting it wrong breaks every `self::`.
|
|
76
|
+
// src/lib.rs → `src/` the crate root's children sit in src/
|
|
77
|
+
// src/a/b.rs → `src/a/b/` the module is `a::b`, so `self::x` is a/b/x.rs
|
|
78
|
+
// src/a/b/mod.rs → `src/a/b/` the same module, declared the directory way
|
|
79
|
+
//
|
|
80
|
+
// The lib/main special case is the one that is easy to miss: they are named
|
|
81
|
+
// after the crate, not after their module, so stripping `.rs` from `src/lib.rs`
|
|
82
|
+
// would invent a module called `lib`.
|
|
83
|
+
export function moduleDirFor(fromPath) {
|
|
84
|
+
const normalized = String(fromPath).replace(/\\/g, '/');
|
|
85
|
+
const dir = dirOfPath(normalized);
|
|
86
|
+
const name = normalized.slice(dir.length);
|
|
87
|
+
if (name === 'mod.rs' || name === 'lib.rs' || name === 'main.rs') return dir;
|
|
88
|
+
return normalized.replace(/\.rs$/, '') + '/';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A module named by a path is either `path.rs` or `path/mod.rs`. Both are
|
|
92
|
+
// idiomatic Rust, so both are tried and the first that exists wins.
|
|
93
|
+
function moduleCandidates(path) {
|
|
94
|
+
return [path + '.rs', path + '/mod.rs'];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveImport(spec, fromPath, has, context = {}, meta = {}) {
|
|
98
|
+
const from = String(fromPath).replace(/\\/g, '/');
|
|
99
|
+
const raw = String(spec);
|
|
100
|
+
// `::foo::Bar` is a 2018 absolute path — another crate, not this one.
|
|
101
|
+
if (raw.startsWith('::')) return { unresolved: spec };
|
|
102
|
+
const segments = raw.split('::').filter(Boolean);
|
|
103
|
+
if (!segments.length) return { unresolved: spec };
|
|
104
|
+
|
|
105
|
+
// Where the path starts. `crate::` is the crate root; `self::` and a bare
|
|
106
|
+
// `mod foo;` are the current file's own module; `super::` is one level up.
|
|
107
|
+
const head = segments[0];
|
|
108
|
+
let base;
|
|
109
|
+
let rest;
|
|
110
|
+
if (head === 'crate') {
|
|
111
|
+
base = crateRootFor(from);
|
|
112
|
+
rest = segments.slice(1);
|
|
113
|
+
} else if (head === 'super') {
|
|
114
|
+
// `dirOfPath` already ends in a separator, so do not add another — `src/a//`
|
|
115
|
+
// never matches a real path, and the miss would be silent.
|
|
116
|
+
base = dirOfPath(moduleDirFor(from).replace(/\/$/, ''));
|
|
117
|
+
rest = segments.slice(1);
|
|
118
|
+
} else if (head === 'self') {
|
|
119
|
+
base = moduleDirFor(from);
|
|
120
|
+
rest = segments.slice(1);
|
|
121
|
+
} else {
|
|
122
|
+
// A bare `mod foo;` declares a module beside the current one. A bare
|
|
123
|
+
// `use foo::Bar;` means an *external crate* in Rust 2018, so it will not be
|
|
124
|
+
// found here and stays unresolved — which is the right answer for
|
|
125
|
+
// `serde::Serialize` and every other dependency.
|
|
126
|
+
base = moduleDirFor(from);
|
|
127
|
+
rest = segments;
|
|
128
|
+
}
|
|
129
|
+
if (!rest.length) return { unresolved: spec };
|
|
130
|
+
|
|
131
|
+
// `crate::a::b::T` means T is declared *in* the module `a::b`, so the answer is
|
|
132
|
+
// the module file for the longest prefix that exists. Trying the deepest path
|
|
133
|
+
// first costs one lookup and covers the flat `mod a;` → `a.rs` case.
|
|
134
|
+
for (let end = rest.length; end > 0; end -= 1) {
|
|
135
|
+
const candidate = base + rest.slice(0, end).join('/');
|
|
136
|
+
for (const file of moduleCandidates(candidate)) {
|
|
137
|
+
if (has(file)) return { path: file };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// `std`, `serde`, anything from Cargo.toml: outside this crate. Unresolved is
|
|
142
|
+
// the honest answer, and scan.js reports these by name.
|
|
41
143
|
return { unresolved: spec };
|
|
42
144
|
}
|
package/shared/analyzer/scan.js
CHANGED
|
@@ -126,8 +126,31 @@ export async function scanRepo(source, options = {}) {
|
|
|
126
126
|
}
|
|
127
127
|
const findByName = (n) => nameIndex.get(n) || null;
|
|
128
128
|
|
|
129
|
+
// Find a directory whose path *ends with* a run of segments. This is what C#
|
|
130
|
+
// needs and Java does not: a C# file's folder mirrors the namespace, but the
|
|
131
|
+
// root namespace (the company or project name) is normally not a directory,
|
|
132
|
+
// so `namespace Acme.Services` lives in `src/Services/`. Matching on a path
|
|
133
|
+
// suffix is the only thing that finds it — stripping the whole namespace, the
|
|
134
|
+
// Java approach, matches nothing and silently drops every import.
|
|
135
|
+
//
|
|
136
|
+
// Keyed by the last three segments, which is enough to tell
|
|
137
|
+
// `Acme/Models/Entities` apart from a bare `Entities` elsewhere while staying
|
|
138
|
+
// O(1) per lookup. A collision at the shortest key keeps the first match, and
|
|
139
|
+
// callers query longest-first, so the most specific answer is tried first.
|
|
140
|
+
const dirByTail = new Map();
|
|
141
|
+
for (const d of dirSet) {
|
|
142
|
+
if (!d) continue;
|
|
143
|
+
const segments = d.split('/');
|
|
144
|
+
for (let take = 1; take <= 3 && take <= segments.length; take += 1) {
|
|
145
|
+
const key = segments.slice(segments.length - take).join('/');
|
|
146
|
+
if (!dirByTail.has(key)) dirByTail.set(key, d);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const findDirEndingWith = (suffix) => (suffix ? dirByTail.get(suffix) || null : null);
|
|
150
|
+
|
|
129
151
|
const context = {
|
|
130
152
|
hasDir,
|
|
153
|
+
findDirEndingWith,
|
|
131
154
|
findByName,
|
|
132
155
|
modulePath: await readModulePath(source),
|
|
133
156
|
tsPaths: await readTsConfigPaths(source),
|
|
@@ -212,15 +235,20 @@ export async function scanRepo(source, options = {}) {
|
|
|
212
235
|
const tally = { total: 0, internal: 0, external: 0, unresolved: 0 };
|
|
213
236
|
const unresolvedSpecs = new Map(); // spec -> { count, from }
|
|
214
237
|
|
|
215
|
-
//
|
|
238
|
+
// A resolver may answer with a *directory* instead of a file: Go's import is a
|
|
239
|
+
// package, and a C# `using Acme.Models;` names a namespace whose classes we
|
|
240
|
+
// cannot know from the import alone. Every file in that directory is a
|
|
216
241
|
// target. Indexed once here rather than scanned per import — a Go repo with
|
|
217
242
|
// 1,000 files and 8,000 imports was doing eight million comparisons for it.
|
|
218
|
-
|
|
243
|
+
//
|
|
244
|
+
// Language-neutral on purpose: this used to be `goFilesByDir`, built only from
|
|
245
|
+
// Go files, which meant no other language could use the mechanism that already
|
|
246
|
+
// existed for exactly this problem.
|
|
247
|
+
const filesByDir = new Map();
|
|
219
248
|
for (const f of files) {
|
|
220
|
-
|
|
221
|
-
const bucket = goFilesByDir.get(f.dir);
|
|
249
|
+
const bucket = filesByDir.get(f.dir);
|
|
222
250
|
if (bucket) bucket.push(f.path);
|
|
223
|
-
else
|
|
251
|
+
else filesByDir.set(f.dir, [f.path]);
|
|
224
252
|
}
|
|
225
253
|
|
|
226
254
|
for (const file of files) {
|
|
@@ -233,7 +261,10 @@ export async function scanRepo(source, options = {}) {
|
|
|
233
261
|
addEdge(edges, edgeKeys, file.path, res.path, 'imports', imp.symbols);
|
|
234
262
|
} else if (res.packageDir) {
|
|
235
263
|
tally.internal++;
|
|
236
|
-
|
|
264
|
+
// A directory import means "everything in there". addEdge de-duplicates,
|
|
265
|
+
// so a package with fifty files yields fifty honest edges rather than
|
|
266
|
+
// one edge standing in for all of them.
|
|
267
|
+
for (const target of filesByDir.get(res.packageDir) || []) {
|
|
237
268
|
addEdge(edges, edgeKeys, file.path, target, 'imports', imp.symbols);
|
|
238
269
|
}
|
|
239
270
|
} else if (res.external) {
|