@ecoma-io/archkeep 0.13.0
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/LICENSE +202 -0
- package/README.md +262 -0
- package/cli.mjs +2792 -0
- package/index.mjs +85 -0
- package/lsp.mjs +81 -0
- package/nx.mjs +24 -0
- package/package.json +81 -0
- package/presets/clean-architecture.json +78 -0
- package/presets/ddd-bounded-contexts.json +88 -0
- package/presets/hexagonal.json +68 -0
- package/presets/layered.json +92 -0
- package/presets/modular-monolith.json +85 -0
- package/presets/vertical-slice.json +68 -0
- package/src/analysis/analyze.mjs +218 -0
- package/src/analysis/contract.md +259 -0
- package/src/analysis/go.mjs +414 -0
- package/src/analysis/manifest-util.mjs +68 -0
- package/src/analysis/python.mjs +1266 -0
- package/src/analysis/registry.mjs +74 -0
- package/src/analysis/rust.mjs +674 -0
- package/src/analysis/source-util.mjs +230 -0
- package/src/analysis/typescript.mjs +1034 -0
- package/src/analysis/vue.mjs +156 -0
- package/src/architecture-intent/intent-fingerprint.mjs +29 -0
- package/src/architecture-intent/judge.mjs +539 -0
- package/src/architecture-intent/model.mjs +703 -0
- package/src/architecture-intent/selectors.mjs +170 -0
- package/src/canonical.mjs +48 -0
- package/src/commands/README.md +266 -0
- package/src/commands/adr.mjs +248 -0
- package/src/commands/check.mjs +989 -0
- package/src/commands/context-command.mjs +212 -0
- package/src/commands/context.mjs +790 -0
- package/src/commands/custom-rules.mjs +428 -0
- package/src/commands/debt.mjs +218 -0
- package/src/commands/diff.mjs +523 -0
- package/src/commands/discover.mjs +159 -0
- package/src/commands/drift.mjs +473 -0
- package/src/commands/edge-constraints.mjs +355 -0
- package/src/commands/explain.mjs +359 -0
- package/src/commands/fitness.mjs +226 -0
- package/src/commands/graph.mjs +297 -0
- package/src/commands/health.mjs +213 -0
- package/src/commands/history.mjs +614 -0
- package/src/commands/impact.mjs +226 -0
- package/src/commands/plan-context-command.mjs +496 -0
- package/src/commands/policy.mjs +138 -0
- package/src/commands/provenance-command.mjs +352 -0
- package/src/commands/provenance.mjs +159 -0
- package/src/commands/reconcile.mjs +219 -0
- package/src/commands/report.mjs +553 -0
- package/src/commands/snapshot-meta.mjs +107 -0
- package/src/commands/waivers.mjs +240 -0
- package/src/config.mjs +1308 -0
- package/src/containment.mjs +234 -0
- package/src/custom-rules/evidence.mjs +340 -0
- package/src/custom-rules/host.mjs +1023 -0
- package/src/custom-rules/values.mjs +43 -0
- package/src/entry-point.mjs +55 -0
- package/src/errors.mjs +36 -0
- package/src/eslint-config.mjs +542 -0
- package/src/go-work.mjs +394 -0
- package/src/governance/adr-registry.mjs +539 -0
- package/src/governance/clock.mjs +69 -0
- package/src/governance/debt-ledger.mjs +274 -0
- package/src/governance/discovery-proposal.mjs +423 -0
- package/src/governance/fitness-registry.mjs +504 -0
- package/src/governance/fitness-rules.mjs +668 -0
- package/src/governance/metrics.mjs +392 -0
- package/src/governance/preset-fingerprints.json +16 -0
- package/src/governance/profile-registry.mjs +366 -0
- package/src/governance/provenance-record.mjs +177 -0
- package/src/governance/reconcile-candidates.mjs +301 -0
- package/src/governance/reconcile-score.mjs +503 -0
- package/src/governance/row-schema.mjs +208 -0
- package/src/governance/verdict.mjs +127 -0
- package/src/governance/waiver.mjs +105 -0
- package/src/graph/create-dependencies.mjs +96 -0
- package/src/intent/intent-manifest.json +347 -0
- package/src/intent/mask-non-code.mjs +640 -0
- package/src/lsp/boundary-config.mjs +225 -0
- package/src/lsp/diagnose.mjs +202 -0
- package/src/lsp/diagnostics.mjs +241 -0
- package/src/lsp/protocol.mjs +215 -0
- package/src/lsp/server.mjs +922 -0
- package/src/lsp/workspace-index.mjs +891 -0
- package/src/nx-json.mjs +95 -0
- package/src/options.mjs +611 -0
- package/src/process.mjs +91 -0
- package/src/providers/moon.mjs +733 -0
- package/src/providers/native/README.md +204 -0
- package/src/providers/native/coverage.mjs +74 -0
- package/src/providers/native/differential.fixtures.mjs +1277 -0
- package/src/providers/native/discover.mjs +431 -0
- package/src/providers/native/graph.mjs +234 -0
- package/src/providers/native/index.mjs +152 -0
- package/src/providers/native/model.mjs +755 -0
- package/src/providers/nx.mjs +178 -0
- package/src/report/README.md +89 -0
- package/src/report/adr-text.mjs +129 -0
- package/src/report/context-text.mjs +109 -0
- package/src/report/debt-text.mjs +105 -0
- package/src/report/diff-text.mjs +219 -0
- package/src/report/discover-text.mjs +186 -0
- package/src/report/drift-text.mjs +194 -0
- package/src/report/envelope-shape.mjs +161 -0
- package/src/report/evidence.mjs +157 -0
- package/src/report/explain-text.mjs +159 -0
- package/src/report/graph-text.mjs +116 -0
- package/src/report/health-text.mjs +123 -0
- package/src/report/history-text.mjs +204 -0
- package/src/report/impact-text.mjs +128 -0
- package/src/report/json.mjs +173 -0
- package/src/report/plan-context-text.mjs +159 -0
- package/src/report/provenance-text.mjs +78 -0
- package/src/report/reconcile-text.mjs +159 -0
- package/src/report/report-text.mjs +264 -0
- package/src/report/sarif.mjs +953 -0
- package/src/report/text.mjs +823 -0
- package/src/report/waivers-text.mjs +100 -0
- package/src/rules/README.md +123 -0
- package/src/rules/index.mjs +962 -0
- package/src/rules/match.mjs +1708 -0
- package/src/rules/messages.mjs +73 -0
- package/src/rules/reachability.mjs +224 -0
- package/src/rules/specifiers.mjs +300 -0
- package/src/rules/tags.mjs +238 -0
- package/src/rules/topology.mjs +333 -0
- package/src/tsconfig-paths.mjs +237 -0
- package/src/verdict.mjs +145 -0
- package/src/workspace.mjs +580 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust resolver — reads Cargo manifests with a real TOML parser (smol-toml),
|
|
3
|
+
* no `cargo` binary required.
|
|
4
|
+
*
|
|
5
|
+
* Model: one crate per Nx project (`<projectRoot>/Cargo.toml`). Edges come
|
|
6
|
+
* from dependency entries that resolve to another project's directory:
|
|
7
|
+
* - `{ path = "…" }` entries, relative to the declaring manifest;
|
|
8
|
+
* - `{ workspace = true }` entries, resolved through the nearest ancestor
|
|
9
|
+
* manifest carrying `[workspace]` (its `[workspace.dependencies]` entry
|
|
10
|
+
* must itself be a `path` dependency to point at a project).
|
|
11
|
+
* Registry (crates.io) dependencies are ignored — external nodes are out of
|
|
12
|
+
* scope for this plugin; only project↔project edges matter to `nx affected`.
|
|
13
|
+
*
|
|
14
|
+
* ## Source level, alongside the manifests
|
|
15
|
+
*
|
|
16
|
+
* `analyzeRust` reads `.rs` sources for what the manifests cannot show: WHICH
|
|
17
|
+
* file reaches another crate, on which line, through which path. A Cargo
|
|
18
|
+
* manifest says a crate may be used; it never says a boundary was crossed, and
|
|
19
|
+
* `Cargo.toml:1` is not a location a reader can act on.
|
|
20
|
+
*
|
|
21
|
+
* The crate a `use` names is the **import** name, which is not always the
|
|
22
|
+
* package name. Two spellings have to be reconciled, and both occur in real
|
|
23
|
+
* trees: Cargo replaces `-` with `_` for the identifier (`engine-core`
|
|
24
|
+
* is `use engine_core::…`), and a `[lib] name` overrides the package name
|
|
25
|
+
* outright — a Tauri desktop package declaring `[lib] name = "app_lib"` makes
|
|
26
|
+
* that the only spelling its own `main.rs` can use. Both sides are normalised to
|
|
27
|
+
* underscores before matching, and `[lib] name` wins when it is present.
|
|
28
|
+
*
|
|
29
|
+
* Known parse limits, deliberate and pinned by tests. The worst case of each
|
|
30
|
+
* is a spurious record naming text the file really contains — never a missed
|
|
31
|
+
* project, which is the standard the Go header sets:
|
|
32
|
+
*
|
|
33
|
+
* - **`use` is matched at a line start, after a `;`/`{`/`}`, or after a
|
|
34
|
+
* same-line attribute block**, read up to but NOT consuming the `;` that
|
|
35
|
+
* closes it — the path ends AT that `;` and the scan resumes there, so the
|
|
36
|
+
* same `;` is still available to open the next match's `(?:^|[{;}])` when a
|
|
37
|
+
* second `use` shares the line. Every position Rust allows a `use` statement
|
|
38
|
+
* starts one of those ways, and the closing `;` is never swallowed, so two
|
|
39
|
+
* or more `use` statements sharing one line are each read, not just the
|
|
40
|
+
* first. A same-line attribute's bracketed
|
|
41
|
+
* content is read past a balanced quoted string rather than stopping at the
|
|
42
|
+
* first `]`, so `#[doc = "see [x]"] use a::b;` still reaches its `use`. A
|
|
43
|
+
* `use` inside a raw string literal that starts its own line would be read,
|
|
44
|
+
* and a `;` inside a comment or string can let a `use` written there be
|
|
45
|
+
* read — both are the accepted spurious-record trade (text the file really
|
|
46
|
+
* contains), never a missed project.
|
|
47
|
+
* - **A `use` whose path opens with a brace group** — `use {a::b, c::d};` — is
|
|
48
|
+
* a LIST of paths and is read as one: each arm names its own crate at its
|
|
49
|
+
* head, so the statement means exactly `use a::b; use c::d;` and produces one
|
|
50
|
+
* record per arm, at the arm's own position. Nothing is guessed, because
|
|
51
|
+
* nothing is ambiguous. Only text that is not a well-formed group — braces
|
|
52
|
+
* that do not balance, or anything after the group's close — keeps the older
|
|
53
|
+
* answer: one record with `resolved: null` and a failure beside it, never a
|
|
54
|
+
* dropped record.
|
|
55
|
+
* - **Uniform paths are ambiguous and resolved toward the crate.** Since Rust
|
|
56
|
+
* 2018, `use foo::Bar` can name either an extern crate `foo` or a local
|
|
57
|
+
* `mod foo`. A first segment matching another project's crate name is read
|
|
58
|
+
* as that crate. A local module deliberately named after a sibling crate
|
|
59
|
+
* would produce a spurious record.
|
|
60
|
+
* - **`mod` is not an import.** A `mod` declaration names a file inside the
|
|
61
|
+
* same crate, so it crosses no project boundary and is never recorded.
|
|
62
|
+
* - **A UTF-8 BOM before a first-line statement is tolerated** (`contract.md`,
|
|
63
|
+
* byte tolerance): every anchor here is `^` or a character class the BOM
|
|
64
|
+
* fails, so an editor-written `\uFEFF` used to drop a first-line `use` and
|
|
65
|
+
* a first-line `extern crate` — records gone with no failure beside them.
|
|
66
|
+
* The BOM is blanked to a space, not stripped, so every offset stays an
|
|
67
|
+
* offset into the file as it sits on disk; the bare-path and fully-qualified
|
|
68
|
+
* forms needed nothing, their `(^|[^…])` prefix already reads the BOM as
|
|
69
|
+
* ordinary preceding text.
|
|
70
|
+
*
|
|
71
|
+
* **A renamed dependency IS followed, scoped to the project that renamed
|
|
72
|
+
* it.** `dep = { package = "real", path = "../real" }` in a project's own
|
|
73
|
+
* Cargo.toml makes `real`'s crate reachable from THAT project's `.rs` sources
|
|
74
|
+
* only under the identifier `dep` — Rust builds a crate's `extern` prelude
|
|
75
|
+
* from its own manifest alone, so a different project renaming something else
|
|
76
|
+
* to `dep`, or `dep` happening to be nobody's rename at all, has no bearing on
|
|
77
|
+
* what THIS project's `use dep::…` means. `renamedDepsOf` below resolves the
|
|
78
|
+
* rename exactly the way `resolveRustDependencies` resolves the manifest
|
|
79
|
+
* entry it comes from — same `path`/`workspace = true` handling — so a `use`
|
|
80
|
+
* naming the rename lands on the same project the graph edge already points
|
|
81
|
+
* at, never a second, disagreeing answer.
|
|
82
|
+
*/
|
|
83
|
+
import { normalizePath, parseManifest } from "./manifest-util.mjs";
|
|
84
|
+
import {
|
|
85
|
+
emptyResult,
|
|
86
|
+
fileFailure,
|
|
87
|
+
lineStartsOf,
|
|
88
|
+
perWorkspace,
|
|
89
|
+
positionAt,
|
|
90
|
+
projectOwning,
|
|
91
|
+
trackedManifests,
|
|
92
|
+
} from "./source-util.mjs";
|
|
93
|
+
|
|
94
|
+
const DEP_SECTIONS = ["dependencies", "dev-dependencies", "build-dependencies"];
|
|
95
|
+
|
|
96
|
+
/** All dependency tables in a manifest: top-level plus per-target ones. */
|
|
97
|
+
function* depTables(manifest) {
|
|
98
|
+
for (const section of DEP_SECTIONS) {
|
|
99
|
+
if (manifest[section]) yield manifest[section];
|
|
100
|
+
}
|
|
101
|
+
for (const targetCfg of Object.values(manifest.target ?? {})) {
|
|
102
|
+
for (const section of DEP_SECTIONS) {
|
|
103
|
+
if (targetCfg?.[section]) yield targetCfg[section];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Nearest ancestor dir (starting at the parent of `startDir`) whose
|
|
110
|
+
* Cargo.toml declares `[workspace]`; `{ dir, manifest }` or null.
|
|
111
|
+
*/
|
|
112
|
+
function findWorkspaceManifest(startDir, readFile) {
|
|
113
|
+
let dir = startDir;
|
|
114
|
+
while (dir.includes("/")) {
|
|
115
|
+
dir = dir.slice(0, dir.lastIndexOf("/"));
|
|
116
|
+
const manifest = parseManifest(readFile(`${dir}/Cargo.toml`) ?? "");
|
|
117
|
+
if (manifest?.workspace) return { dir, manifest };
|
|
118
|
+
}
|
|
119
|
+
const manifest = parseManifest(readFile("Cargo.toml") ?? "");
|
|
120
|
+
return manifest?.workspace ? { dir: "", manifest } : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Static edges between Rust projects. Same contract as the Go resolver:
|
|
125
|
+
* `projects` [{ name, root }], `filesOf(name)`, `readFile(path)` → raw deps.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveRustDependencies(projects, filesOf, readFile) {
|
|
128
|
+
const projectByRoot = new Map();
|
|
129
|
+
const crates = [];
|
|
130
|
+
for (const project of projects) {
|
|
131
|
+
const manifestPath = normalizePath(project.root, "Cargo.toml");
|
|
132
|
+
if (!filesOf(project.name).includes(manifestPath)) continue;
|
|
133
|
+
const manifest = parseManifest(readFile(manifestPath) ?? "");
|
|
134
|
+
if (!manifest?.package?.name) continue; // workspace-only manifests are not crates
|
|
135
|
+
// Normalized, not raw: Nx spells the workspace-root project's `root` as
|
|
136
|
+
// `"."`, which `normalizePath` collapses to `""` — the same value a
|
|
137
|
+
// dependency pointing AT that project resolves `pathDir` to below. Keying
|
|
138
|
+
// on the raw `root` would leave `"."` in the map and miss every lookup.
|
|
139
|
+
projectByRoot.set(normalizePath(project.root, ""), project.name);
|
|
140
|
+
crates.push({ project, manifest, manifestPath });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const dependencies = [];
|
|
144
|
+
for (const { project, manifest, manifestPath } of crates) {
|
|
145
|
+
const workspace = { resolved: false, value: null }; // lazy per-crate lookup
|
|
146
|
+
for (const table of depTables(manifest)) {
|
|
147
|
+
for (const [depName, spec] of Object.entries(table)) {
|
|
148
|
+
if (typeof spec !== "object" || spec === null) continue;
|
|
149
|
+
let pathDir = null;
|
|
150
|
+
if (typeof spec.path === "string") {
|
|
151
|
+
pathDir = normalizePath(project.root, spec.path);
|
|
152
|
+
} else if (spec.workspace === true) {
|
|
153
|
+
if (!workspace.resolved) {
|
|
154
|
+
workspace.resolved = true;
|
|
155
|
+
workspace.value = findWorkspaceManifest(project.root, readFile);
|
|
156
|
+
}
|
|
157
|
+
// `[workspace.dependencies]` is keyed by the member's LOCAL name,
|
|
158
|
+
// the key Cargo inherits from (measured against cargo 1.96: a member
|
|
159
|
+
// `foo = { package = "bar", workspace = true }` fails to parse). A
|
|
160
|
+
// rename on an inherited dep lives in the workspace spec's own
|
|
161
|
+
// `package`, and `wsSpec.path` still resolves the crate.
|
|
162
|
+
const wsSpec = workspace.value?.manifest.workspace?.dependencies?.[depName];
|
|
163
|
+
if (typeof wsSpec?.path === "string") {
|
|
164
|
+
pathDir = normalizePath(workspace.value.dir, wsSpec.path);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// `pathDir` is `""` (falsy, but a real answer) when a dependency
|
|
168
|
+
// points AT the workspace-root project — `normalizePath` resolves an
|
|
169
|
+
// in-tree path that climbs all the way to the root as the empty
|
|
170
|
+
// string. `projectByRoot` is keyed the same way (normalized, not
|
|
171
|
+
// raw), so this matches that project regardless of whether its own
|
|
172
|
+
// `root` is spelled `""` or Nx's `"."`. Only `null` (no
|
|
173
|
+
// `path`/`workspace = true` resolved at all) means "no target".
|
|
174
|
+
if (pathDir === null) continue;
|
|
175
|
+
const target = projectByRoot.get(pathDir);
|
|
176
|
+
if (target && target !== project.name) {
|
|
177
|
+
dependencies.push({
|
|
178
|
+
source: project.name,
|
|
179
|
+
target,
|
|
180
|
+
sourceFile: manifestPath,
|
|
181
|
+
type: "static",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return dependencies;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Cargo's identifier spelling of a crate name: `-` and `.` become `_`. */
|
|
191
|
+
export function crateIdentifier(name) {
|
|
192
|
+
return name.replace(/[-.]/g, "_");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The name every `.rs` source must spell to reach a crate: `[lib] name` when
|
|
197
|
+
* the manifest overrides it, the package name otherwise, always as an
|
|
198
|
+
* identifier.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} manifest A parsed Cargo.toml.
|
|
201
|
+
* @returns {string|null}
|
|
202
|
+
*/
|
|
203
|
+
export function crateImportName(manifest) {
|
|
204
|
+
const declared = manifest?.lib?.name ?? manifest?.package?.name;
|
|
205
|
+
return typeof declared === "string" && declared !== "" ? crateIdentifier(declared) : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Crate import name → project name, over every tracked `Cargo.toml` in each
|
|
210
|
+
* project rather than only the one at its root — see `trackedManifests` for
|
|
211
|
+
* why analysis is broader here than the edge resolver above, and for the one
|
|
212
|
+
* project in this workspace that needs it. A workspace-only manifest declares
|
|
213
|
+
* no `[package]`, so the repo-root `Cargo.toml` contributes nothing.
|
|
214
|
+
*/
|
|
215
|
+
const crateNamesOf = perWorkspace((workspace) => {
|
|
216
|
+
const byCrate = new Map();
|
|
217
|
+
for (const project of workspace.projects) {
|
|
218
|
+
for (const manifestPath of trackedManifests(workspace, project.name, "Cargo.toml")) {
|
|
219
|
+
const crate = crateImportName(parseManifest(workspace.readFile(manifestPath) ?? ""));
|
|
220
|
+
if (crate) byCrate.set(crate, project.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return byCrate;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Per-project renamed-dependency aliases — see the header's "A renamed
|
|
228
|
+
* dependency IS followed". Reads each project's OWN Cargo.toml dependency
|
|
229
|
+
* tables (the same `depTables` the edge resolver walks) for an entry carrying
|
|
230
|
+
* `package = "…"`, and resolves its `path`/`workspace = true` spec exactly
|
|
231
|
+
* the way `resolveRustDependencies` does, so the two can never disagree about
|
|
232
|
+
* which project a rename reaches.
|
|
233
|
+
*
|
|
234
|
+
* Scoped per project rather than folded into the single global `crateNamesOf`
|
|
235
|
+
* map: the alias is a fact about the IMPORTER's manifest, not the imported
|
|
236
|
+
* crate, and a sibling project could rename the very same dependency to a
|
|
237
|
+
* different local name, or not rename it at all.
|
|
238
|
+
*
|
|
239
|
+
* Only at the project root, matching `resolveRustDependencies` rather than
|
|
240
|
+
* `crateNamesOf`'s broader `trackedManifests` walk — a nested manifest (the
|
|
241
|
+
* Tauri `src-tauri/` shape) draws no graph edge to resolve a rename against
|
|
242
|
+
* in the first place, so there is no target here to be consistent with.
|
|
243
|
+
*
|
|
244
|
+
* @returns {Map<string, Map<string, string>>} project name -> (the identifier
|
|
245
|
+
* a `.rs` file spells -> the project the rename actually reaches).
|
|
246
|
+
*/
|
|
247
|
+
const renamedDepsOf = perWorkspace((workspace) => {
|
|
248
|
+
const projectByRoot = new Map();
|
|
249
|
+
// Normalized, not raw — see the identical comment in
|
|
250
|
+
// `resolveRustDependencies` above: Nx's `"."` root spelling and `""` both
|
|
251
|
+
// have to land on the same map key for a rename pointing at the
|
|
252
|
+
// workspace-root project to resolve.
|
|
253
|
+
for (const project of workspace.projects) {
|
|
254
|
+
projectByRoot.set(normalizePath(project.root, ""), project.name);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const byProject = new Map();
|
|
258
|
+
for (const project of workspace.projects) {
|
|
259
|
+
const manifestPath = normalizePath(project.root, "Cargo.toml");
|
|
260
|
+
if (!workspace.filesOf(project.name).includes(manifestPath)) continue;
|
|
261
|
+
const manifest = parseManifest(workspace.readFile(manifestPath) ?? "");
|
|
262
|
+
if (!manifest) continue;
|
|
263
|
+
|
|
264
|
+
const aliases = new Map();
|
|
265
|
+
const ws = { resolved: false, value: null }; // lazy per-crate lookup, as the edge resolver
|
|
266
|
+
for (const table of depTables(manifest)) {
|
|
267
|
+
for (const [depName, spec] of Object.entries(table)) {
|
|
268
|
+
if (typeof spec !== "object" || spec === null) continue;
|
|
269
|
+
let renamed = false;
|
|
270
|
+
let pathDir = null;
|
|
271
|
+
if (typeof spec.path === "string") {
|
|
272
|
+
pathDir = normalizePath(project.root, spec.path);
|
|
273
|
+
renamed = typeof spec.package === "string";
|
|
274
|
+
} else if (spec.workspace === true) {
|
|
275
|
+
if (!ws.resolved) {
|
|
276
|
+
ws.resolved = true;
|
|
277
|
+
ws.value = findWorkspaceManifest(project.root, workspace.readFile);
|
|
278
|
+
}
|
|
279
|
+
// `[workspace.dependencies]` is keyed by the member's LOCAL name
|
|
280
|
+
// (`depName`), which is the key Cargo inherits from — measured
|
|
281
|
+
// against cargo 1.96, a member `foo = { package = "bar",
|
|
282
|
+
// workspace = true }` fails to parse. A rename on an inherited dep
|
|
283
|
+
// lives in the WORKSPACE spec (`as_real = { path = …, package =
|
|
284
|
+
// "real" }`), never on the member entry, which may carry no
|
|
285
|
+
// `package` at all.
|
|
286
|
+
const wsSpec = ws.value?.manifest.workspace?.dependencies?.[depName];
|
|
287
|
+
if (typeof wsSpec?.path === "string") {
|
|
288
|
+
pathDir = normalizePath(ws.value.dir, wsSpec.path);
|
|
289
|
+
renamed = typeof spec.package === "string" || typeof wsSpec.package === "string";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// A plain `use real::…` resolves through `crateNamesOf`; the alias
|
|
293
|
+
// map is only for names the local spelling does not match, so an
|
|
294
|
+
// entry that is not a rename contributes nothing here. `pathDir` is
|
|
295
|
+
// `""` (falsy, but a real answer) when the dependency points AT the
|
|
296
|
+
// workspace-root project — see the identical guard and its comment in
|
|
297
|
+
// `resolveRustDependencies` above — so the "no target" check and the
|
|
298
|
+
// "not a rename" check must stay two separate conditions.
|
|
299
|
+
if (pathDir === null) continue;
|
|
300
|
+
if (!renamed) continue;
|
|
301
|
+
const target = projectByRoot.get(pathDir);
|
|
302
|
+
if (target && target !== project.name) aliases.set(crateIdentifier(depName), target);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (aliases.size > 0) byProject.set(project.name, aliases);
|
|
306
|
+
}
|
|
307
|
+
return byProject;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
/** Path prefixes that name the crate being compiled rather than another one. */
|
|
311
|
+
const OWN_CRATE_ROOTS = new Set(["crate", "self", "super"]);
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Is this `use` path spelled as a reference inside the file's own project —
|
|
315
|
+
* the `spelling.relative` bit of the analysis record (`contract.md`)?
|
|
316
|
+
*
|
|
317
|
+
* Two spellings qualify, and the second is the one a JavaScript-shaped
|
|
318
|
+
* predicate cannot see.
|
|
319
|
+
*
|
|
320
|
+
* 1. **`crate::`, `self::`, `super::`** — Rust's relative forms. They are what
|
|
321
|
+
* `./x` and `../x` are to JavaScript, and a `.rs` file that uses them has
|
|
322
|
+
* not left its crate at all.
|
|
323
|
+
* 2. **A crate name this file's OWN project declares.** One Cargo package
|
|
324
|
+
* compiles several crates — a `[lib]`, a `[[bin]]`, tests, examples — and a
|
|
325
|
+
* binary reaches its package's library by naming it (`rba_desktop_lib::run`,
|
|
326
|
+
* which `[lib] name` may rename outright). Nx models the package as one
|
|
327
|
+
* project, so source and target land on the same node; Cargo offers no other
|
|
328
|
+
* spelling for it, and its crate graph cannot cycle, so this is never the
|
|
329
|
+
* round trip out through a public alias and back in that
|
|
330
|
+
* `noSelfCircularDependencies` names.
|
|
331
|
+
*
|
|
332
|
+
* Nothing here is a filesystem path: a `use` path names items inside a module
|
|
333
|
+
* tree, so `spelling.path` is always false for Rust.
|
|
334
|
+
*
|
|
335
|
+
* @param {string|null} root The `use` path's first segment; `null` for a brace group.
|
|
336
|
+
* @param {{name: string}|null} owner The project owning the source file.
|
|
337
|
+
* @param {Map<string, string>} byCrate Crate import name → project name.
|
|
338
|
+
* @returns {boolean}
|
|
339
|
+
*/
|
|
340
|
+
function isOwnProjectPath(root, owner, byCrate) {
|
|
341
|
+
if (root === null) return false;
|
|
342
|
+
if (OWN_CRATE_ROOTS.has(root)) return true;
|
|
343
|
+
return owner !== null && byCrate.get(crateIdentifier(root)) === owner.name;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* The arms of a `use` path that opens with a brace group, each with its offset
|
|
348
|
+
* inside `path`.
|
|
349
|
+
*
|
|
350
|
+
* `use {a::b, c::d};` is not ambiguous and never was: the group is a list, and
|
|
351
|
+
* every arm is a complete path naming its own crate at its head — the
|
|
352
|
+
* statement means exactly `use a::b; use c::d;`. Reading it as "names no crate"
|
|
353
|
+
* cost every arm its record, which is what made 29 files of a real Rust
|
|
354
|
+
* repository report dependencies nothing could see (`scripts/coverage-real-trees.mjs`
|
|
355
|
+
* pins the count that found it).
|
|
356
|
+
*
|
|
357
|
+
* Splitting is done by hand rather than by a regex because commas nest:
|
|
358
|
+
* `{a::{b, c}, d::e}` has two top-level arms, and a comma-split would produce
|
|
359
|
+
* three. Depth is counted over `{}` only — a `use` path holds no other
|
|
360
|
+
* bracket.
|
|
361
|
+
*
|
|
362
|
+
* Returns `null` for a group whose braces do not balance, which keeps the
|
|
363
|
+
* loud path for text that is not a well-formed group: a half-read group is
|
|
364
|
+
* exactly the guess this analyzer refuses.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} path The `use` path, from the first non-space to the `;`.
|
|
367
|
+
* @returns {{text: string, offset: number}[] | null}
|
|
368
|
+
*/
|
|
369
|
+
export function braceGroupArms(path) {
|
|
370
|
+
const open = path.indexOf("{");
|
|
371
|
+
if (open === -1 || path.slice(0, open).trim() !== "") return null;
|
|
372
|
+
/** @type {{text: string, offset: number}[]} */
|
|
373
|
+
const arms = [];
|
|
374
|
+
let depth = 0;
|
|
375
|
+
let start = -1;
|
|
376
|
+
for (let index = open; index < path.length; index += 1) {
|
|
377
|
+
const character = path[index];
|
|
378
|
+
if (character === "{") {
|
|
379
|
+
depth += 1;
|
|
380
|
+
if (depth === 1) start = index + 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (character === "}") {
|
|
384
|
+
depth -= 1;
|
|
385
|
+
if (depth === 0) {
|
|
386
|
+
arms.push({ text: path.slice(start, index), offset: start });
|
|
387
|
+
// Anything after the group's close is not a path this reader knows how
|
|
388
|
+
// to split — `use {a}::b;` is not Rust — so it is left to the caller's
|
|
389
|
+
// loud path rather than half-read.
|
|
390
|
+
if (path.slice(index + 1).trim() !== "") return null;
|
|
391
|
+
start = -1;
|
|
392
|
+
}
|
|
393
|
+
if (depth < 0) return null;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (character === "," && depth === 1) {
|
|
397
|
+
arms.push({ text: path.slice(start, index), offset: start });
|
|
398
|
+
start = index + 1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (depth !== 0 || start !== -1) return null;
|
|
402
|
+
return arms
|
|
403
|
+
.map((arm) => {
|
|
404
|
+
// Each arm carries its own leading whitespace, so the offset moves with
|
|
405
|
+
// the trim: a record's column must point at the arm, not at the comma
|
|
406
|
+
// before it.
|
|
407
|
+
const lead = arm.text.length - arm.text.trimStart().length;
|
|
408
|
+
return { text: arm.text.trim().replace(/\s+/gu, " "), offset: arm.offset + lead };
|
|
409
|
+
})
|
|
410
|
+
.filter((arm) => arm.text !== "");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The crate segment a `use` path starts with, or `null` when the path opens
|
|
415
|
+
* with a brace group and names none.
|
|
416
|
+
*/
|
|
417
|
+
export function useRootSegment(path) {
|
|
418
|
+
const match = /^\s*(?:::\s*)?([A-Za-z_]\w*)/.exec(path);
|
|
419
|
+
return match ? match[1] : null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Every source-level crate reference in a `.rs` file, in source order.
|
|
424
|
+
*
|
|
425
|
+
* Four forms, matched separately and then merged: a `use` declaration, an
|
|
426
|
+
* `extern crate`, a fully-qualified `::crate::` path, and — only for crates
|
|
427
|
+
* named in `knownCrates` — a bare `crate_name::item` path written inline with
|
|
428
|
+
* no `use` at all. The forms overlap (`use ::serde::de;` is two of them), so a
|
|
429
|
+
* match inside a range already claimed by a `use` or `extern crate` is dropped
|
|
430
|
+
* rather than recorded twice.
|
|
431
|
+
*
|
|
432
|
+
* **Why the bare form needs the crate list.** Since Rust 2018 a crate can be
|
|
433
|
+
* used with no `use` line anywhere — this workspace's own `main.rs` is exactly
|
|
434
|
+
* that, calling `rba_desktop_lib::run()` and importing nothing. But a bare
|
|
435
|
+
* `Name::item` is far more often a type (`String::from`), an enum
|
|
436
|
+
* (`Ordering::Less`), or a local module, and telling those apart needs the
|
|
437
|
+
* resolver this file exists without. Matching only against crate names the
|
|
438
|
+
* workspace actually declares is what makes the form decidable: the crossing
|
|
439
|
+
* that matters is caught, and no identifier is guessed at. A crate outside the
|
|
440
|
+
* workspace referenced only by a bare path is therefore not recorded here —
|
|
441
|
+
* `resolveRustDependencies` above still reads it from `Cargo.toml`.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} rustText
|
|
444
|
+
* @param {Set<string>} [knownCrates] Crate identifiers the workspace declares.
|
|
445
|
+
* @returns {{ specifier: string, root: string|null, kind: string, offset: number }[]}
|
|
446
|
+
*/
|
|
447
|
+
export function parseRustUseSites(rustText, knownCrates = new Set()) {
|
|
448
|
+
// A UTF-8 BOM is blanked, not stripped (see the header's byte-tolerance
|
|
449
|
+
// bullet): same length, so every offset below stays an offset into the
|
|
450
|
+
// original, and `^`-anchored forms see a line that starts like any other.
|
|
451
|
+
const source = rustText.replace(/^\uFEFF/, " ");
|
|
452
|
+
const sites = [];
|
|
453
|
+
const claimed = [];
|
|
454
|
+
|
|
455
|
+
// `use` opens at a line start, after a same-line `;`/`{`/`}` statement
|
|
456
|
+
// boundary, or after a same-line `#[…]` attribute block — every position
|
|
457
|
+
// Rust allows one. `[{;}]` inside a string or comment can open a spurious
|
|
458
|
+
// match (text the file really contains), never a missed one; the `#[…]`
|
|
459
|
+
// token's content may itself hold a quoted string containing `]`
|
|
460
|
+
// (`#[doc = "see [x]"]`), so it is read past a balanced quoted string
|
|
461
|
+
// rather than stopping at the first `]`. The two alternatives inside that
|
|
462
|
+
// repeated group — a quoted string, or a single non-`]` character — are
|
|
463
|
+
// kept DISJOINT on `"` (the fallback is `[^"\]]`, not `[^\]]`): letting
|
|
464
|
+
// both branches match `"` gives the regex engine two ways to reach the
|
|
465
|
+
// same position for every quote character, and a `.rs` file with many `"`
|
|
466
|
+
// and no closing `]` then backtracks exponentially over that ambiguity
|
|
467
|
+
// (measured: ~285ms at 30 quote characters, seconds at ~50, `.rs` content
|
|
468
|
+
// is attacker-supplied per SECURITY.md). Excluding `"` from the fallback
|
|
469
|
+
// makes the split unambiguous — linear in input length — while still
|
|
470
|
+
// reading the same attribute text: `"` can now only ever be consumed by
|
|
471
|
+
// starting the string branch.
|
|
472
|
+
//
|
|
473
|
+
// The pattern stops at the `use`'s whitespace; the path and its terminating
|
|
474
|
+
// `;` are taken by `indexOf` below rather than by a `([^;]*)(?=;)` tail,
|
|
475
|
+
// and that is the SAME defect class as the one above rather than a
|
|
476
|
+
// different one. `[^;]*` cannot cross a `;`, so it is cheap while one is
|
|
477
|
+
// coming — but a `.rs` file with no `;` after a `use` makes it run to
|
|
478
|
+
// end-of-file and then backtrack one character at a time testing the
|
|
479
|
+
// lookahead, once per `use` start, which is quadratic in file size
|
|
480
|
+
// (measured on the pre-fix pattern: 22KB→16ms, 44KB→65ms, 89KB→256ms,
|
|
481
|
+
// 179KB→1141ms — four times the time for twice the bytes; one crafted 1MB
|
|
482
|
+
// file cost 17.9 SECONDS). `indexOf` finds the same terminator with no
|
|
483
|
+
// backtracking, and the scan windows of successive matches do not overlap,
|
|
484
|
+
// so the whole pass stays linear.
|
|
485
|
+
const usePattern =
|
|
486
|
+
/(?:^|[{;}])[ \t]*(?:(?:#\[(?:"(?:[^"\\]|\\.)*"|[^"\]])*\][ \t]*)+)?(pub(?:\s*\([^)]*\))?[ \t]+)?use[ \t\r\n]+/gm;
|
|
487
|
+
for (let m = usePattern.exec(source); m !== null; m = usePattern.exec(source)) {
|
|
488
|
+
const pathOffset = m.index + m[0].length;
|
|
489
|
+
const terminator = source.indexOf(";", pathOffset);
|
|
490
|
+
// No `;` anywhere after this `use` means no later `use` can be terminated
|
|
491
|
+
// either — every later candidate starts further along the same text — so
|
|
492
|
+
// the old pattern's remaining attempts were all going to fail too. It is
|
|
493
|
+
// the same verdict (no site), reached without re-scanning the tail once
|
|
494
|
+
// per candidate.
|
|
495
|
+
if (terminator === -1) break;
|
|
496
|
+
// Everything between the `use`'s whitespace and that `;`, which is what
|
|
497
|
+
// `[^;]*` matched: the terminator is the first `;`, so no `;` is inside.
|
|
498
|
+
const path = source.slice(pathOffset, terminator);
|
|
499
|
+
// Resume exactly where the lookahead used to leave `matchAll`: AT the
|
|
500
|
+
// `;`, never past it, so it is still there — unclaimed — for a second
|
|
501
|
+
// `use` sharing the same line to open its own match against.
|
|
502
|
+
usePattern.lastIndex = terminator;
|
|
503
|
+
const lead = path.length - path.trimStart().length;
|
|
504
|
+
// A use path may wrap across lines inside a brace group. The record is
|
|
505
|
+
// printed in `file:line:column: specifier` reports, so line breaks are
|
|
506
|
+
// collapsed — Rust has no single-token module specifier to keep verbatim.
|
|
507
|
+
const specifier = path.trim().replace(/\s+/g, " ");
|
|
508
|
+
const kind = m[1] ? "re-export" : "static";
|
|
509
|
+
// A path opening with a brace group is a LIST of paths, and each arm names
|
|
510
|
+
// its own crate — see `braceGroupArms`. One site per arm, at the arm's own
|
|
511
|
+
// position, so a report sends a reader to the import they have to change.
|
|
512
|
+
// `null` back from the splitter means the text is not a well-formed group,
|
|
513
|
+
// and the single `root: null` site below keeps that loud.
|
|
514
|
+
const arms = braceGroupArms(path);
|
|
515
|
+
if (arms !== null) {
|
|
516
|
+
for (const arm of arms) {
|
|
517
|
+
sites.push({
|
|
518
|
+
specifier: arm.text,
|
|
519
|
+
root: useRootSegment(arm.text),
|
|
520
|
+
kind,
|
|
521
|
+
offset: pathOffset + arm.offset,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
claimed.push([m.index, terminator]);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
sites.push({
|
|
528
|
+
specifier,
|
|
529
|
+
root: useRootSegment(path),
|
|
530
|
+
kind,
|
|
531
|
+
offset: pathOffset + lead,
|
|
532
|
+
});
|
|
533
|
+
claimed.push([m.index, terminator]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const m of source.matchAll(/^[ \t]*(?:pub[ \t]+)?extern[ \t]+crate[ \t]+([A-Za-z_]\w*)/gm)) {
|
|
537
|
+
sites.push({
|
|
538
|
+
specifier: m[1],
|
|
539
|
+
root: m[1],
|
|
540
|
+
kind: "static",
|
|
541
|
+
offset: m.index + m[0].lastIndexOf(m[1]),
|
|
542
|
+
});
|
|
543
|
+
claimed.push([m.index, m.index + m[0].length]);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Is `offset` outside every range a `use` or an `extern crate` already
|
|
547
|
+
// claimed? Answered by binary search over the ranges sorted by start, not by
|
|
548
|
+
// testing all of them: a `.rs` file pairing N `use` statements with N
|
|
549
|
+
// fully-qualified `::crate::` calls — ordinary Rust, not a crafted file —
|
|
550
|
+
// otherwise pays N*N range tests, measured at 11.4ms for 55KB, 32ms for
|
|
551
|
+
// 110KB and 123ms for 220KB, the same four-times-for-twice-the-bytes shape
|
|
552
|
+
// the `use` scan above was fixed for.
|
|
553
|
+
//
|
|
554
|
+
// The two passes' ranges are each ascending and disjoint, but a range from
|
|
555
|
+
// one CAN contain a range from the other (an `extern crate` line sitting
|
|
556
|
+
// inside a `use` group that wraps across lines), so containment is decided
|
|
557
|
+
// against a running maximum of the ends rather than against the end of the
|
|
558
|
+
// last range that starts early enough. That is exactly what the `some()`
|
|
559
|
+
// asked: some range starts at or before the offset AND reaches past it.
|
|
560
|
+
claimed.sort((a, b) => a[0] - b[0]);
|
|
561
|
+
/** `reach[i]` — the furthest end among `claimed[0..i]`. */
|
|
562
|
+
const reach = [];
|
|
563
|
+
for (let i = 0; i < claimed.length; i++) {
|
|
564
|
+
reach.push(i === 0 ? claimed[i][1] : Math.max(reach[i - 1], claimed[i][1]));
|
|
565
|
+
}
|
|
566
|
+
const unclaimed = (offset) => {
|
|
567
|
+
let low = 0;
|
|
568
|
+
let high = claimed.length - 1;
|
|
569
|
+
let last = -1; // the last range starting at or before `offset`
|
|
570
|
+
while (low <= high) {
|
|
571
|
+
const mid = (low + high) >> 1;
|
|
572
|
+
if (claimed[mid][0] <= offset) {
|
|
573
|
+
last = mid;
|
|
574
|
+
low = mid + 1;
|
|
575
|
+
} else {
|
|
576
|
+
high = mid - 1;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return last === -1 || reach[last] <= offset;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
for (const m of source.matchAll(/(^|[^:\w])::([A-Za-z_]\w*)::/gm)) {
|
|
583
|
+
const offset = m.index + m[1].length;
|
|
584
|
+
if (!unclaimed(offset)) continue;
|
|
585
|
+
sites.push({ specifier: `::${m[2]}::`, root: m[2], kind: "static", offset });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (knownCrates.size > 0) {
|
|
589
|
+
for (const m of source.matchAll(/(^|[^:\w.])([A-Za-z_]\w*)::/gm)) {
|
|
590
|
+
const offset = m.index + m[1].length;
|
|
591
|
+
if (!knownCrates.has(crateIdentifier(m[2])) || !unclaimed(offset)) continue;
|
|
592
|
+
sites.push({ specifier: `${m[2]}::`, root: m[2], kind: "static", offset });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return sites.sort((a, b) => a.offset - b.offset);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Analyzes one `.rs` file.
|
|
601
|
+
*
|
|
602
|
+
* `file` is always `null`: a Rust path names an item inside a crate, and which
|
|
603
|
+
* `.rs` file defines it is a question only the compiler's module tree can
|
|
604
|
+
* answer. `crate::`/`self::`/`super::` resolve to the file's own project —
|
|
605
|
+
* intra-project imports are recorded, not dropped (`contract.md`) — and
|
|
606
|
+
* `isOwnProjectPath` above states which spellings reach the file's own project
|
|
607
|
+
* without leaving it, which is the fact the self-circular rule reads.
|
|
608
|
+
*
|
|
609
|
+
* @param {{ sourceFile: string, text: string, workspace: object }} request
|
|
610
|
+
* @returns {{ imports: object[], failures: object[] }}
|
|
611
|
+
*/
|
|
612
|
+
export function analyzeRust({ sourceFile, text, workspace }) {
|
|
613
|
+
const result = emptyResult();
|
|
614
|
+
try {
|
|
615
|
+
const byCrate = crateNamesOf(workspace);
|
|
616
|
+
const owner = projectOwning(workspace.projects, sourceFile);
|
|
617
|
+
// A rename is legible only inside the project whose OWN manifest declares
|
|
618
|
+
// it — see the header's "A renamed dependency IS followed".
|
|
619
|
+
const ownAliases = owner ? renamedDepsOf(workspace).get(owner.name) : undefined;
|
|
620
|
+
const knownCrates = ownAliases
|
|
621
|
+
? new Set([...byCrate.keys(), ...ownAliases.keys()])
|
|
622
|
+
: new Set(byCrate.keys());
|
|
623
|
+
|
|
624
|
+
// One line-start index for the whole file, built here and handed to every
|
|
625
|
+
// site: a `.rs` file with thousands of `use` statements otherwise pays a
|
|
626
|
+
// rescan of the file per site (`source-util.mjs`'s `lineStartsOf`).
|
|
627
|
+
const lineStarts = lineStartsOf(text);
|
|
628
|
+
for (const site of parseRustUseSites(text, knownCrates)) {
|
|
629
|
+
const { line, column } = positionAt(text, site.offset, lineStarts);
|
|
630
|
+
let resolved = null;
|
|
631
|
+
if (site.root === null) {
|
|
632
|
+
result.failures.push({
|
|
633
|
+
sourceFile,
|
|
634
|
+
line,
|
|
635
|
+
column,
|
|
636
|
+
reason: `'use ${site.specifier}' opens with a brace group, so it names no crate to resolve`,
|
|
637
|
+
});
|
|
638
|
+
} else if (OWN_CRATE_ROOTS.has(site.root)) {
|
|
639
|
+
resolved = {
|
|
640
|
+
target: owner?.name ?? null,
|
|
641
|
+
file: null,
|
|
642
|
+
external: owner === null,
|
|
643
|
+
packageName: null,
|
|
644
|
+
};
|
|
645
|
+
} else {
|
|
646
|
+
const identifier = crateIdentifier(site.root);
|
|
647
|
+
const target = byCrate.get(identifier) ?? ownAliases?.get(identifier) ?? null;
|
|
648
|
+
resolved = {
|
|
649
|
+
target,
|
|
650
|
+
file: null,
|
|
651
|
+
external: target === null,
|
|
652
|
+
// The identifier spelling, which is the only one a source states.
|
|
653
|
+
// A Cargo package name that hyphenates cannot be recovered from it,
|
|
654
|
+
// so a `bannedExternalImports` glob is written against this form.
|
|
655
|
+
packageName: target === null ? site.root : null,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
result.imports.push({
|
|
659
|
+
sourceFile,
|
|
660
|
+
line,
|
|
661
|
+
column,
|
|
662
|
+
specifier: site.specifier,
|
|
663
|
+
kind: site.kind,
|
|
664
|
+
spelling: { path: false, relative: isOwnProjectPath(site.root, owner, byCrate) },
|
|
665
|
+
resolved,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} catch (cause) {
|
|
669
|
+
result.failures.push(
|
|
670
|
+
fileFailure(sourceFile, `Rust analysis failed: ${cause?.message ?? cause}`),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|