@exodus/stasis 1.0.0-beta.3 → 1.0.0-beta.6
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 +9 -2
- package/bin/stasis.js +253 -22
- package/package.json +42 -10
- package/src/audit-corrections.js +72 -0
- package/src/audit.js +181 -112
- package/src/bundle-from-lockfile.js +3 -5
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +3 -5
- package/src/cmd/bundle.js +677 -402
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +3 -11
- package/src/diff.js +22 -14
- package/src/json-stream.js +283 -0
- package/src/loaders/bash.js +17 -36
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +212 -127
- package/src/loaders/rust.js +3234 -166
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -137
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +9 -24
- package/src/mock.js +3 -5
- package/src/resolve-fields.js +338 -0
- package/src/resolve-node.js +416 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +35 -19
- package/src/scan.js +211 -90
- package/src/vfs-bundle/entries.js +140 -0
- package/src/vfs-bundle/github.js +199 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +613 -0
- package/src/vfs-bundle.js +75 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -42
package/src/loaders/rust.js
CHANGED
|
@@ -1,223 +1,3204 @@
|
|
|
1
|
-
// Rust loader: `{ sources, resolutions, missing }` from a crate's files.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// module
|
|
1
|
+
// Rust loader: `{ sources, resolutions, missing }` from a crate's files.
|
|
2
|
+
//
|
|
3
|
+
// Reachability follows what rustc follows. A `mod foo;` declaration (incl. `#[path = …]` and
|
|
4
|
+
// `#[cfg_attr(…, path = …)]`) pulls a module file in, and a reference to a crate whose source is
|
|
5
|
+
// in-tree -- the package's own lib target, a Cargo `path` dependency, or a `cargo vendor`ed crate
|
|
6
|
+
// -- pulls that crate's root in. `crate::` / `self::` / `super::` / relative paths resolve against
|
|
7
|
+
// the per-crate module tree for the import graph but never widen the walk: a `use` can't add a
|
|
8
|
+
// file to a crate, only `mod` can. Registry dependencies that aren't vendored live outside the
|
|
9
|
+
// bundle root and are dropped.
|
|
5
10
|
|
|
6
|
-
import {
|
|
11
|
+
import { isUtf8 } from 'node:buffer'
|
|
12
|
+
import { realpathSync } from 'node:fs'
|
|
7
13
|
import { readFile } from 'node:fs/promises'
|
|
8
|
-
import { dirname, extname,
|
|
14
|
+
import { dirname, extname, join, posix, relative, resolve } from 'node:path'
|
|
9
15
|
|
|
10
|
-
import { assertRealPathWithinBase } from '@exodus/stasis-core/util'
|
|
16
|
+
import { assertRealPathWithinBase, relativeEscapes, toPosix } from '@exodus/stasis-core/util'
|
|
17
|
+
import { isFile } from '../resolve-typescript.js'
|
|
18
|
+
import { TARGET_CFG_KEYS, TARGET_UNIT, VENDOR_DIR, cached, cfgName, createCargoContext, evalCfg, evalCfgKey, isTestTargetPath, normName, normalizeCfg, normalizeRel } from './cargo.js'
|
|
19
|
+
import { assertWithinBase } from './paths.js'
|
|
20
|
+
import { matchClose, splitTopLevel } from './toml.js'
|
|
11
21
|
|
|
12
|
-
//
|
|
13
|
-
const
|
|
22
|
+
// Leads of the expression-position paths anchored on the module tree rather than on a name.
|
|
23
|
+
const PATH_KEYWORDS = new Set(['crate', 'self', 'super'])
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
// Path keywords, and the sysroot crates every build links: a path whose lead is one of these
|
|
26
|
+
// never names an in-tree crate.
|
|
27
|
+
const NON_CRATE_LEADS = new Set([...PATH_KEYWORDS, 'std', 'core', 'alloc'])
|
|
16
28
|
|
|
17
|
-
//
|
|
18
|
-
|
|
29
|
+
// The other sysroot crates a program may name (`extern crate proc_macro;`, `test::Bencher`):
|
|
30
|
+
// never in-tree, so never worth reporting as unresolved.
|
|
31
|
+
const OTHER_SYSROOT_CRATES = new Set(['proc_macro', 'test'])
|
|
19
32
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
const EXTERN_CRATE_RE = /^(?:pub\s+)?extern\s+crate\s+(\w+)\s*;/u
|
|
23
|
-
const PATH_LEAD_RE = /\b([a-z_]\w*)(?:::\w+)+/gu
|
|
24
|
-
const NON_CRATE_LEADS = new Set(['crate', 'self', 'super', 'std', 'core', 'alloc'])
|
|
33
|
+
// Files rustc treats as crate roots by name; every entry is one by role (see crateRoots).
|
|
34
|
+
const ROOT_NAMES = new Set(['main.rs', 'lib.rs'])
|
|
25
35
|
|
|
26
|
-
//
|
|
27
|
-
|
|
36
|
+
// Cap on `mod`-chain depth when building a module tree: with the `seen` set it bounds cycles
|
|
37
|
+
// and the O(depth²) growth of joined module-path strings on absurd input.
|
|
38
|
+
const MAX_MODULE_DEPTH = 1000
|
|
39
|
+
|
|
40
|
+
const baseName = (p) => (p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p)
|
|
41
|
+
const isNamedRoot = (p) => ROOT_NAMES.has(baseName(p))
|
|
42
|
+
const isWordChar = (ch) => ch !== undefined && /\w/u.test(ch)
|
|
43
|
+
|
|
44
|
+
// --- Lexing -----------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
// Single left-to-right pass over Rust source producing two same-length views (newlines kept, so
|
|
47
|
+
// lines and offsets stay aligned with the input): `code` blanks comments only; `masked` blanks
|
|
48
|
+
// comments AND the contents of string/char literals. Block comments nest; a `//` inside a string
|
|
49
|
+
// is not a comment and a `/*` inside a `//` comment opens nothing -- rules a pair of regexes gets
|
|
50
|
+
// wrong in both directions (a commented-out `mod` taken for a real one, or real ones swallowed).
|
|
51
|
+
export function lexRust(content) {
|
|
52
|
+
const n = content.length
|
|
53
|
+
// Each view is assembled from chunks: the source up to a blanked range, then the range with
|
|
54
|
+
// every line replaced by as many spaces as it has UTF-16 units (a char may be two), so indexes
|
|
55
|
+
// stay those of `content`.
|
|
56
|
+
const code = []
|
|
57
|
+
const masked = []
|
|
58
|
+
let codeAt = 0
|
|
59
|
+
let maskedAt = 0
|
|
60
|
+
const blanks = (start, end) => content.slice(start, end).split('\n').map((line) => ' '.repeat(line.length)).join('\n')
|
|
61
|
+
const blankMasked = (start, end) => {
|
|
62
|
+
masked.push(content.slice(maskedAt, start), blanks(start, end))
|
|
63
|
+
maskedAt = end
|
|
64
|
+
}
|
|
65
|
+
const blankBoth = (start, end) => {
|
|
66
|
+
code.push(content.slice(codeAt, start), blanks(start, end))
|
|
67
|
+
codeAt = end
|
|
68
|
+
blankMasked(start, end)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let i = 0
|
|
72
|
+
while (i < n) {
|
|
73
|
+
const ch = content[i]
|
|
74
|
+
if (ch === '/' && content[i + 1] === '/') {
|
|
75
|
+
const eol = content.indexOf('\n', i)
|
|
76
|
+
const end = eol === -1 ? n : eol
|
|
77
|
+
blankBoth(i, end)
|
|
78
|
+
i = end
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (ch === '/' && content[i + 1] === '*') {
|
|
82
|
+
const start = i
|
|
83
|
+
let depth = 0
|
|
84
|
+
do {
|
|
85
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
86
|
+
depth++
|
|
87
|
+
i += 2
|
|
88
|
+
} else if (content[i] === '*' && content[i + 1] === '/') {
|
|
89
|
+
depth--
|
|
90
|
+
i += 2
|
|
91
|
+
} else {
|
|
92
|
+
i++
|
|
93
|
+
}
|
|
94
|
+
} while (i < n && depth > 0)
|
|
95
|
+
blankBoth(start, Math.min(i, n))
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const lit = stringStart(content, i)
|
|
99
|
+
if (lit) {
|
|
100
|
+
const start = lit.open + 1 // past the `b`/`c`/`r#` prefix and the opening quote
|
|
101
|
+
if (lit.raw === null) {
|
|
102
|
+
i = start
|
|
103
|
+
while (i < n && content[i] !== '"') i += content[i] === '\\' ? 2 : 1 // an escape takes two
|
|
104
|
+
blankMasked(start, Math.min(i, n))
|
|
105
|
+
i++ // closing quote
|
|
106
|
+
} else {
|
|
107
|
+
const close = `"${'#'.repeat(lit.raw)}`
|
|
108
|
+
let end = content.indexOf(close, start)
|
|
109
|
+
if (end === -1) end = n
|
|
110
|
+
blankMasked(start, end)
|
|
111
|
+
i = end + close.length
|
|
112
|
+
}
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if (ch === "'") {
|
|
116
|
+
const end = charLiteralEnd(content, i)
|
|
117
|
+
if (end !== -1) {
|
|
118
|
+
blankMasked(i + 1, end)
|
|
119
|
+
i = end + 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
i++
|
|
124
|
+
}
|
|
125
|
+
code.push(content.slice(codeAt))
|
|
126
|
+
masked.push(content.slice(maskedAt))
|
|
127
|
+
return { code: code.join(''), masked: masked.join('') }
|
|
128
|
+
}
|
|
28
129
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
130
|
+
// If a string literal starts at `i` -- counting a `b`/`c` byte/C-string prefix and the raw
|
|
131
|
+
// marker `r`/`r#…#` -- return `{ open, raw }` where `open` indexes the opening quote and `raw` is
|
|
132
|
+
// a raw string's `#` count (null for a plain one); else null. A prefix must not be the tail of
|
|
133
|
+
// an identifier (`bar"` is not a prefix).
|
|
134
|
+
function stringStart(content, i) {
|
|
135
|
+
if (content[i] === '"') return { open: i, raw: null }
|
|
136
|
+
let j = i
|
|
137
|
+
if ((content[j] === 'b' || content[j] === 'c') && (content[j + 1] === '"' || content[j + 1] === 'r')) j++
|
|
138
|
+
if (content[j] !== 'r' && content[j] !== '"') return null
|
|
139
|
+
if (isWordChar(content[i - 1])) return null
|
|
140
|
+
if (content[j] === '"') return { open: j, raw: null }
|
|
141
|
+
let k = j + 1
|
|
142
|
+
while (content[k] === '#') k++
|
|
143
|
+
return content[k] === '"' ? { open: k, raw: k - j - 1 } : null
|
|
35
144
|
}
|
|
36
145
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cfgPending = false
|
|
49
|
-
} else if (CFG_ATTR_RE.test(line)) {
|
|
50
|
-
cfgPending = true
|
|
51
|
-
} else if (!line.startsWith('#[')) {
|
|
52
|
-
cfgPending = false // a non-attribute line ends the attribute group
|
|
146
|
+
// Index of the quote closing a char literal opening at `i` (`'x'`, `'\n'`, `'\u{1F600}'`), or -1
|
|
147
|
+
// when the quote starts a lifetime/label (`'a`), which has no closing quote.
|
|
148
|
+
function charLiteralEnd(content, i) {
|
|
149
|
+
if (content[i + 1] === '\\') {
|
|
150
|
+
let j = i + 2
|
|
151
|
+
if (content[j] === 'u' && content[j + 1] === '{') {
|
|
152
|
+
const close = content.indexOf('}', j)
|
|
153
|
+
if (close === -1 || close - j > 10) return -1
|
|
154
|
+
j = close + 1
|
|
155
|
+
} else {
|
|
156
|
+
j += 1
|
|
53
157
|
}
|
|
158
|
+
return content[j] === "'" ? j : -1
|
|
54
159
|
}
|
|
55
|
-
return
|
|
160
|
+
if (content[i + 1] === undefined || content[i + 1] === "'") return -1
|
|
161
|
+
if (content[i + 2] === "'") return i + 2
|
|
162
|
+
// An astral char is two UTF-16 units.
|
|
163
|
+
const cp = content.codePointAt(i + 1)
|
|
164
|
+
return cp > 0xff_ff && content[i + 3] === "'" ? i + 3 : -1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Item scanning ----------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
const WORD_RE = /[A-Za-z_]\w*/uy
|
|
170
|
+
const WS_RE = /\s/u
|
|
171
|
+
// First word of an item or statement that runs to its `;` or `{ … }` body, whatever commas its
|
|
172
|
+
// generics, arguments or `where` clause hold; anything else (a field, a variant, a match arm)
|
|
173
|
+
// ends at a top-level comma. Weak keywords a field may be named after (`default`, `union`) are
|
|
174
|
+
// left out on purpose.
|
|
175
|
+
const ITEM_KEYWORDS = new Set(['fn', 'impl', 'struct', 'enum', 'trait', 'type', 'const', 'static', 'mod', 'use', 'extern', 'macro_rules', 'unsafe', 'async', 'let'])
|
|
176
|
+
// Macros whose literal string argument names a file relative to the invoking one: Rust source
|
|
177
|
+
// spliced in (`include!`), or an asset embedded as text / bytes. Only a literal can be followed;
|
|
178
|
+
// `concat!(env!("OUT_DIR"), …)` is build output.
|
|
179
|
+
const INCLUDE_MACROS = new Set(['include', 'include_str', 'include_bytes'])
|
|
180
|
+
// Macros whose body is a template of code for some other crate (a proc macro's output): nothing
|
|
181
|
+
// in it is an item, import or path of the crate holding it.
|
|
182
|
+
const TEMPLATE_MACROS = new Set(['quote', 'quote_spanned', 'parse_quote', 'parse_quote_spanned'])
|
|
183
|
+
// An include macro's argument: a string literal (plain, with escapes, or raw with any number of
|
|
184
|
+
// `#`s), or `concat!(env!("CARGO_MANIFEST_DIR"), "<literal>")`, a path from the package root.
|
|
185
|
+
const STRING_LITERAL_RE = /^\s*(?:"((?:[^"\\]|\\[\s\S])*)"|r(#*)"([\s\S]*?)"\2)\s*$/u
|
|
186
|
+
const MANIFEST_DIR_CONCAT_RE = /^\s*concat!\s*\(\s*env!\s*\(\s*"CARGO_MANIFEST_DIR"\s*\)\s*,\s*([\s\S]*?),?\s*\)\s*$/u
|
|
187
|
+
// An include macro opening inside an attribute's text: `#![doc = include_str!("../README.md")]`.
|
|
188
|
+
const INCLUDE_IN_ATTR_RE = /\b(include|include_str|include_bytes)!\s*(?=[([{])/gu
|
|
189
|
+
// Items a module defines by name, for `pub struct X` behind a glob; `mod`, `use`, `extern crate`
|
|
190
|
+
// and `macro_rules!` have their own records.
|
|
191
|
+
const DEFINING_KEYWORDS = new Set(['struct', 'enum', 'union', 'trait', 'type', 'fn', 'const', 'static'])
|
|
192
|
+
// Of those, the items in the value namespace: never a path's lead or prefix (a `fn log` beside
|
|
193
|
+
// `use log::info` hides no crate).
|
|
194
|
+
const VALUE_KEYWORDS = new Set(['fn', 'const', 'static'])
|
|
195
|
+
|
|
196
|
+
// The path an include macro's argument names: `{ path, base }` (`base`: `file` for a path relative
|
|
197
|
+
// to the invoking file, `manifest` for one from the package root), or null when it isn't one the
|
|
198
|
+
// loader can follow (`concat!(env!("OUT_DIR"), …)` is build output).
|
|
199
|
+
function includeArg(text) {
|
|
200
|
+
const manifest = MANIFEST_DIR_CONCAT_RE.exec(text)
|
|
201
|
+
const lit = STRING_LITERAL_RE.exec(manifest ? manifest[1] : text)
|
|
202
|
+
if (!lit) return null
|
|
203
|
+
const path = lit[1] === undefined ? lit[3] : unescapeString(lit[1])
|
|
204
|
+
return manifest ? { path: path.replace(/^\/+/u, ''), base: 'manifest' } : { path, base: 'file' }
|
|
56
205
|
}
|
|
57
206
|
|
|
58
|
-
|
|
59
|
-
|
|
207
|
+
// The value of a plain string literal's body: `\\`, `\"`, `\'`, `\n`, `\r`, `\t`, `\0`,
|
|
208
|
+
// `\x41`, `\u{1F600}` and a backslash-newline continuation.
|
|
209
|
+
function unescapeString(body) {
|
|
210
|
+
return body.replaceAll(/\\(?:u\{([0-9a-fA-F]{1,6})\}|x([0-9a-fA-F]{2})|\n\s*|(.))/gsu, (m, u, x, c) => {
|
|
211
|
+
if (u !== undefined) return String.fromCodePoint(Number.parseInt(u, 16))
|
|
212
|
+
if (x !== undefined) return String.fromCharCode(Number.parseInt(x, 16))
|
|
213
|
+
if (c === undefined) return ''
|
|
214
|
+
return { n: '\n', r: '\r', t: '\t', 0: '\0' }[c] ?? c
|
|
215
|
+
})
|
|
60
216
|
}
|
|
217
|
+
const EXTERN_CRATE_RE = /extern\s+crate\s+(?:r#)?(\w+)(?:\s+as\s+(?:r#)?(\w+))?\s*;/uy
|
|
218
|
+
const MACRO_EXPORT_RE = /^\s*macro_export\s*(?:\([\s\S]*\))?\s*$/u
|
|
219
|
+
// Expression-position paths: any `lead::…` path whose lead is lowercase (skipping `Type::assoc`
|
|
220
|
+
// associated-item paths) -- a `crate`/`self`/`super` keyword path, or one led by a module or
|
|
221
|
+
// crate name. Runs over the masked view, so string contents can't fake one.
|
|
222
|
+
const LEAD_PATH_RE = /\b([a-z_]\w*)(?:::\w+)+/gu
|
|
223
|
+
const ATTR_PATH_RE = /^\s*path\s*=\s*"([^"]*)"\s*$/u
|
|
61
224
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
225
|
+
// Combine an item's cfg predicates (several `#[cfg]` attributes all apply) into one; null when ungated.
|
|
226
|
+
const joinCfgs = (preds) => (preds.length === 0 ? null : (preds.length === 1 ? preds[0] : `all(${preds.join(', ')})`))
|
|
227
|
+
|
|
228
|
+
const CFG_ATTR_RE = /^\s*cfg\s*\(([\s\S]*)\)\s*$/u
|
|
229
|
+
|
|
230
|
+
// One outer attribute's text (inside `#[…]`) → `cfg`: the predicate gating the item (`#[cfg(<pred>)]`;
|
|
231
|
+
// `test` for a `#[test]` fn; for `#[cfg_attr(<pred>, cfg(<inner>))]` the item is compiled unless
|
|
232
|
+
// pred holds and inner doesn't, i.e. `any(not(pred), inner)`; null when the item is unconditional
|
|
233
|
+
// -- a `cfg_attr` applying any other attribute, `doc(cfg(…))` included, gates nothing);
|
|
234
|
+
// `paths`: the module file paths it names, `#[path = "…"]` outright (cfg null) or each
|
|
235
|
+
// `#[cfg_attr(<pred>, path = "…")]` under its predicate; `pred`: a `cfg_attr`'s own predicate
|
|
236
|
+
// (null for any other attribute) -- when it can't hold, the whole attribute applies nothing;
|
|
237
|
+
// `macroExport`: true for `#[macro_export]` / `#[macro_export(local_inner_macros)]`, outright or
|
|
238
|
+
// under a `cfg_attr` (the macro then lives at the crate root, as `$crate::name!`); `includes`: the
|
|
239
|
+
// files an include macro in the text names (`#[doc = include_str!("../README.md")]`, or from the
|
|
240
|
+
// package root with `concat!(env!("CARGO_MANIFEST_DIR"), …)`; see includeArg). `masked` is the same
|
|
241
|
+
// text with string contents blanked (lexRust), for matching the macro's brackets.
|
|
242
|
+
function parseAttr(text, masked = text) {
|
|
243
|
+
const includes = []
|
|
244
|
+
for (const m of masked.matchAll(INCLUDE_IN_ATTR_RE)) {
|
|
245
|
+
const open = m.index + m[0].length
|
|
246
|
+
const arg = includeArg(text.slice(open + 1, matchClose(masked, open)))
|
|
247
|
+
if (arg !== null) includes.push({ kind: m[1], ...arg })
|
|
70
248
|
}
|
|
71
|
-
|
|
72
|
-
|
|
249
|
+
if (MACRO_EXPORT_RE.test(text)) return { cfg: null, paths: [], pred: null, includes, macroExport: true }
|
|
250
|
+
if (/^\s*macro_use\s*$/u.test(text)) return { cfg: null, paths: [], pred: null, includes, macroUse: true }
|
|
251
|
+
const path = ATTR_PATH_RE.exec(text)
|
|
252
|
+
if (path) return { cfg: null, paths: [{ path: path[1], cfg: null }], pred: null, includes }
|
|
253
|
+
if (/^\s*test\s*$/u.test(text)) return { cfg: 'test', paths: [], pred: null, includes }
|
|
254
|
+
const cfg = CFG_ATTR_RE.exec(text)
|
|
255
|
+
if (cfg) return { cfg: normalizeCfg(cfg[1]), paths: [], pred: null, includes }
|
|
256
|
+
const cfgAttr = /^\s*cfg_attr\s*\(([\s\S]*)\)\s*$/u.exec(text)
|
|
257
|
+
if (cfgAttr) {
|
|
258
|
+
const parts = splitTopLevel(cfgAttr[1])
|
|
259
|
+
const pred = normalizeCfg(parts.shift() ?? '')
|
|
260
|
+
const paths = []
|
|
261
|
+
const inner = []
|
|
262
|
+
let macroExport = false
|
|
263
|
+
for (const part of parts) {
|
|
264
|
+
const m = ATTR_PATH_RE.exec(part)
|
|
265
|
+
const c = CFG_ATTR_RE.exec(part)
|
|
266
|
+
if (m) paths.push({ path: m[1], cfg: pred })
|
|
267
|
+
else if (c) inner.push(normalizeCfg(c[1]))
|
|
268
|
+
else if (MACRO_EXPORT_RE.test(part)) macroExport = true
|
|
269
|
+
}
|
|
270
|
+
return { cfg: inner.length === 0 ? null : `any(not(${pred}), ${joinCfgs(inner)})`, paths, pred, includes, macroExport }
|
|
73
271
|
}
|
|
74
|
-
return [
|
|
272
|
+
return { cfg: null, paths: [], pred: null, includes }
|
|
75
273
|
}
|
|
76
274
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
275
|
+
// An attribute that applies nothing in this build (a `cfg_attr` whose predicate can't hold).
|
|
276
|
+
const INERT_ATTR = { cfg: null, paths: [], pred: null, includes: [] }
|
|
277
|
+
|
|
278
|
+
// The visibility a `pub(…)` restriction spells, from the text between its parentheses: `crate`,
|
|
279
|
+
// `self`, `super`, or `in <path>` (normalized to `in a::b`); a bare `pub` is `crate` -- the loader
|
|
280
|
+
// never follows a path into another crate's tree, so `pub` and `pub(crate)` are one to it.
|
|
281
|
+
function parseVisibility(restriction) {
|
|
282
|
+
const r = restriction.trim().replaceAll(/\s+/gu, ' ')
|
|
283
|
+
if (r === 'crate' || r === 'self' || r === 'super') return r
|
|
284
|
+
if (r.startsWith('in ')) return `in ${r.slice(3).replaceAll(' ', '')}`
|
|
285
|
+
return 'crate'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Flatten a `use` tree body (the text between `use` and `;`) into the paths it imports, as
|
|
289
|
+
// `{ segments, absolute, spec, binding, glob }`: `a::{b::C, d::{E, F}}` → `a::b::C`, `a::d::E`,
|
|
290
|
+
// `a::d::F`; a glob (`glob` true) or `self` inside a group names the group's own module; a leading
|
|
291
|
+
// `::` marks the path absolute (an external crate). `spec` is the flat path as it would be
|
|
292
|
+
// written; `binding` the name the import brings into scope (its last segment, or the `as` alias;
|
|
293
|
+
// null for a glob or `_`).
|
|
294
|
+
export function parseUseTree(body) {
|
|
295
|
+
const tokens = [...body.matchAll(/::|[{},*]|\bas\b|(?:r#)?\w+/gu)].map((m) => m[0])
|
|
296
|
+
const out = []
|
|
297
|
+
let pos = 0
|
|
298
|
+
const peek = () => tokens[pos]
|
|
299
|
+
const next = () => tokens[pos++]
|
|
300
|
+
const isIdent = (tok) => tok !== undefined && tok !== 'as' && /^(?:r#)?\w+$/u.test(tok)
|
|
301
|
+
const emit = (segments, absolute, binding, glob) => {
|
|
302
|
+
if (segments.length > 0) {
|
|
303
|
+
out.push({ segments, absolute, spec: `${absolute ? '::' : ''}${segments.join('::')}`, binding: binding === '_' ? null : binding, glob })
|
|
89
304
|
}
|
|
90
305
|
}
|
|
91
|
-
|
|
306
|
+
const parse = (prefix, absolute) => {
|
|
307
|
+
let tok = peek()
|
|
308
|
+
if (tok === '::' && prefix.length === 0) {
|
|
309
|
+
next()
|
|
310
|
+
absolute = true
|
|
311
|
+
tok = peek()
|
|
312
|
+
}
|
|
313
|
+
if (tok === '{') {
|
|
314
|
+
next()
|
|
315
|
+
while (pos < tokens.length && peek() !== '}') {
|
|
316
|
+
const before = pos
|
|
317
|
+
parse(prefix, absolute)
|
|
318
|
+
if (peek() === ',') next()
|
|
319
|
+
if (pos === before) next() // malformed: make progress
|
|
320
|
+
}
|
|
321
|
+
next()
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (tok === '*') {
|
|
325
|
+
next()
|
|
326
|
+
emit(prefix, absolute, null, true)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
if (!isIdent(tok)) return
|
|
330
|
+
const segments = [...prefix]
|
|
331
|
+
for (;;) {
|
|
332
|
+
tok = next()
|
|
333
|
+
if (!isIdent(tok)) return
|
|
334
|
+
segments.push(tok.replace(/^r#/u, ''))
|
|
335
|
+
if (peek() !== '::') break
|
|
336
|
+
next()
|
|
337
|
+
if (peek() === '{' || peek() === '*') {
|
|
338
|
+
parse(segments, absolute)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let alias = null
|
|
343
|
+
if (peek() === 'as') {
|
|
344
|
+
next()
|
|
345
|
+
alias = isIdent(peek()) ? next().replace(/^r#/u, '') : null
|
|
346
|
+
}
|
|
347
|
+
// `use a::{self, b}`: `self` names the group's own module.
|
|
348
|
+
if (prefix.length > 0 && segments.length === prefix.length + 1 && segments.at(-1) === 'self') segments.pop()
|
|
349
|
+
emit(segments, absolute, alias ?? segments.at(-1), false)
|
|
350
|
+
}
|
|
351
|
+
parse([], false)
|
|
352
|
+
return out
|
|
92
353
|
}
|
|
93
354
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
355
|
+
// Statically scan one file's items. An item whose cfg can never hold in the build (`#[cfg(test)]`,
|
|
356
|
+
// `#[test]`, `#[cfg(doc)]`, a `#[cfg(feature = "x")]` with `x` off in the crate's resolved
|
|
357
|
+
// `features`, a `#[cfg(windows)]` when `target` -- the build target's cfg set, see evalCfg -- says
|
|
358
|
+
// otherwise) is skipped whole -- an inline `mod tests { … }` with everything in it, a `fn`'s
|
|
359
|
+
// body, a `use` -- so dead modules aren't bundled and the dependencies only dead code reaches for
|
|
360
|
+
// don't get pulled in. Returns
|
|
361
|
+
// mods: external `mod` declarations `{ name, inlinePath, inlineDirs, cfg, conditional,
|
|
362
|
+
// paths, noDefault, vis, macroUse, macro }` -- `inlinePath` is the chain of inline
|
|
363
|
+
// `mod x { … }` blocks it sits in, `inlineDirs` the directories those stand for
|
|
364
|
+
// (each its name, or its own `#[path = "…"]`, which may be empty), `vis` its
|
|
365
|
+
// visibility (see imports), `macroUse` a `#[macro_use]` on it, `macro` the macro
|
|
366
|
+
// invocation it sits in (if any), `noDefault` that a `#[cfg_attr(…, path)]` variant
|
|
367
|
+
// always applies so the default file lookup never happens, `cfg` the
|
|
368
|
+
// `#[cfg(…)]` predicate gating it (several → `all(…)`; null when ungated),
|
|
369
|
+
// `conditional` marks one that may not exist as an item (a cfg the loader can't
|
|
370
|
+
// decide, an inline ancestor so gated, or inside a macro invocation body), `paths`
|
|
371
|
+
// the explicit file paths its attributes name (see parseAttr), minus the
|
|
372
|
+
// `cfg_attr` variants whose predicate can't hold in the build, `at` where the
|
|
373
|
+
// declaration is written and `offset` where it stands (textual macro scope: what a
|
|
374
|
+
// mounted file sees) -- the same, unless it sits in a `macro_rules!` body
|
|
375
|
+
// (`template`, that macro's name): then the macro's first bare invocation in
|
|
376
|
+
// this file, Infinity for none;
|
|
377
|
+
// refs: path references `{ spec, segments, absolute, inlinePath, fromUse, macroCall }` --
|
|
378
|
+
// flattened `use` trees plus expression-position `crate::`/`self::`/`super::`/
|
|
379
|
+
// `lead::…` paths (`macroCall`: one invoked, `$crate::name!(…)`);
|
|
380
|
+
// externCrates: `extern crate x [as y];` as `{ name, alias, inlinePath }`;
|
|
381
|
+
// defined: the items the file defines by name, `{ name, inlinePath, vis, cfg }`: a `struct`,
|
|
382
|
+
// `enum`, `union`, `trait`, `type`, `fn`, `const` or `static` at module level (not
|
|
383
|
+
// an `impl`'s or trait's associated item, nor a `macro_rules!` template's) -- what
|
|
384
|
+
// a glob into the module brings in; `cfg` the predicate gating it (see mods);
|
|
385
|
+
// bindings: names the file's `use` items and `extern crate … as` aliases bring into scope --
|
|
386
|
+
// a path lead among them names an import, not a crate;
|
|
387
|
+
// imports: the `use` items, and `extern crate` declarations as the imports they amount to
|
|
388
|
+
// (`extern crate serde_core as s` ≡ `use ::serde_core as s`; `extern crate self as
|
|
389
|
+
// x` ≡ `use crate as x`): `{ segments, absolute, binding, glob, inlinePath, vis,
|
|
390
|
+
// cfg, macro }` -- what a path written in that module may be anchored on, and, as
|
|
391
|
+
// far as `vis` reaches (null: private to the module; `crate`; `super`; `self`; `in
|
|
392
|
+
// a::b`), what a path from another module may continue along, i.e. a re-export;
|
|
393
|
+
// `cfg` gates it (see mods), `macro` is the invocation body it sits in, if any (one
|
|
394
|
+
// in a `macro_rules!` template is the invoking module's, not this one's);
|
|
395
|
+
// macros: `macro_rules!` definitions `{ name, exported, includes, calls, offset, template, gate }` -- an
|
|
396
|
+
// exported one is `$crate::name!` from anywhere in the crate, and lives in this
|
|
397
|
+
// file; `includes` are the include macros in its body, which rustc resolves
|
|
398
|
+
// relative to each file that invokes the macro; one written in another
|
|
399
|
+
// `macro_rules!`'s body (`template`, its name) is defined where that one is invoked
|
|
400
|
+
// (`offset`: the first bare invocation in this file, Infinity for none), as is a
|
|
401
|
+
// `mod` there;
|
|
402
|
+
// invocations: the names of the macros invoked by bare name (`ready!(…)`) outside `macro_rules!`
|
|
403
|
+
// templates -- for an edge to the file of a `macro_rules!` defined elsewhere in
|
|
404
|
+
// the crate; `calls` has each name's invocation offsets, in order (textual scope
|
|
405
|
+
// is decided at the call); a template's own bare calls are its definition's
|
|
406
|
+
// `calls`, made wherever it is invoked;
|
|
407
|
+
// includes: `include!` / `include_str!` / `include_bytes!` invocations outside macro
|
|
408
|
+
// definitions, `{ kind, path, base, conditional }` -- a file to carry, named
|
|
409
|
+
// relative to this one (`base` `file`) or to the package root (`manifest`, for
|
|
410
|
+
// `concat!(env!("CARGO_MANIFEST_DIR"), …)`); `unfollowed` counts the ones whose
|
|
411
|
+
// argument names no file the loader can follow (`concat!(env!("OUT_DIR"), …)`);
|
|
412
|
+
// inlineModules: the paths of the inline `mod x { … }` blocks, for the module tree, and
|
|
413
|
+
// `inlineModuleVis`, each path (`a::b`) → its visibility;
|
|
414
|
+
// skipped: the template macros (see `templates` below) whose bodies were skipped.
|
|
415
|
+
// A file whose inner `#![cfg(<pred>)]` can never hold is compiled empty: the scan is empty too.
|
|
416
|
+
// `templates` names the macros whose bodies are token templates for another crate's code (quote!)
|
|
417
|
+
// and are skipped whole: TEMPLATE_MACROS, less the ones the file's package defines itself. The
|
|
418
|
+
// build (see evalCfg): `features` on for certain and `maybeFeatures` on maybe, `test`, the
|
|
419
|
+
// platform's cfg set `target`; or `units`, one such build per way the file is compiled.
|
|
420
|
+
export function scanRustItems(content, { features = null, maybeFeatures = null, test = false, target = null, units = null, templates = TEMPLATE_MACROS } = {}) {
|
|
421
|
+
const env = units === null ? { features, maybeFeatures, test, target } : { units: units.map((u) => ({ ...u, test })) }
|
|
422
|
+
const { code, masked } = lexRust(content)
|
|
423
|
+
const n = masked.length
|
|
424
|
+
const mods = []
|
|
425
|
+
const uses = []
|
|
426
|
+
const imports = []
|
|
427
|
+
const macros = []
|
|
428
|
+
const invocations = new Set()
|
|
429
|
+
const calls = new Map() // macro name → the offsets of its bare invocations, in order
|
|
430
|
+
const pathInvocations = new Set() // the macros invoked by path (`crate::m!()`), by their last segment
|
|
431
|
+
const callSites = new Map() // macro name → each invocation, bare or by path: `{ offset, inlinePath, inlineDirs }`
|
|
432
|
+
const includes = []
|
|
433
|
+
const defined = []
|
|
434
|
+
const skipped = new Set() // the template macros whose bodies were skipped
|
|
435
|
+
let unfollowed = 0
|
|
436
|
+
const useSpans = [] // [start, end) of each `use` item's body, parsed as a tree below
|
|
437
|
+
const deadSpans = [] // [start, end) of each skipped test/doc-only item
|
|
438
|
+
const externCrates = []
|
|
439
|
+
const bindings = new Set()
|
|
440
|
+
const spans = [] // closed inline module blocks: { start, end, path, vis }
|
|
441
|
+
const stack = [] // open inline modules: { name, dir, depth, conditional, start, vis }
|
|
442
|
+
// `impl … { }` and `trait … { }` bodies, `{ start, end, depth }`: a `fn`, `const` or `type` at
|
|
443
|
+
// their depth is an associated item, not one of the module's. `extern "C" { }` blocks the same
|
|
444
|
+
// way: what they declare is the module's. Both are met in source order and nest or lie apart,
|
|
445
|
+
// so each list is a stack: the ones the scan has left are popped off its end.
|
|
446
|
+
const assocBlocks = []
|
|
447
|
+
const foreignBlocks = []
|
|
448
|
+
const openBlocks = (blocks, at) => {
|
|
449
|
+
while (blocks.length > 0 && blocks.at(-1).end < at) blocks.pop()
|
|
450
|
+
return blocks
|
|
451
|
+
}
|
|
452
|
+
// The `macro_rules!` names this file defines: a `quote!` of its own is no template.
|
|
453
|
+
const localMacros = new Set([...masked.matchAll(/\bmacro_rules!\s*([A-Za-z_]\w*)/gu)].map((m) => m[1]))
|
|
454
|
+
let depth = 0
|
|
455
|
+
let pending = [] // parsed outer attributes (parseAttr) waiting for their item
|
|
456
|
+
let attrStart = 0 // where the first pending attribute begins: a dead item is dead from there
|
|
457
|
+
let visNext = null // the previous token was a `pub` visibility (parseVisibility): the coming item has it
|
|
458
|
+
// Open blocks a live `#[cfg]` gates -- `if #[cfg(unix)] { mod imp; }` in a `cfg_if!` body, a
|
|
459
|
+
// `#[cfg(x)] mod m { mod a; }` -- as `{ depth, cfg }`: everything declared inside is gated too.
|
|
460
|
+
// `scopeCfg` is their conjunction, kept as they open and close.
|
|
461
|
+
const cfgScopes = []
|
|
462
|
+
let scopeCfg = null
|
|
463
|
+
const openScope = (d, cfg) => {
|
|
464
|
+
cfgScopes.push({ depth: d, cfg })
|
|
465
|
+
scopeCfg = joinCfgs(cfgScopes.map((s) => s.cfg))
|
|
466
|
+
}
|
|
467
|
+
const closeScopes = (d) => {
|
|
468
|
+
if (cfgScopes.length === 0 || cfgScopes.at(-1).depth <= d) return
|
|
469
|
+
while (cfgScopes.length > 0 && cfgScopes.at(-1).depth > d) cfgScopes.pop()
|
|
470
|
+
scopeCfg = joinCfgs(cfgScopes.map((s) => s.cfg))
|
|
471
|
+
}
|
|
472
|
+
// A predicate's verdict in this scan's build (evalCfg), once per predicate: the open scopes'
|
|
473
|
+
// conjunction is asked at every token inside them.
|
|
474
|
+
const verdicts = new Map()
|
|
475
|
+
const verdict = (pred) => cached(verdicts, pred, () => evalCfg(pred, env))
|
|
476
|
+
// The cfg of a `#[cfg(…)] name! { … }`: it gates the invocation, so everything the body
|
|
477
|
+
// declares (mio's `#[cfg(unix)] cfg_os_poll! { mod unix; }`); taken up by the body's `{`.
|
|
478
|
+
let macroBodyCfg = null
|
|
479
|
+
// A chain of cfg-gated branches, `if #[cfg(a)] { … } else if #[cfg(b)] { … } else { … }` (a
|
|
480
|
+
// `cfg_if!` body): a branch applies only when the cfgs of those before it don't hold, so its
|
|
481
|
+
// block is gated on `all(not(a), b)` -- dead outright once an earlier cfg is known to hold.
|
|
482
|
+
// Open chains by the depth of their branch blocks (a `cfg_if!` inside a `cfg_if!` branch is a
|
|
483
|
+
// chain of its own): depth → the cfgs of the branches so far.
|
|
484
|
+
const chains = new Map()
|
|
485
|
+
// The word right before `pos` (skipping whitespace) and where it starts, or null.
|
|
486
|
+
const wordBefore = (pos) => {
|
|
487
|
+
let k = pos - 1
|
|
488
|
+
while (k >= 0 && WS_RE.test(masked[k])) k--
|
|
489
|
+
const end = k + 1
|
|
490
|
+
while (k >= 0 && isWordChar(masked[k])) k--
|
|
491
|
+
return k + 1 < end ? { word: masked.slice(k + 1, end), start: k + 1 } : null
|
|
492
|
+
}
|
|
493
|
+
// The cfg a `{` at `i` opens its block under, when the block is a branch of a cfg chain: its own
|
|
494
|
+
// (`own`, from the attributes right after the `if`) and the negation of every earlier branch's;
|
|
495
|
+
// null for any other block. Records the branch on its chain.
|
|
496
|
+
const branchCfg = (i, own) => {
|
|
497
|
+
const lead = wordBefore(pending.length > 0 ? attrStart : i)
|
|
498
|
+
if (lead === null || (lead.word !== 'if' && lead.word !== 'else')) return null
|
|
499
|
+
const continuing = lead.word === 'else' || wordBefore(lead.start)?.word === 'else'
|
|
500
|
+
const negations = continuing ? chains.get(depth + 1) ?? [] : []
|
|
501
|
+
if (own === null && negations.length === 0) return null
|
|
502
|
+
chains.set(depth + 1, own === null ? negations : [...negations, own])
|
|
503
|
+
return joinCfgs([...negations.map((c) => `not(${c})`), ...(own === null ? [] : [own])])
|
|
504
|
+
}
|
|
505
|
+
// After a branch block at `blockDepth` closed at `end`: its chain goes on only if an `else` follows.
|
|
506
|
+
const endBranch = (end, blockDepth) => {
|
|
507
|
+
if (chains.has(blockDepth) && readWord(skipWs(end)) !== 'else') chains.delete(blockDepth)
|
|
508
|
+
}
|
|
509
|
+
// End of the outermost macro invocation body being scanned (`m! { … }`, `m!( … )`,
|
|
510
|
+
// `macro_rules! m { … }`). Its tokens are macro input: a `mod x;` there only becomes an item if
|
|
511
|
+
// the macro emits it (cfg_if! does; serde_with's generate_guide! turns it into an inline module
|
|
512
|
+
// documented from a .md file), so such declarations are followed when their file exists and
|
|
513
|
+
// tolerated when it doesn't -- the same footing as a `#[cfg]`-gated one. `macroName` is that
|
|
514
|
+
// macro's name: what stands for the cfg a `mod` it emits is under (tokio's `cfg_has_atomic_u64!
|
|
515
|
+
// { mod imp; }` beside `cfg_not_has_atomic_u64! { mod imp; }`).
|
|
516
|
+
let macroUntil = -1
|
|
517
|
+
let macroName = null
|
|
518
|
+
// The `macro_rules!` being defined (its name) while inside its body: what it declares (serde's
|
|
519
|
+
// `crate_root! { … mod de; … }`) appears where the macro is invoked, not where it is defined, so
|
|
520
|
+
// such a `mod` takes the offset of the macro's first bare invocation in this file (`calls`).
|
|
521
|
+
let templateName = null
|
|
522
|
+
let templateMacro = null // the top-level `macro_rules!` whose body the scan is in: what its body calls is its own, nested definitions' too
|
|
523
|
+
// The `macro_rules!` bodies the scan is in, outermost first, `{ macro, end, depth }`: what a
|
|
524
|
+
// nested one's body calls, includes and declares is that one's, made where it is invoked (which
|
|
525
|
+
// may be another directory than where the outer one is); `depth`, the inline modules around
|
|
526
|
+
// its definition, which a `mod` its body declares is not in where it is invoked.
|
|
527
|
+
const defs = []
|
|
528
|
+
const innermostEntry = (at) => {
|
|
529
|
+
while (defs.length > 0 && defs.at(-1).end < at) defs.pop()
|
|
530
|
+
return defs.at(-1)
|
|
531
|
+
}
|
|
532
|
+
const innermostDef = (at) => innermostEntry(at)?.macro ?? templateMacro
|
|
533
|
+
|
|
534
|
+
// Whether the token at `at` sits in a `macro_rules!` body: a template, expanded where the
|
|
535
|
+
// macro is invoked.
|
|
536
|
+
const inTemplate = (at) => at < macroUntil && macroName === 'macro_rules'
|
|
537
|
+
// Whether what is declared at `at` under `cfg` may not exist as an item: a cfg not known to
|
|
538
|
+
// hold, an inline module so gated around it, or a macro invocation's body.
|
|
539
|
+
const conditionalAt = (cfg, at) => (cfg !== null && verdict(cfg) !== true) || stack.some((s) => s.conditional) || at < macroUntil
|
|
540
|
+
const inlinePath = () => stack.map((s) => s.name)
|
|
541
|
+
const closeTo = (targetDepth, at) => {
|
|
542
|
+
while (stack.length > 0 && stack.at(-1).depth > targetDepth) {
|
|
543
|
+
const top = stack.pop()
|
|
544
|
+
spans.push({ start: top.start, end: at, path: [...inlinePath(), top.name], vis: top.vis })
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Whether an item at `at` (depth `d`) sits directly in an `impl`/`trait` body.
|
|
548
|
+
const isAssociated = (at, d) => openBlocks(assocBlocks, at).some((b) => b.start < at && b.depth === d)
|
|
549
|
+
// Whether an item at `at` is at its module's own level: the module body (a file's, an inline
|
|
550
|
+
// module's), a `#[cfg]`-gated block or an `extern "C" { }` block there, or a macro invocation's
|
|
551
|
+
// body (libc's `s! { … }`); not a `fn` body.
|
|
552
|
+
const atModuleLevel = (at) => {
|
|
553
|
+
if (at < macroUntil) return true
|
|
554
|
+
const base = stack.length > 0 ? stack.at(-1).depth : 0
|
|
555
|
+
return depth === base || cfgScopes.some((s) => s.depth === depth) || openBlocks(foreignBlocks, at).some((b) => b.start < at && b.depth === depth)
|
|
556
|
+
}
|
|
557
|
+
// The `{` opening the body of the item starting at `from` (an `impl`/`trait` header may hold
|
|
558
|
+
// generics, bounds and a `where` clause), or -1 when a `;` ends it first.
|
|
559
|
+
const bodyOpen = (from) => {
|
|
560
|
+
let nest = 0
|
|
561
|
+
for (let k = from; k < n; k++) {
|
|
562
|
+
const ch = masked[k]
|
|
563
|
+
if (ch === '(' || ch === '[' || ch === '<') nest++
|
|
564
|
+
else if (ch === ')' || ch === ']' || (ch === '>' && masked[k - 1] !== '-' && masked[k - 1] !== '=')) nest = Math.max(0, nest - 1)
|
|
565
|
+
else if (nest === 0 && ch === '{') return k
|
|
566
|
+
else if (nest === 0 && ch === ';') return -1
|
|
567
|
+
}
|
|
568
|
+
return -1
|
|
569
|
+
}
|
|
570
|
+
const readWord = (at) => {
|
|
571
|
+
WORD_RE.lastIndex = at
|
|
572
|
+
const m = WORD_RE.exec(masked)
|
|
573
|
+
return m ? m[0] : null
|
|
574
|
+
}
|
|
575
|
+
const skipWs = (at) => {
|
|
576
|
+
while (at < n && WS_RE.test(masked[at])) at++
|
|
577
|
+
return at
|
|
578
|
+
}
|
|
579
|
+
const skipWsBack = (at) => {
|
|
580
|
+
while (at >= 0 && WS_RE.test(masked[at])) at--
|
|
581
|
+
return at
|
|
582
|
+
}
|
|
583
|
+
// The end of the item starting at `from`, without leaving its enclosing block: through a
|
|
584
|
+
// top-level `;` or its `{ … }` body, or -- for a field, variant or match arm (`itemLike`
|
|
585
|
+
// false) -- a top-level `,`; a `}` or `)` closing the enclosing block is left for the main
|
|
586
|
+
// loop. Parentheses/brackets nest (a tuple field's `(u8, u8)` comma isn't the field's end), and
|
|
587
|
+
// so does a generic argument list -- a `<` right after a name or `::` (`HashMap<K, V>`, `f::<T>`)
|
|
588
|
+
// up to its `>` (the `>` of `->` / `=>` is not one) -- so the comma in `Result<A, B>` isn't
|
|
589
|
+
// either. An item (`fn f<'a, T>(…) -> R where T: Tr, { … }`, `impl<A, B> …`) never ends at a
|
|
590
|
+
// comma at all. A dead statement or arm whose value is an `if` (`elseChain`: anything but a bare
|
|
591
|
+
// `{ … }` block) takes its `else` branches with it; a dead bare block does not: in `cfg_if! { if
|
|
592
|
+
// #[cfg(a)] { … } else { … } }` the `else` is what applies when the cfg doesn't.
|
|
593
|
+
// A `let` (`toSemicolon`) runs to its `;` whatever blocks its initializer holds.
|
|
594
|
+
const skipItem = (from, itemLike, { elseChain = false, toSemicolon = false } = {}) => {
|
|
595
|
+
let nest = 0
|
|
596
|
+
let angle = 0
|
|
597
|
+
for (let k = from; k < n; k++) {
|
|
598
|
+
const ch = masked[k]
|
|
599
|
+
if (ch === '(' || ch === '[') nest++
|
|
600
|
+
else if (ch === ')' || ch === ']') {
|
|
601
|
+
if (--nest < 0) return k
|
|
602
|
+
} else if (nest === 0) {
|
|
603
|
+
if (ch === ';') return k + 1
|
|
604
|
+
if (ch === '}') return k
|
|
605
|
+
if (ch === '{') {
|
|
606
|
+
const close = matchClose(masked, k)
|
|
607
|
+
if (toSemicolon) {
|
|
608
|
+
k = close
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
const after = skipWs(close + 1)
|
|
612
|
+
if (!elseChain || readWord(after) !== 'else') return close + 1
|
|
613
|
+
k = after + 3
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
616
|
+
if (ch === '<' && (angle > 0 || isWordChar(masked[k - 1]) || masked[k - 1] === ':')) angle++
|
|
617
|
+
else if (ch === '>' && angle > 0 && masked[k - 1] !== '-' && masked[k - 1] !== '=') angle--
|
|
618
|
+
else if (ch === ',' && angle === 0 && !itemLike) return k + 1
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return n
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let i = 0
|
|
625
|
+
while (i < n) {
|
|
626
|
+
const ch = masked[i]
|
|
627
|
+
if (WS_RE.test(ch)) {
|
|
628
|
+
i++
|
|
629
|
+
continue
|
|
630
|
+
}
|
|
631
|
+
if (i >= macroUntil) macroName = null
|
|
632
|
+
if (ch === '#' && (masked[i + 1] === '[' || (masked[i + 1] === '!' && masked[i + 2] === '['))) {
|
|
633
|
+
const outer = masked[i + 1] === '['
|
|
634
|
+
const open = outer ? i + 1 : i + 2
|
|
635
|
+
const close = matchClose(masked, open)
|
|
636
|
+
const attr = parseAttr(code.slice(open + 1, close), masked.slice(open + 1, close))
|
|
637
|
+
if (outer) {
|
|
638
|
+
// `#[cfg_attr(<pred>, derive(serde::Serialize))]` with pred never holding applies nothing:
|
|
639
|
+
// the paths in its text are dead code too, and so is whatever else it would apply.
|
|
640
|
+
const inert = attr.pred !== null && verdict(attr.pred) === false
|
|
641
|
+
if (inert) deadSpans.push([i, close + 1])
|
|
642
|
+
if (pending.length === 0) attrStart = i
|
|
643
|
+
pending.push(inert ? INERT_ATTR : attr)
|
|
644
|
+
} else {
|
|
645
|
+
// An inner `#![…]` attribute applies to the enclosing module, not to the next item. A
|
|
646
|
+
// `#![cfg(<pred>)]` that can't hold empties that module: the whole file at the top, else
|
|
647
|
+
// the inline module it opens.
|
|
648
|
+
// (An inner attribute inside a macro invocation's body is the macro's business.)
|
|
649
|
+
if (attr.cfg !== null && verdict(attr.cfg) === false && i >= macroUntil) {
|
|
650
|
+
if (depth === 0) return { mods: [], refs: [], externCrates: [], bindings: new Set(), imports: [], macros: [], invocations: new Set(), calls: new Map(), pathInvocations: new Set(), callSites: new Map(), includes: [], unfollowed: 0, defined: [], inlineModules: [], inlineModuleVis: new Map(), skipped }
|
|
651
|
+
const top = stack.at(-1)
|
|
652
|
+
if (top !== undefined && top.depth === depth) {
|
|
653
|
+
const end = matchClose(masked, top.start - 1)
|
|
654
|
+
deadSpans.push([i, end])
|
|
655
|
+
pending = []
|
|
656
|
+
i = end
|
|
657
|
+
continue
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// `#![doc = include_str!("../README.md")]`: the module's own include, applied here.
|
|
661
|
+
const conditional = conditionalAt(null, i)
|
|
662
|
+
for (const inc of attr.includes) includes.push({ ...inc, conditional })
|
|
663
|
+
pending = []
|
|
664
|
+
}
|
|
665
|
+
i = close + 1
|
|
666
|
+
continue
|
|
667
|
+
}
|
|
668
|
+
// The cfg gating the item the pending attributes belong to (several `#[cfg]`s all apply,
|
|
669
|
+
// and so does every enclosing block's). `pub` is visibility, not the item: its attributes
|
|
670
|
+
// stay pending for the keyword after it.
|
|
671
|
+
const word = readWord(i)
|
|
672
|
+
const attrCfg = word === 'pub' ? null : joinCfgs(pending.map((a) => a.cfg).filter((c) => c !== null))
|
|
673
|
+
// A `{` opening a branch of a cfg chain is gated on the earlier branches' cfgs not holding too.
|
|
674
|
+
const own = ch === '{' ? branchCfg(i, attrCfg) ?? attrCfg : attrCfg
|
|
675
|
+
const cfg = own === null ? scopeCfg : joinCfgs([...cfgScopes.map((s) => s.cfg), own])
|
|
676
|
+
// An item gated on a cfg that never holds in the build is dead code for the bundle: skip it
|
|
677
|
+
// whole -- from its first attribute (a `#[derive(serde::Serialize)]` on it names nothing
|
|
678
|
+
// live) through a declaration's `;`, a field's or variant's `,`, or a body's or block's `}`
|
|
679
|
+
// -- without recording anything in it, whatever token starts it: a keyword or a name, a
|
|
680
|
+
// `{ … }` block statement, a `(a, b)` / `[a, ..]` pattern or tuple type, a `&x`, `*x` or literal.
|
|
681
|
+
if (cfg !== null && verdict(cfg) === false) {
|
|
682
|
+
pending = []
|
|
683
|
+
visNext = null
|
|
684
|
+
const end = skipItem(word === null ? i : i + word.length, word !== null && ITEM_KEYWORDS.has(word), { elseChain: ch !== '{', toSemicolon: word === 'let' })
|
|
685
|
+
deadSpans.push([attrStart, end])
|
|
686
|
+
if (ch === '{') endBranch(end, depth + 1)
|
|
687
|
+
i = end
|
|
688
|
+
continue
|
|
689
|
+
}
|
|
690
|
+
// The item is live, so the includes its attributes name (`#[doc = include_str!("docs/x.md")]`)
|
|
691
|
+
// are too; a `pub` keeps them pending for the keyword after it.
|
|
692
|
+
if (word !== 'pub' && pending.length > 0) {
|
|
693
|
+
const conditional = conditionalAt(cfg, i)
|
|
694
|
+
for (const a of pending) {
|
|
695
|
+
for (const inc of a.includes) includes.push({ ...inc, conditional })
|
|
696
|
+
a.includes = []
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (ch === '{') {
|
|
700
|
+
depth++
|
|
701
|
+
const gate = own ?? macroBodyCfg
|
|
702
|
+
macroBodyCfg = null
|
|
703
|
+
if (gate !== null) openScope(depth, gate)
|
|
704
|
+
pending = []
|
|
705
|
+
i++
|
|
706
|
+
continue
|
|
707
|
+
}
|
|
708
|
+
if (ch === '}') {
|
|
709
|
+
depth--
|
|
710
|
+
closeTo(depth, i)
|
|
711
|
+
closeScopes(depth)
|
|
712
|
+
endBranch(i + 1, depth + 1)
|
|
713
|
+
pending = []
|
|
714
|
+
i++
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
const vis = visNext
|
|
718
|
+
visNext = null
|
|
719
|
+
if (word === null) {
|
|
720
|
+
pending = []
|
|
721
|
+
i++
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
// `name!` followed by a bracket opens a macro invocation body (`macro_rules! name` too).
|
|
725
|
+
const bang = skipWs(i + word.length)
|
|
726
|
+
const pathQualified = masked[i - 1] === ':' && masked[i - 2] === ':'
|
|
727
|
+
if (masked[bang] === '!') {
|
|
728
|
+
let open = skipWs(bang + 1)
|
|
729
|
+
let definition = null // the `macro_rules!` this word defines
|
|
730
|
+
if (word === 'macro_rules') {
|
|
731
|
+
const name = readWord(open)
|
|
732
|
+
if (name !== null) {
|
|
733
|
+
open = skipWs(open + name.length)
|
|
734
|
+
const gate = GATE_MACRO_RE.test(name) && masked[open] === '{' ? gatePredicate(code, masked, open, matchClose(masked, open)) : undefined
|
|
735
|
+
macros.push({ name, exported: pending.some((a) => a.macroExport === true), includes: [], calls: new Set(), pathCalls: new Set(), offset: i, at: i, template: inTemplate(i) ? templateName : null, gate, cfg, macro: i < macroUntil ? macroName : null })
|
|
736
|
+
definition = macros.at(-1)
|
|
737
|
+
if (i >= macroUntil) {
|
|
738
|
+
templateName = name
|
|
739
|
+
templateMacro = macros.at(-1)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (masked[open] === '{' || masked[open] === '(' || masked[open] === '[') {
|
|
744
|
+
const close = matchClose(masked, open)
|
|
745
|
+
if (definition !== null) {
|
|
746
|
+
innermostDef(i)
|
|
747
|
+
defs.push({ macro: definition, end: close, depth: stack.length })
|
|
748
|
+
}
|
|
749
|
+
if (templates.has(word) && !localMacros.has(word)) {
|
|
750
|
+
// serde_derive's `quote! { use #path as _serde; … _serde::__private::Ok(…) }`, bare or as
|
|
751
|
+
// `quote::quote! { … }`: skipped whole. (A crate's own `macro_rules! quote` is no template.)
|
|
752
|
+
deadSpans.push([i, close + 1])
|
|
753
|
+
skipped.add(word)
|
|
754
|
+
pending = []
|
|
755
|
+
i = close + 1
|
|
756
|
+
continue
|
|
757
|
+
}
|
|
758
|
+
if (i >= macroUntil) macroName = word
|
|
759
|
+
// (A cfg on a `macro_rules!` definition gates the definition, not what its template
|
|
760
|
+
// declares wherever it is invoked -- an approximation kept from before.)
|
|
761
|
+
if (own !== null && masked[open] === '{' && word !== 'macro_rules') macroBodyCfg = own
|
|
762
|
+
if (word !== 'macro_rules') {
|
|
763
|
+
// Invoked by bare name; `$crate::name!` / `serde_json::json!` is a path (a ref). One in a
|
|
764
|
+
// `macro_rules!` template is the template's (`calls` on the definition): rustc resolves
|
|
765
|
+
// it where the macro is invoked, not where it is written -- and a recursive arm's
|
|
766
|
+
// `m!(…)` is no invocation of this file's.
|
|
767
|
+
// A call by path (`crate::m!()`, `$crate::m!()`, `dep::m!()`) is no name to resolve in
|
|
768
|
+
// scope, but an invocation of a macro of that name all the same, for where what its
|
|
769
|
+
// template declares and includes is made (`pathInvocations`, `pathCalls`); each call
|
|
770
|
+
// outside templates is also kept with the inline modules around it (`callSites`).
|
|
771
|
+
if (inTemplate(i) && templateMacro !== null) (pathQualified ? innermostDef(i).pathCalls : innermostDef(i).calls).add(word)
|
|
772
|
+
else {
|
|
773
|
+
if (pathQualified) pathInvocations.add(word)
|
|
774
|
+
else {
|
|
775
|
+
invocations.add(word)
|
|
776
|
+
;(calls.get(word) ?? calls.set(word, []).get(word)).push(i)
|
|
777
|
+
}
|
|
778
|
+
;(callSites.get(word) ?? callSites.set(word, []).get(word)).push({ offset: i, inlinePath: inlinePath(), inlineDirs: stack.map((s) => s.dir) })
|
|
779
|
+
}
|
|
780
|
+
if (INCLUDE_MACROS.has(word)) {
|
|
781
|
+
const text = code.slice(open + 1, close)
|
|
782
|
+
const arg = includeArg(text)
|
|
783
|
+
const conditional = conditionalAt(cfg, i)
|
|
784
|
+
// Inside a `macro_rules!` body the path is relative to whichever file invokes the
|
|
785
|
+
// macro: recorded on the definition, resolved per invoking file. Build output
|
|
786
|
+
// (`concat!(env!("OUT_DIR"), …)`) is nobody's to follow; any other unreadable argument
|
|
787
|
+
// is counted, for a warning.
|
|
788
|
+
if (arg === null) unfollowed += /\bOUT_DIR\b/u.test(text) ? 0 : 1
|
|
789
|
+
else if (inTemplate(i) && templateMacro !== null) innermostDef(i).includes.push({ kind: word, ...arg })
|
|
790
|
+
else includes.push({ kind: word, ...arg, conditional })
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
macroUntil = Math.max(macroUntil, close)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// An `impl`/`trait` body holds associated items, an `extern "C" { }` block the module's own
|
|
797
|
+
// (an `extern "C" fn f() { }` is a fn, with a body of its own).
|
|
798
|
+
if (word === 'impl' || word === 'trait') {
|
|
799
|
+
const open = bodyOpen(i + word.length)
|
|
800
|
+
if (open !== -1) assocBlocks.push({ start: open, end: matchClose(masked, open), depth: depth + 1 })
|
|
801
|
+
} else if (word === 'extern' && readWord(bang) !== 'crate') {
|
|
802
|
+
let k = bang
|
|
803
|
+
if (masked[k] === '"') k = skipWs(masked.indexOf('"', k + 1) + 1)
|
|
804
|
+
if (masked[k] === '{') foreignBlocks.push({ start: k, end: matchClose(masked, k), depth: depth + 1 })
|
|
805
|
+
}
|
|
806
|
+
// An item the module defines by name (`pub struct sigset_t`, inside libc's `s! { … }` or an
|
|
807
|
+
// `extern "C" { }` block too), at the module's own level -- not a `fn`'s local item, nor an
|
|
808
|
+
// `impl`/`trait` body's associated one, nor a `macro_rules!` template's (that is some invoking
|
|
809
|
+
// module's item); `const fn` / `static mut` / lazy_static's `static ref` are not items named
|
|
810
|
+
// `fn` / `mut` / `ref`.
|
|
811
|
+
if (DEFINING_KEYWORDS.has(word) && atModuleLevel(i) && !isAssociated(i, depth) && !inTemplate(i) && !(word === 'const' && masked[skipWsBack(i - 1)] === '*')) {
|
|
812
|
+
let at = skipWs(i + word.length)
|
|
813
|
+
if (masked.startsWith('mut ', at) || masked.startsWith('ref ', at)) at = skipWs(at + 3)
|
|
814
|
+
if (masked.startsWith('r#', at)) at += 2
|
|
815
|
+
const name = readWord(at)
|
|
816
|
+
const next = name === null ? null : masked[skipWs(at + name.length)]
|
|
817
|
+
// `ns`: a `fn`, `const` or `static` lives in the value namespace and can't lead a path
|
|
818
|
+
// (`*const libc::c_char`, a raw pointer type, is no item at all; a `fn log` beside
|
|
819
|
+
// `use log::info` hides no crate).
|
|
820
|
+
if (name !== null && !ITEM_KEYWORDS.has(name) && (word !== 'union' || next === '{' || next === '<')) defined.push({ name, inlinePath: inlinePath(), vis, cfg, macro: i < macroUntil ? macroName : null, ns: VALUE_KEYWORDS.has(word) ? 'value' : 'type' })
|
|
821
|
+
}
|
|
822
|
+
if (word === 'pub') {
|
|
823
|
+
// Visibility sits between an item's attributes and its keyword; `pub(crate)` etc. included.
|
|
824
|
+
i = skipWs(i + 3)
|
|
825
|
+
visNext = 'crate'
|
|
826
|
+
if (masked[i] === '(') {
|
|
827
|
+
const close = matchClose(masked, i)
|
|
828
|
+
visNext = parseVisibility(masked.slice(i + 1, close))
|
|
829
|
+
i = close + 1
|
|
830
|
+
}
|
|
831
|
+
continue
|
|
832
|
+
}
|
|
833
|
+
if (word === 'mod') {
|
|
834
|
+
let j = skipWs(i + 3)
|
|
835
|
+
if (masked.startsWith('r#', j)) j += 2
|
|
836
|
+
const name = readWord(j)
|
|
837
|
+
if (name === null) {
|
|
838
|
+
pending = []
|
|
839
|
+
i = j
|
|
840
|
+
continue
|
|
841
|
+
}
|
|
842
|
+
const k = skipWs(j + name.length)
|
|
843
|
+
const attrs = pending
|
|
844
|
+
pending = []
|
|
845
|
+
// Dead cfgs were skipped above; a decidable-true one (`not(test)`) is as firm as no cfg at all.
|
|
846
|
+
const conditional = conditionalAt(cfg, i)
|
|
847
|
+
if (masked[k] === ';') {
|
|
848
|
+
// rustc applies the first `#[path]` / `#[cfg_attr(<pred>, path = …)]` whose predicate holds.
|
|
849
|
+
// A variant whose predicate can't hold names nothing in this build; one whose predicate
|
|
850
|
+
// holds ends the list -- outright the `#[path]` when nothing undecided precedes it -- and
|
|
851
|
+
// either way the default `<name>.rs` lookup never happens.
|
|
852
|
+
const live = attrs.flatMap((a) => a.paths).filter((p) => p.cfg === null || verdict(p.cfg) !== false)
|
|
853
|
+
const applies = live.findIndex((p) => p.cfg === null || verdict(p.cfg) === true)
|
|
854
|
+
const paths = applies === -1 ? live : (applies === 0 ? [{ path: live[0].path, cfg: null }] : live.slice(0, applies + 1))
|
|
855
|
+
mods.push({
|
|
856
|
+
name, inlinePath: inlinePath(), inlineDirs: stack.map((s) => s.dir), cfg, conditional, paths, noDefault: applies !== -1, vis,
|
|
857
|
+
macroUse: attrs.some((a) => a.macroUse === true), macro: i < macroUntil ? macroName : null, offset: i, at: i,
|
|
858
|
+
template: inTemplate(i) ? innermostDef(i).name : null, templateDepth: inTemplate(i) ? innermostEntry(i)?.depth ?? 0 : 0,
|
|
859
|
+
})
|
|
860
|
+
i = k + 1
|
|
861
|
+
continue
|
|
862
|
+
}
|
|
863
|
+
if (masked[k] === '{') {
|
|
864
|
+
// `#[path = "dir"] mod m { mod a; }` finds `a` under `dir/`, not `m/`; solana-program's
|
|
865
|
+
// `#[path = ""] mod non_bpf_modules { mod account_keys; }` right beside the file.
|
|
866
|
+
const dir = attrs.flatMap((a) => a.paths).find((p) => p.cfg === null)?.path ?? name
|
|
867
|
+
depth++
|
|
868
|
+
if (own !== null) openScope(depth, own)
|
|
869
|
+
stack.push({ name, dir, depth, conditional, start: k + 1, vis })
|
|
870
|
+
i = k + 1
|
|
871
|
+
continue
|
|
872
|
+
}
|
|
873
|
+
i = k
|
|
874
|
+
continue
|
|
875
|
+
}
|
|
876
|
+
if (word === 'use') {
|
|
877
|
+
const end = masked.indexOf(';', i + 3)
|
|
878
|
+
const stop = end === -1 ? n : end
|
|
879
|
+
const body = code.slice(i + 3, stop)
|
|
880
|
+
// A `use` item's tree starts with a name, `::`, `{` or -- in a `macro_rules!` template --
|
|
881
|
+
// `$crate`. Anything else is the word in macro input (syn's `Token![use]`), and a tree
|
|
882
|
+
// interpolating a metavariable (`use $m::X;`, quote's `use #path as _serde;`; a raw
|
|
883
|
+
// identifier's `r#` is no interpolation) is a template for code the loader can't place:
|
|
884
|
+
// neither is an import of this file.
|
|
885
|
+
if (!/^\s*[A-Za-z_{:$]/u.test(body) || /(?<!\br)#|\$(?!crate\b)/u.test(body)) {
|
|
886
|
+
pending = []
|
|
887
|
+
i += 3
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
const ip = inlinePath()
|
|
891
|
+
const macro = i < macroUntil ? macroName : null
|
|
892
|
+
// `use $crate::…` in a `macro_rules!` template imports into whichever module invokes the macro.
|
|
893
|
+
const template = macro === 'macro_rules' && /\$crate\b/u.test(body)
|
|
894
|
+
for (const p of parseUseTree(body)) {
|
|
895
|
+
uses.push({ ...p, inlinePath: ip })
|
|
896
|
+
const im = { segments: p.segments, absolute: p.absolute, binding: p.binding, glob: p.glob, inlinePath: ip, vis, cfg, macro, template }
|
|
897
|
+
imports.push(im)
|
|
898
|
+
if (hidesCrate(im)) bindings.add(p.binding)
|
|
899
|
+
}
|
|
900
|
+
useSpans.push([i, stop])
|
|
901
|
+
pending = []
|
|
902
|
+
i = stop + 1
|
|
903
|
+
continue
|
|
904
|
+
}
|
|
905
|
+
if (word === 'extern') {
|
|
906
|
+
EXTERN_CRATE_RE.lastIndex = i
|
|
907
|
+
const m = EXTERN_CRATE_RE.exec(masked)
|
|
908
|
+
const macroUse = pending.some((a) => a.macroUse === true)
|
|
909
|
+
pending = []
|
|
910
|
+
if (m) {
|
|
911
|
+
const ip = inlinePath()
|
|
912
|
+
externCrates.push({ name: m[1], alias: m[2] ?? null, inlinePath: ip, macroUse })
|
|
913
|
+
if (m[2] !== undefined) bindings.add(m[2])
|
|
914
|
+
// `extern crate self as x;` names this crate's root; any other, a crate by its own name.
|
|
915
|
+
const selfCrate = m[1] === 'self'
|
|
916
|
+
imports.push({ segments: [selfCrate ? 'crate' : m[1]], absolute: !selfCrate, binding: m[2] ?? m[1], glob: false, inlinePath: ip, vis, cfg, macro: i < macroUntil ? macroName : null })
|
|
917
|
+
i = EXTERN_CRATE_RE.lastIndex
|
|
918
|
+
continue
|
|
919
|
+
}
|
|
920
|
+
i += word.length
|
|
921
|
+
continue
|
|
922
|
+
}
|
|
923
|
+
pending = []
|
|
924
|
+
i += word.length
|
|
925
|
+
}
|
|
926
|
+
closeTo(0, n)
|
|
927
|
+
// A `mod` or `macro_rules!` a `macro_rules!` template declares stands where the macro is first
|
|
928
|
+
// invoked here (serde's `crate_root! { … macro_rules! tri { … } … mod de; … }`).
|
|
929
|
+
for (const m of [...mods, ...macros]) if (m.template !== null) m.offset = calls.get(m.template)?.[0] ?? Infinity
|
|
930
|
+
|
|
931
|
+
// Innermost inline module enclosing an offset (spans nest, so the longest path wins).
|
|
932
|
+
const inlineAt = (offset) => {
|
|
933
|
+
let best = null
|
|
934
|
+
for (const s of spans) {
|
|
935
|
+
if (s.start <= offset && offset < s.end && (best === null || s.path.length > best.path.length)) best = s
|
|
936
|
+
}
|
|
937
|
+
return best ? best.path : []
|
|
938
|
+
}
|
|
939
|
+
const refs = new Map()
|
|
940
|
+
// `fromUse` marks a path written in a `use` item -- the reliable signal that its lead names a
|
|
941
|
+
// crate (an expression path's lead is as likely a module or an imported item). `macroCall`
|
|
942
|
+
// marks an invoked path (`$crate::span!(…)`): it names a macro, not the module of that name.
|
|
943
|
+
const addRef = (spec, segments, absolute, ip, fromUse, macroCall) => {
|
|
944
|
+
const key = `${ip.join('::')}\0${spec}\0${macroCall ? '!' : ''}`
|
|
945
|
+
if (!refs.has(key)) refs.set(key, { spec, segments, absolute, inlinePath: ip, fromUse, macroCall })
|
|
946
|
+
}
|
|
947
|
+
for (const u of uses) addRef(u.spec, u.segments, u.absolute, u.inlinePath, true, false)
|
|
948
|
+
// Expression-position paths: scan everything outside `use` items (the tree parser owns those;
|
|
949
|
+
// a regex over `use syn::{parse::Parse}` would take `parse::Parse` for a local module path) and
|
|
950
|
+
// outside skipped test/doc-only items (a `quickcheck::quickcheck(…)` in a `#[test]` fn body
|
|
951
|
+
// must not pull the vendored dev-dependency in): blank those spans, keeping offsets.
|
|
952
|
+
let exprs = ''
|
|
953
|
+
let at = 0
|
|
954
|
+
for (const [start, end] of [...useSpans, ...deadSpans].toSorted((a, b) => a[0] - b[0])) {
|
|
955
|
+
const from = Math.max(start, at)
|
|
956
|
+
if (end <= from) continue
|
|
957
|
+
exprs += masked.slice(at, from) + ' '.repeat(end - from)
|
|
958
|
+
at = end
|
|
959
|
+
}
|
|
960
|
+
exprs += masked.slice(at)
|
|
961
|
+
// Keyword paths (`crate::`/`self::`/`super::`) first, then paths led by a module or crate name.
|
|
962
|
+
const keywordPaths = []
|
|
963
|
+
const leadPaths = []
|
|
964
|
+
for (const m of exprs.matchAll(LEAD_PATH_RE)) {
|
|
965
|
+
// `$trait::fmt(…)` in a `macro_rules!` template: the lead is a metavariable, not a name;
|
|
966
|
+
// `$crate::…` is the one interpolation that names something (this crate).
|
|
967
|
+
if (exprs[m.index - 1] === '$' && m[1] !== 'crate') continue
|
|
968
|
+
if (PATH_KEYWORDS.has(m[1])) keywordPaths.push(m)
|
|
969
|
+
else if (!NON_CRATE_LEADS.has(m[1])) leadPaths.push(m)
|
|
970
|
+
}
|
|
971
|
+
for (const m of [...keywordPaths, ...leadPaths]) {
|
|
972
|
+
let after = m.index + m[0].length
|
|
973
|
+
while (after < n && WS_RE.test(exprs[after])) after++
|
|
974
|
+
const macroCall = exprs[after] === '!' && exprs[after + 1] !== '='
|
|
975
|
+
addRef(m[0], m[0].split('::'), false, inlineAt(m.index), false, macroCall)
|
|
976
|
+
}
|
|
977
|
+
const inlineModuleVis = new Map()
|
|
978
|
+
for (const s of spans) if (!inlineModuleVis.has(s.path.join('::'))) inlineModuleVis.set(s.path.join('::'), s.vis)
|
|
979
|
+
const inlineModules = [...inlineModuleVis.keys()].map((p) => p.split('::'))
|
|
980
|
+
return { mods, refs: [...refs.values()], externCrates, bindings, imports, macros, invocations, calls, pathInvocations, callSites, includes, unfollowed, defined, inlineModules, inlineModuleVis, skipped }
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Whether a `use` import binds a name that is not the crate of that name: `use std::io;` binds
|
|
984
|
+
// `io`, but `use serde_json;` / `use rand::{self, Rng};` bind the crate itself under its own name.
|
|
985
|
+
const hidesCrate = (im) => im.binding !== null && (im.segments.length > 1 || im.binding !== im.segments[0])
|
|
986
|
+
|
|
987
|
+
// --- Module files -----------------------------------------------------------------------
|
|
988
|
+
|
|
989
|
+
// Directory holding this file's submodules, per Rust's path rules: siblings for crate roots,
|
|
990
|
+
// mod.rs and files loaded through `#[path = …]` (rustc treats those like mod.rs), else under a
|
|
991
|
+
// `<stem>/` subdir. `root` marks a crate root by role (an entry such as `src/bin/tool.rs` or
|
|
992
|
+
// `tests/it.rs`) rather than by name.
|
|
993
|
+
export function getModuleDir(filePath, { root = false } = {}) {
|
|
97
994
|
const lastSlash = filePath.lastIndexOf('/')
|
|
98
995
|
const dir = lastSlash === -1 ? '' : filePath.slice(0, lastSlash)
|
|
99
996
|
const name = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1)
|
|
100
|
-
if (
|
|
997
|
+
if (root || ROOT_NAMES.has(name) || name === 'mod.rs') return dir
|
|
101
998
|
const stem = name.replace(/\.rs$/u, '')
|
|
102
999
|
return dir ? `${dir}/${stem}` : stem
|
|
103
1000
|
}
|
|
104
1001
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const base = modDir ? `${modDir}/${modName}` : modName
|
|
109
|
-
const candidates = [`${base}.rs`, `${base}/mod.rs`]
|
|
1002
|
+
// Whether `file` owns its directory for submodule lookup: a crate root by role, or a file a
|
|
1003
|
+
// `#[path]` attribute loaded (`pathLoaded`).
|
|
1004
|
+
const ownsDir = (file, { roots, pathLoaded }) => roots?.has(file) === true || pathLoaded?.has(file) === true
|
|
110
1005
|
|
|
1006
|
+
function firstFile(candidates, { knownSources, baseDir }) {
|
|
111
1007
|
if (knownSources) return candidates.find((c) => knownSources.has(c)) ?? null
|
|
112
|
-
if (baseDir)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1008
|
+
if (baseDir) return candidates.find((c) => isFile(join(baseDir, c))) ?? null
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Resolve `mod <name>;` declared in `fromFile` (inside the inline modules `inlinePath`) to its
|
|
1013
|
+
// file, trying `<dir>/<name>.rs` then `<dir>/<name>/mod.rs`. `roots` are the files walked as crate
|
|
1014
|
+
// roots (the entries) and `pathLoaded` the files a `#[path]` loaded: their submodules are
|
|
1015
|
+
// siblings. Both also get the non-root `<stem>/` rule as a fallback: a glob-listed module file
|
|
1016
|
+
// still resolves, and so does a file that a plain `mod` mounts as well as a `#[path]` (each mount
|
|
1017
|
+
// is its own module to rustc; the loader keeps one resolution, whichever file exists).
|
|
1018
|
+
export function resolveModPath(modName, fromFile, { knownSources, baseDir, roots, pathLoaded, inlinePath = [], boundary = null } = {}) {
|
|
1019
|
+
const owns = ownsDir(fromFile, { roots, pathLoaded })
|
|
1020
|
+
const dirs = [getModuleDir(fromFile, { root: owns })]
|
|
1021
|
+
if (owns) dirs.push(getModuleDir(fromFile)) // the same dir for main.rs/lib.rs/mod.rs
|
|
1022
|
+
for (const dir of new Set(dirs)) {
|
|
1023
|
+
// An inline module's `#[path = "…"]` may climb (`..`): normalized, and held to `boundary`
|
|
1024
|
+
// like any explicit path (resolveExplicitModPath).
|
|
1025
|
+
const base = normalizeRel(dir, [...inlinePath, modName].filter(Boolean).join('/'))
|
|
1026
|
+
if (base === null || (boundary !== null && !withinDir(base, boundary))) continue
|
|
1027
|
+
const found = firstFile([`${base}.rs`, `${base}/mod.rs`], { knownSources, baseDir })
|
|
1028
|
+
if (found) return found
|
|
118
1029
|
}
|
|
119
1030
|
return null
|
|
120
1031
|
}
|
|
121
1032
|
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
1033
|
+
// Resolve a `#[path = "…"]` target the way rustc does: relative to the declaring file's directory,
|
|
1034
|
+
// or -- inside inline modules -- to the file's module dir plus the inline module names. Absolute
|
|
1035
|
+
// or root-escaping paths resolve to nothing (a read would refuse them anyway).
|
|
1036
|
+
export function resolveExplicitModPath(explicitPath, fromFile, { knownSources, baseDir, roots, pathLoaded, inlinePath = [], boundary = null } = {}) {
|
|
1037
|
+
const dir = inlinePath.length === 0
|
|
1038
|
+
? posix.dirname(fromFile)
|
|
1039
|
+
: [getModuleDir(fromFile, { root: ownsDir(fromFile, { roots, pathLoaded }) }), ...inlinePath].filter(Boolean).join('/')
|
|
1040
|
+
const rel = normalizeRel(dir, explicitPath)
|
|
1041
|
+
if (rel === null || (boundary !== null && !withinDir(rel, boundary))) return null
|
|
1042
|
+
return firstFile([rel], { knownSources, baseDir })
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Whether project-relative `rel` lies in directory `dir` (`.`: anywhere in the bundle root).
|
|
1046
|
+
const withinDir = (rel, dir) => dir === '.' || rel === dir || rel.startsWith(`${dir}/`)
|
|
1047
|
+
|
|
1048
|
+
// Whether the file at project-relative `rel` really lies in directory `within` (`.`: anywhere in
|
|
1049
|
+
// the bundle root, which assertRealPathWithinBase holds it to): a vendored crate's file linked out
|
|
1050
|
+
// of its package -- to the project's `.env`, say -- doesn't, and is refused like an include naming
|
|
1051
|
+
// the target outright, warned as `who`'s. `realDirs` keeps the directories' real paths.
|
|
1052
|
+
export function withinRealDir(baseDir, rel, within, { who = 'loader.rust', realDirs = new Map() } = {}) {
|
|
1053
|
+
if (within === '.') return true
|
|
1054
|
+
if (!realDirs.has(within)) realDirs.set(within, realpathSync(join(baseDir, within)))
|
|
1055
|
+
const back = toPosix(relative(realDirs.get(within), realpathSync(join(baseDir, rel))))
|
|
1056
|
+
if (!relativeEscapes(back)) return true
|
|
1057
|
+
console.warn(`[${who}] Refusing file outside its package: ${rel} (a link out of ${within})`)
|
|
1058
|
+
return false
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// The directory a file's `#[path]`, include and build-script paths may reach into: a published
|
|
1062
|
+
// crate never legitimately names a file outside its own package, so a file of a vendored one is
|
|
1063
|
+
// held to that package's directory (`vendor/<crate>`); the project's own code -- the root package,
|
|
1064
|
+
// workspace members, path dependencies -- may reach anywhere in the bundle root (`.`).
|
|
1065
|
+
export function boundaryOf(file, ctx) {
|
|
1066
|
+
return ctx?.isVendored(file) === true ? ctx.packageInfo(file).dir : '.'
|
|
1067
|
+
}
|
|
125
1068
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1069
|
+
// Resolve an include macro's argument (scanRustItems' `{ kind, path, base }`) named in `file` to a
|
|
1070
|
+
// project-relative path: relative to the file, or to its package's root for a
|
|
1071
|
+
// `concat!(env!("CARGO_MANIFEST_DIR"), …)` path; null when it escapes the bundle root, or the
|
|
1072
|
+
// package when the file is a vendored crate's (boundaryOf).
|
|
1073
|
+
function includeTarget(inc, file, ctx) {
|
|
1074
|
+
const dir = inc.base === 'manifest' ? ctx?.packageInfo(file)?.dir ?? null : posix.dirname(file)
|
|
1075
|
+
if (dir === null) return null
|
|
1076
|
+
const rel = normalizeRel(dir, inc.path)
|
|
1077
|
+
if (rel === null || !withinDir(rel, boundaryOf(file, ctx))) {
|
|
1078
|
+
console.warn(`[loader.rust] Refusing include outside its package: ${inc.path} from ${file}`)
|
|
1079
|
+
return null
|
|
1080
|
+
}
|
|
1081
|
+
return rel
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Every file a `mod` declaration can denote, as `{ cfg, file, explicit }`: an unconditional
|
|
1085
|
+
// `#[path]` names one outright; otherwise each `#[cfg_attr(<pred>, path = …)]` names one under
|
|
1086
|
+
// its predicate (`cfg`), and the default `<name>.rs`/`<name>/mod.rs` lookup is the fallback (`cfg`
|
|
1087
|
+
// null); the scanner has dropped those whose predicate can't hold in the build it scanned under.
|
|
1088
|
+
// `explicit` marks a file a `#[path]` named (its own submodules then sit beside it). The inline
|
|
1089
|
+
// modules the declaration sits in contribute their directories (`inlineDirs`: an inline module's
|
|
1090
|
+
// own `#[path]` replaces its name, an empty one adds nothing), else their names.
|
|
1091
|
+
export function resolveModDecl(decl, fromFile, opts = {}) {
|
|
1092
|
+
const o = { ...opts, inlinePath: decl.inlineDirs ?? decl.inlinePath }
|
|
1093
|
+
const unconditional = decl.paths.find((p) => p.cfg === null)
|
|
1094
|
+
if (unconditional) {
|
|
1095
|
+
const file = resolveExplicitModPath(unconditional.path, fromFile, o)
|
|
1096
|
+
return file ? [{ cfg: null, file, explicit: true }] : []
|
|
1097
|
+
}
|
|
1098
|
+
const out = []
|
|
1099
|
+
for (const { path, cfg } of decl.paths) {
|
|
1100
|
+
const file = resolveExplicitModPath(path, fromFile, o)
|
|
1101
|
+
if (file && !out.some((x) => x.file === file)) out.push({ cfg, file, explicit: true })
|
|
1102
|
+
}
|
|
1103
|
+
// A variant that always applies (`decl.noDefault`) leaves no build for the default lookup.
|
|
1104
|
+
const fallback = decl.noDefault === true ? null : resolveModPath(decl.name, fromFile, o)
|
|
1105
|
+
if (fallback && !out.some((x) => x.file === fallback)) out.push({ cfg: null, file: fallback, explicit: false })
|
|
1106
|
+
return out
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Resolve a crate name to its vendored root (`vendor/<dir>/src/lib.rs`) among already-loaded
|
|
1110
|
+
// sources, or null. `use` names underscore but the vendor dir may hyphenate, so try both.
|
|
1111
|
+
export function resolveVendoredCrate(crateName, { knownSources } = {}) {
|
|
1112
|
+
const norm = normName(crateName)
|
|
1113
|
+
for (const dir of norm.includes('_') ? [norm, norm.replaceAll('_', '-')] : [norm]) {
|
|
1114
|
+
const lib = `${VENDOR_DIR}/${dir}/src/lib.rs`
|
|
1115
|
+
if (knownSources?.has(lib)) return lib
|
|
1116
|
+
}
|
|
1117
|
+
return null
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// --- Module trees -----------------------------------------------------------------------
|
|
1121
|
+
|
|
1122
|
+
// The files walked as crate roots: every file named main.rs/lib.rs (a vendored crate's root, the
|
|
1123
|
+
// lib beside a bin, …), every file a manifest names as its lib target (`[lib] path =
|
|
1124
|
+
// "src/other.rs"`, given a Cargo context), then every explicit root (entry) that was loaded. Roots
|
|
1125
|
+
// by name/manifest come first so their trees claim the files they reach (buildModuleTrees) before
|
|
1126
|
+
// a glob-listed module file, an entry only by role, claims itself.
|
|
1127
|
+
export function crateRoots(sources, explicit = [], cargo = null) {
|
|
1128
|
+
const roots = new Set()
|
|
1129
|
+
for (const path of sources.keys()) {
|
|
1130
|
+
if (isNamedRoot(path) || cargo?.isLibRoot(path) === true) roots.add(path)
|
|
1131
|
+
}
|
|
1132
|
+
for (const r of explicit) if (sources.has(r)) roots.add(r)
|
|
1133
|
+
return roots
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// One module tree per crate root -- `trees`: root file → Map<modulePath, file> -- built by
|
|
1137
|
+
// following `mod` edges breadth-first from `crate`, plus `files`: file → `{ root, modulePath }`
|
|
1138
|
+
// (first root to reach a file claims it). A file is reached under one module path per `mod` spec
|
|
1139
|
+
// (`mod a::b` for a declaration inside inline module `a`); a cfg-variant Map target contributes
|
|
1140
|
+
// every variant under the same path, the first naming the module's file in the tree.
|
|
1141
|
+
// `inlineModules` (file → the paths of its inline `mod x { … }` blocks, from scanRustItems) puts
|
|
1142
|
+
// those modules in the tree too, owned by the file holding them -- unless a `mod` of the same
|
|
1143
|
+
// name names a file (`#[cfg(unix)] mod imp;` beside `#[cfg(not(unix))] mod imp { … }`): the file
|
|
1144
|
+
// is the more telling target, the inline body's is where the path was written anyway. Each file's
|
|
1145
|
+
// entry also carries `leaves`, the cfg leaves (cfgLeaves) of every `mod` on the way down to it --
|
|
1146
|
+
// the declaration's own cfg and the gate macro it sits in (`mountCfgs`: file → spec → target
|
|
1147
|
+
// file → its declarations' `{ cfg, macro }`; a file declared under several is compiled under any
|
|
1148
|
+
// one of them: an `any` leaf, see cfgLeaves and gateLeaves; `gatesOf`: file → the gate macros
|
|
1149
|
+
// of its package, see gateLeaves) and, for a `mod` with several files, a `variant` leaf naming
|
|
1150
|
+
// which (two variants of one `mod` never both apply) -- `parent`, the file that mounted it, and
|
|
1151
|
+
// `roots`, every crate root whose tree reaches it (a file a build script and its lib share).
|
|
1152
|
+
export function buildModuleTrees(sources, resolutions, roots, inlineModules = null, mountCfgs = null, gatesOf = null) {
|
|
1153
|
+
const trees = new Map()
|
|
1154
|
+
const files = new Map()
|
|
1155
|
+
const variantsOf = (target) => (target instanceof Map ? [...target] : [['*', target]])
|
|
1156
|
+
const mountLeaves = (decls, path) => (decls === undefined ? [] : anyOf(decls.map(({ cfg, macro, via }) => [...cfgLeaves(cfg), ...gateLeaves(macro, gatesOf?.(path)), ...mountLeaves(via, path)])))
|
|
1157
|
+
const mounted = (path, spec, sub, key, file, several, leaves) => [
|
|
1158
|
+
...leaves,
|
|
1159
|
+
...mountLeaves(mountCfgs?.get(path)?.get(spec)?.get(file), path),
|
|
1160
|
+
...(several ? [{ key: 'variant', site: sub, value: key.replace(/#\d+$/u, ''), neg: false }] : []),
|
|
1161
|
+
]
|
|
1162
|
+
for (const root of roots) {
|
|
1163
|
+
if (!sources.has(root)) continue
|
|
1164
|
+
const tree = new Map([['crate', root]])
|
|
1165
|
+
trees.set(root, tree)
|
|
133
1166
|
const seen = new Set()
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const [path, modulePath, depth] =
|
|
137
|
-
|
|
1167
|
+
const queue = [[root, 'crate', 0, [], null]]
|
|
1168
|
+
for (let qi = 0; qi < queue.length; qi++) {
|
|
1169
|
+
const [path, modulePath, depth, leaves, parent] = queue[qi]
|
|
1170
|
+
if (!files.has(path)) files.set(path, { root, modulePath, leaves, parent, roots: new Set() })
|
|
1171
|
+
files.get(path).roots.add(root)
|
|
138
1172
|
if (seen.has(path) || depth >= MAX_MODULE_DEPTH) continue
|
|
139
1173
|
seen.add(path)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1174
|
+
// `spec`'s module `sub` mounted on each file of `target` (its variants); the tree keeps the
|
|
1175
|
+
// first.
|
|
1176
|
+
const mount = (spec, sub, target) => {
|
|
1177
|
+
const variants = variantsOf(target)
|
|
1178
|
+
for (const [key, file] of variants) {
|
|
1179
|
+
if (!tree.has(sub)) tree.set(sub, file)
|
|
1180
|
+
queue.push([file, sub, depth + 1, mounted(path, spec, sub, key, file, variants.length > 1, leaves), path])
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// Each prefix of `segments` (`a`, `a::b`, …) below this module: an inline module whose body
|
|
1184
|
+
// lives in this file.
|
|
1185
|
+
const claimInline = (segments) => {
|
|
1186
|
+
for (let k = 1; k <= segments.length; k++) {
|
|
1187
|
+
const key = `${modulePath}::${segments.slice(0, k).join('::')}`
|
|
1188
|
+
if (!tree.has(key)) tree.set(key, path)
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// Module files first, then the inline modules that hold nested declarations, then the other
|
|
1192
|
+
// inline modules: a `mod imp;` file beats a `mod imp { … }` of the same name however the
|
|
1193
|
+
// declarations are ordered.
|
|
1194
|
+
const specs = [...(resolutions.get(path) ?? [])]
|
|
1195
|
+
for (const [spec, target] of specs) {
|
|
143
1196
|
if (!spec.startsWith('mod ')) continue
|
|
144
|
-
|
|
1197
|
+
const parts = spec.slice(4).split('::')
|
|
1198
|
+
if (parts.length > 1) continue
|
|
1199
|
+
mount(spec, `${modulePath}::${parts[0]}`, target)
|
|
145
1200
|
}
|
|
1201
|
+
for (const [spec, target] of specs) {
|
|
1202
|
+
// An `include!`d file's tokens are this module's: it is walked under the same module path.
|
|
1203
|
+
if (spec.startsWith('include ')) {
|
|
1204
|
+
queue.push([target, modulePath, depth + 1, leaves, path])
|
|
1205
|
+
continue
|
|
1206
|
+
}
|
|
1207
|
+
if (!spec.startsWith('mod ')) continue
|
|
1208
|
+
const parts = spec.slice(4).split('::')
|
|
1209
|
+
if (parts.length === 1) continue
|
|
1210
|
+
// `mod a::b::name`: `a` and `a::b` are inline modules whose bodies live in this file, so
|
|
1211
|
+
// paths into them (`a::b::x`, `super::` from `name`) resolve to it.
|
|
1212
|
+
claimInline(parts.slice(0, -1))
|
|
1213
|
+
mount(spec, `${modulePath}::${spec.slice(4)}`, target)
|
|
1214
|
+
}
|
|
1215
|
+
for (const inline of inlineModules?.get(path) ?? []) claimInline(inline)
|
|
146
1216
|
}
|
|
147
1217
|
}
|
|
148
|
-
return
|
|
1218
|
+
return { trees, files }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// The crate a path's lead may name: null for a path keyword or a sysroot crate, and -- unless the
|
|
1222
|
+
// path is absolute (`::name::…`) -- for a name the file imported (`bindings`: `use std::io;
|
|
1223
|
+
// io::stdin()` names no crate `io`).
|
|
1224
|
+
const crateLead = ({ segments, absolute }, bindings) => {
|
|
1225
|
+
const head = segments[0]
|
|
1226
|
+
return head === undefined || NON_CRATE_LEADS.has(head) || (!absolute && bindings.has(head)) ? null : head
|
|
149
1227
|
}
|
|
150
1228
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
1229
|
+
// --- Path resolution ------------------------------------------------------------------------
|
|
1230
|
+
//
|
|
1231
|
+
// A path is resolved against its crate's module tree, module by module. Where a segment names no
|
|
1232
|
+
// module of the one reached, the imports written in that module may provide the name: a `use`
|
|
1233
|
+
// binding it outright, or a glob (`pub use inner::*`) when the module the glob names -- one of
|
|
1234
|
+
// this crate -- provides it in turn. What a name leads to is a property of (module, name) and of
|
|
1235
|
+
// how much of the module's imports the asking module may see, so it is computed once per crate
|
|
1236
|
+
// and shared by every path resolved (`ctx.provided`): resolution stays linear in the number of
|
|
1237
|
+
// paths, whatever the fan-out of the crate's re-exports.
|
|
1238
|
+
//
|
|
1239
|
+
// Results: `{ kind: 'module', modulePath, file }` for a module of the crate; `{ kind: 'item',
|
|
1240
|
+
// file }` for anything else the crate's files hold (an item of the module reached, a name a
|
|
1241
|
+
// `use` brought in from the sysroot -- as far as the bundle is concerned it lives where the `use`
|
|
1242
|
+
// is); `{ kind: 'crate', name, file }` for another in-tree crate; null when nothing here answers.
|
|
1243
|
+
// A result that went through an import at the top level carries `via: { modulePath, consumed,
|
|
1244
|
+
// through }`: the module whose import was followed first, how many of the path's own segments
|
|
1245
|
+
// named modules up to it, and the file holding that import.
|
|
1246
|
+
|
|
1247
|
+
// The module a visibility (scanRustItems' `vis`) written in module `modulePath` reaches: the
|
|
1248
|
+
// crate root for `pub` / `pub(crate)`, the module itself for a private item or `pub(self)`, its
|
|
1249
|
+
// parent for `pub(super)`, the ancestor a `pub(in …)` names (`crate::a`, `super::…`, `self`) --
|
|
1250
|
+
// when it is one; rustc rejects any other, so that reads as `crate`.
|
|
1251
|
+
function visibilityScope(vis, modulePath) {
|
|
1252
|
+
if (vis === null || vis === 'self') return modulePath
|
|
1253
|
+
if (vis === 'crate') return 'crate'
|
|
1254
|
+
const parent = modulePath.includes('::') ? modulePath.slice(0, modulePath.lastIndexOf('::')) : 'crate'
|
|
1255
|
+
if (vis === 'super') return parent
|
|
1256
|
+
const segments = vis.slice(3).split('::')
|
|
1257
|
+
let cur
|
|
1258
|
+
if (segments[0] === 'crate') cur = ['crate']
|
|
1259
|
+
else if (segments[0] === 'self') cur = modulePath.split('::')
|
|
1260
|
+
else if (segments[0] === 'super') cur = parent.split('::')
|
|
1261
|
+
else return 'crate'
|
|
1262
|
+
let i = 1
|
|
1263
|
+
while (segments[i] === 'super' && cur.length > 1) {
|
|
1264
|
+
cur.pop()
|
|
1265
|
+
i++
|
|
158
1266
|
}
|
|
159
|
-
|
|
1267
|
+
const scope = [...cur, ...segments.slice(i)].join('::')
|
|
1268
|
+
return modulePath === scope || modulePath.startsWith(`${scope}::`) ? scope : 'crate'
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// --- cfg compatibility -----------------------------------------------------------------------
|
|
1272
|
+
//
|
|
1273
|
+
// Which of a module's several files, imports or glob sources a path written under some cfgs can
|
|
1274
|
+
// mean: libc's `crate::c_int` from a file under `#[cfg(unix)] mod unix;` is not the one
|
|
1275
|
+
// `#[cfg(target_os = "fuchsia")] pub use fuchsia::*;` brings in, mio's `sys::Waker` from a file in
|
|
1276
|
+
// one `cfg_if!` branch is not the `pub use` in another. Each file carries the leaves of the cfgs
|
|
1277
|
+
// it was mounted under (buildModuleTrees), each import and item those of its own cfg on top; two
|
|
1278
|
+
// leaf sets that can't hold together are exclusive, and such a candidate is skipped.
|
|
1279
|
+
|
|
1280
|
+
// The conjunctive leaves of a cfg predicate, `{ key, value, neg }`: `all(unix, not(feature =
|
|
1281
|
+
// "std"))` → `unix` and `!feature="std"`; `not(any(a, b))` → `!a`, `!b`. An `any(…)` of
|
|
1282
|
+
// alternatives that each decide something is one leaf, `{ key: 'any', alts }`, holding when any
|
|
1283
|
+
// alternative (a conjunction of leaves) does -- `any(target_os = "linux", target_os = "l4re")`
|
|
1284
|
+
// can't hold along with `target_os = "aix"` -- as is a negated `all(…)`; one with an alternative
|
|
1285
|
+
// that decides nothing (`any(unix, libc_core_cvoid)`) decides nothing itself.
|
|
1286
|
+
function cfgLeaves(pred) {
|
|
1287
|
+
if (pred === null || pred === undefined) return []
|
|
1288
|
+
const p = pred.trim()
|
|
1289
|
+
const m = /^(all|any|not)\s*\(([\s\S]*)\)$/u.exec(p)
|
|
1290
|
+
if (!m) {
|
|
1291
|
+
if (p === 'true') return [] // holds: decides nothing (`false` is a leaf the build rules out, see leafFalse)
|
|
1292
|
+
const kv = /^((?:r#)?[\w-]+)\s*(?:=\s*"([^"]*)")?$/u.exec(p)
|
|
1293
|
+
return kv ? [{ key: cfgName(kv[1]), value: kv[2] ?? null, neg: false }] : []
|
|
1294
|
+
}
|
|
1295
|
+
const parts = splitTopLevel(m[2]).filter((part) => part.trim() !== '').map(cfgLeaves)
|
|
1296
|
+
if (m[1] === 'all') return parts.flat()
|
|
1297
|
+
if (m[1] === 'any') return anyOf(parts)
|
|
1298
|
+
return parts.length === 1 ? negated(parts[0]) : []
|
|
1299
|
+
}
|
|
1300
|
+
// The leaves holding when any of `alts` (conjunctions) does: the one alternative when there is
|
|
1301
|
+
// one (the same twice counts once, and one holding whenever another does adds nothing: `a ∨ (a ∧
|
|
1302
|
+
// b)` is `a`), an `any` leaf of them, nothing when one decides nothing -- or, for more than
|
|
1303
|
+
// MAX_ALTERNATIVES (a module reached along every path of a dense glob cycle, a cfg listing a
|
|
1304
|
+
// score of platforms) or one undecided already, the undecided leaf: too many to tell apart along
|
|
1305
|
+
// every path, and never to be taken for holding (see UNDECIDED_KEY).
|
|
1306
|
+
const MAX_ALTERNATIVES = 16
|
|
1307
|
+
const anyOf = (alts) => {
|
|
1308
|
+
const uniq = new Map()
|
|
1309
|
+
for (const alt of alts) {
|
|
1310
|
+
if (alt.length === 0) return []
|
|
1311
|
+
uniq.set(alt.map(leafKey).toSorted().join('&'), alt)
|
|
1312
|
+
}
|
|
1313
|
+
let list = [...uniq.values()]
|
|
1314
|
+
if (list.length > 1) {
|
|
1315
|
+
const keys = list.map((alt) => new Set(alt.map(leafKey)))
|
|
1316
|
+
list = list.filter((_, i) => !keys.some((other, j) => j !== i && other.size < keys[i].size && other.isSubsetOf(keys[i])))
|
|
1317
|
+
}
|
|
1318
|
+
if (list.length > 1 && (list.length > MAX_ALTERNATIVES || list.some((alt) => alt.some((l) => l.many !== undefined)))) return [{ key: UNDECIDED_KEY, value: null, neg: false, many: list }]
|
|
1319
|
+
if (list.length <= 1) return list[0] ?? []
|
|
1320
|
+
return [{ key: 'any', value: null, neg: false, alts: list }]
|
|
1321
|
+
}
|
|
1322
|
+
// The key of the leaf an `any` the loader doesn't follow is kept as (`many`: its alternatives,
|
|
1323
|
+
// for its cfg text, see leafText): one no build decides (leafFalse), no asker holds -- itself
|
|
1324
|
+
// under one included (holds) -- that is no custom cfg (doubtful) and never exclusive with
|
|
1325
|
+
// anything (prepared), its negation too. So a candidate under it is only maybe the answer, beside
|
|
1326
|
+
// the others (withAlternatives), and never certain; and an `any` with one among its alternatives
|
|
1327
|
+
// is undecided too, which keeps the sets along a dense glob cycle few.
|
|
1328
|
+
const UNDECIDED_KEY = 'any(…)!'
|
|
1329
|
+
// The leaves of a conjunction's negation: one leaf flips, an `any` leaf's negation is every
|
|
1330
|
+
// alternative's, several leaves negate to any of their negations.
|
|
1331
|
+
const negated = (leaves) => {
|
|
1332
|
+
if (leaves.length === 0) return []
|
|
1333
|
+
if (leaves.length > 1) return anyOf(leaves.map((l) => negated([l])))
|
|
1334
|
+
const [l] = leaves
|
|
1335
|
+
return l.alts === undefined ? [{ ...l, neg: !l.neg }] : l.alts.flatMap(negated)
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// A cfg gate macro: `cfg_<x>!` (tokio's `cfg_io_uring! { … }`, mio's `cfg_os_poll! { … }`)
|
|
1339
|
+
// wraps each item of its body in the cfg its definition writes -- `($($item:item)*) => { $(
|
|
1340
|
+
// #[cfg(feature = "rt")] $item )* }` -- read from that definition (gatePredicate); `cfg_if!` is
|
|
1341
|
+
// read branch by branch, any other macro's body is no gate.
|
|
1342
|
+
const GATE_MACRO_RE = /^cfg_(?!if$)\w+$/u
|
|
1343
|
+
// The cfg a gate macro's definition puts on what it wraps: the `#[cfg(…)]` attributes of its
|
|
1344
|
+
// transcribers (their conjunction), when every arm writes the same ones; null when an arm writes
|
|
1345
|
+
// none or the arms differ -- a gate the loader can't see into. `code` and `masked` are the lexed
|
|
1346
|
+
// views, `open`/`close` the braces of the `macro_rules!` body.
|
|
1347
|
+
function gatePredicate(code, masked, open, close) {
|
|
1348
|
+
let seen = null
|
|
1349
|
+
for (let i = masked.indexOf('=>', open); i !== -1 && i < close; i = masked.indexOf('=>', i + 2)) {
|
|
1350
|
+
let start = i + 2
|
|
1351
|
+
while (/\s/u.test(masked[start] ?? '')) start++
|
|
1352
|
+
if (!'{(['.includes(masked[start] ?? ' ')) continue
|
|
1353
|
+
const end = matchClose(masked, start)
|
|
1354
|
+
const preds = []
|
|
1355
|
+
for (const m of masked.slice(start, end).matchAll(/#\s*\[\s*cfg\s*\(/gu)) {
|
|
1356
|
+
const paren = start + m.index + m[0].length - 1
|
|
1357
|
+
preds.push(normalizeCfg(code.slice(paren + 1, matchClose(masked, paren))))
|
|
1358
|
+
}
|
|
1359
|
+
if (preds.length === 0) return null
|
|
1360
|
+
const pred = joinCfgs([...new Set(preds)].toSorted())
|
|
1361
|
+
if (seen !== null && seen !== pred) return null
|
|
1362
|
+
seen = pred
|
|
1363
|
+
i = end
|
|
1364
|
+
}
|
|
1365
|
+
return seen
|
|
160
1366
|
}
|
|
1367
|
+
// The leaves of what a gate macro's body declares: its definition's cfg (`gates`: macro name →
|
|
1368
|
+
// predicate, null when unreadable, see gatePredicate), else a leaf of its own name -- which holds
|
|
1369
|
+
// along with anything and is never certain: `cfg_x!` and `cfg_not_x!` are not taken for each
|
|
1370
|
+
// other's negation by their names alone.
|
|
1371
|
+
function gateLeaves(macro, gates = null) {
|
|
1372
|
+
if (macro === null || macro === undefined || !GATE_MACRO_RE.test(macro)) return []
|
|
1373
|
+
const pred = gates?.get(macro)
|
|
1374
|
+
return typeof pred === 'string' ? cfgLeaves(pred) : [{ key: `${macro}!`, value: null, neg: false }]
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Keys a target has one value of: `target_os = "linux"` and `target_os = "macos"` never both hold
|
|
1378
|
+
// (`feature`, `target_family` and `target_has_atomic` may take several).
|
|
1379
|
+
const SINGLE_VALUED_CFG_KEYS = new Set(['target_os', 'target_arch', 'target_env', 'target_vendor', 'target_abi', 'target_pointer_width', 'target_endian', 'panic'])
|
|
1380
|
+
// Operating systems whose targets are not `unix`.
|
|
1381
|
+
const NON_UNIX_OS = new Set(['windows', 'none', 'uefi', 'wasi', 'solana', 'cuda', 'xous', 'zkvm', 'psp', 'unknown'])
|
|
1382
|
+
const isWindowsLeaf = (l) => (l.key === 'windows' && l.value === null) || ((l.key === 'target_os' || l.key === 'target_family') && l.value === 'windows')
|
|
1383
|
+
const isUnixLeaf = (l) => (l.key === 'unix' && l.value === null) || (l.key === 'target_family' && l.value === 'unix')
|
|
1384
|
+
|
|
1385
|
+
// Whether two leaf lists (conjunctions) can't hold at once: a leaf and its negation; two values of
|
|
1386
|
+
// a single-valued key; two variants of one `mod` (`variant` leaves, see buildModuleTrees); Windows
|
|
1387
|
+
// against unix; an `any` leaf none of whose alternatives can hold with the other. Asked once per
|
|
1388
|
+
// pair of leaf sets (compatible), over what `prepared` gathers of each list.
|
|
1389
|
+
function cfgExclusive(a, b) {
|
|
1390
|
+
const pa = prepared(a)
|
|
1391
|
+
const pb = prepared(b)
|
|
1392
|
+
for (const k of pa.negKeys) if (pb.keys.has(k)) return true
|
|
1393
|
+
for (const [k, v] of pa.values) {
|
|
1394
|
+
const w = pb.values.get(k)
|
|
1395
|
+
if (w !== undefined && w !== v) return true
|
|
1396
|
+
}
|
|
1397
|
+
if ((pa.windows && (pb.unix || pb.otherOs)) || (pb.windows && (pa.unix || pa.otherOs)) || (pa.unix && pb.nonUnixOs) || (pb.unix && pa.nonUnixOs)) return true
|
|
1398
|
+
for (const x of pa.anys) if (x.alts.every((alt) => cfgExclusive(alt, b))) return true
|
|
1399
|
+
for (const y of pb.anys) if (y.alts.every((alt) => cfgExclusive(a, alt))) return true
|
|
1400
|
+
return false
|
|
1401
|
+
}
|
|
1402
|
+
// What cfgExclusive asks of a leaf list, gathered once and kept on it: its plain leaves' keys and
|
|
1403
|
+
// their negations', the value each positive single-valued key (and `mod` variant site) takes --
|
|
1404
|
+
// `MANY` for several, which nothing holds along with -- its Windows / unix standing (a
|
|
1405
|
+
// `target_os` other than windows, `otherOs`; one of the non-unix ones, `nonUnixOs`) and its `any`
|
|
1406
|
+
// leaves.
|
|
1407
|
+
const MANY = Symbol('many')
|
|
1408
|
+
function prepared(leaves) {
|
|
1409
|
+
if (leaves.prep !== undefined) return leaves.prep
|
|
1410
|
+
const prep = { keys: new Set(), negKeys: [], values: new Map(), anys: [], windows: false, unix: false, otherOs: false, nonUnixOs: false }
|
|
1411
|
+
for (const l of leaves) {
|
|
1412
|
+
if (l.alts !== undefined) {
|
|
1413
|
+
prep.anys.push(l)
|
|
1414
|
+
continue
|
|
1415
|
+
}
|
|
1416
|
+
if (l.many !== undefined) continue // undecided: exclusive with nothing
|
|
1417
|
+
const k = leafKey(l)
|
|
1418
|
+
prep.keys.add(k)
|
|
1419
|
+
prep.negKeys.push(l.neg ? k.slice(1) : `!${k}`)
|
|
1420
|
+
if (l.neg) continue
|
|
1421
|
+
if (l.value !== null && (SINGLE_VALUED_CFG_KEYS.has(l.key) || l.key === 'variant')) {
|
|
1422
|
+
const site = l.key === 'variant' ? `variant@${l.site}` : l.key
|
|
1423
|
+
prep.values.set(site, prep.values.has(site) && prep.values.get(site) !== l.value ? MANY : l.value)
|
|
1424
|
+
}
|
|
1425
|
+
if (isWindowsLeaf(l)) prep.windows = true
|
|
1426
|
+
if (isUnixLeaf(l)) prep.unix = true
|
|
1427
|
+
if (l.key === 'target_os' && l.value !== null) {
|
|
1428
|
+
if (l.value !== 'windows') prep.otherOs = true
|
|
1429
|
+
if (NON_UNIX_OS.has(l.value)) prep.nonUnixOs = true
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
leaves.prep = prep
|
|
1433
|
+
return prep
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// A stable text for one leaf; an `any` leaf's names its alternatives. Kept per leaf: an `any`
|
|
1437
|
+
// leaf's is asked for again and again as sets of them are interned and compared.
|
|
1438
|
+
const leafKeys = new WeakMap()
|
|
1439
|
+
const leafKey = (l) => cached(leafKeys, l, () => (l.alts === undefined ? `${l.neg ? '!' : ''}${l.key}${l.site === undefined ? '' : `@${l.site}`}${l.value === null ? '' : `=${l.value}`}` : `any(${l.alts.map((alt) => alt.map(leafKey).toSorted().join('&')).toSorted().join(',')})`))
|
|
1440
|
+
|
|
1441
|
+
// Leaf sets are interned per tree (`sets`: key → `{ leaves, key }`), one object per distinct set:
|
|
1442
|
+
// the union of the cfgs along a chain of globs stays small however long the chain, and the
|
|
1443
|
+
// compatibility verdict for a pair of sets is looked up by key, not computed again.
|
|
1444
|
+
const NO_LEAVES = { leaves: [], key: '' }
|
|
1445
|
+
function leafSet(leaves, sets) {
|
|
1446
|
+
if (leaves.length === 0) return NO_LEAVES
|
|
1447
|
+
const uniq = new Map()
|
|
1448
|
+
for (const l of leaves) uniq.set(leafKey(l), l)
|
|
1449
|
+
const key = [...uniq.keys()].toSorted().join('|')
|
|
1450
|
+
return sets.get(key) ?? sets.set(key, { leaves: [...uniq.values()], key }).get(key)
|
|
1451
|
+
}
|
|
1452
|
+
// A union is asked for again and again along a closure (a glob's leaves with each candidate's): kept
|
|
1453
|
+
// on the first set, by the second's key.
|
|
1454
|
+
const unionLeaves = (a, b, sets) => {
|
|
1455
|
+
if (a.key === '') return b
|
|
1456
|
+
if (b.key === '' || b.key === a.key) return a
|
|
1457
|
+
const unions = (a.unions ??= new Map())
|
|
1458
|
+
return unions.get(b.key) ?? unions.set(b.key, leafSet([...a.leaves, ...b.leaves], sets)).get(b.key)
|
|
1459
|
+
}
|
|
1460
|
+
// The leaves holding when either of two sets does (a module two glob paths reach): an `any` of
|
|
1461
|
+
// the two -- of their alternatives, when one is an `any` leaf already.
|
|
1462
|
+
const eitherLeaves = (a, b, sets) => {
|
|
1463
|
+
if (a.key === b.key) return a
|
|
1464
|
+
if (a.key === '' || b.key === '') return NO_LEAVES
|
|
1465
|
+
const altsOf = (s) => (s.leaves.length === 1 && s.leaves[0].alts !== undefined ? s.leaves[0].alts : [s.leaves])
|
|
1466
|
+
return leafSet(anyOf([...altsOf(a), ...altsOf(b)]), sets)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// The leaf set of an `asker` -- `{ file, set, keys, compat, sure, doubt }`, the file a path was
|
|
1470
|
+
// written in, its set the leaves it was mounted under plus, when the build's target is known, the
|
|
1471
|
+
// target's cfgs (`unix`, `target_os = "linux"`, …: what every file of the build is compiled
|
|
1472
|
+
// under) -- against a candidate's: exclusive candidates are skipped; no asker (an internal query)
|
|
1473
|
+
// skips nothing. The verdict is kept per pair of sets in `compat`, shared by the askers of one set.
|
|
1474
|
+
const compatible = (asker, set) => asker === undefined || set.key === '' || asker.contradictory || cached(asker.compat, set.key, () => !cfgExclusive(asker.set.leaves, set.leaves))
|
|
1475
|
+
// Whether a leaf list can't hold in the asker's build (scanRustItems' build, see buildOf): one of
|
|
1476
|
+
// its leaves the build decides is false -- a feature that is off, a platform the build isn't --
|
|
1477
|
+
// or an `any` leaf each of whose alternatives is. What a gate macro's cfg wraps (tokio's
|
|
1478
|
+
// `cfg_rt! { … }` with `rt` off) is then taken last (`dead`, see pick): after any candidate the
|
|
1479
|
+
// build may compile, and only when there is none -- a path written behind the same gate, whose
|
|
1480
|
+
// code the build doesn't compile either, means what it would mean where it compiles. For an
|
|
1481
|
+
// asker whose own leaves can't hold (`deadHere`) the build says nothing.
|
|
1482
|
+
const leafFalse = (l, build) => {
|
|
1483
|
+
if (l.alts !== undefined) return l.alts.every((alt) => alt.some((m) => leafFalse(m, build)))
|
|
1484
|
+
if (l.key === 'false' && l.value === null) return !l.neg // the literal (cfgLeaves)
|
|
1485
|
+
const verdict = evalCfgKey(l.key, l.value, build)
|
|
1486
|
+
return l.neg ? verdict === true : verdict === false
|
|
1487
|
+
}
|
|
1488
|
+
const deadUnder = (leaves, build) => build !== undefined && leaves.some((l) => leafFalse(l, build))
|
|
1489
|
+
const deadFor = (asker, set) => asker !== undefined && !asker.deadHere && set.key !== '' && cached(asker.dead, set.key, () => deadUnder(set.leaves, asker.build))
|
|
1490
|
+
// Whether every leaf of a candidate's set is among the asker's own (`keys`, by leafKey): the two
|
|
1491
|
+
// are certainly compiled together, not merely possibly (compatible). libc's aix/mod.rs, mounted
|
|
1492
|
+
// under `target_os = "aix"`, asks `crate::fsid_t`: the aix glob's, under that same leaf, is the
|
|
1493
|
+
// answer, rather than the first platform's whose cfg is an `any(…)` nothing rules out. Kept per
|
|
1494
|
+
// pair in `sure`. An `any` leaf holds when one of its alternatives does, and so does the negation
|
|
1495
|
+
// of a custom cfg (`not(loom)`: presumably off, see doubtful). An asker under no cfg holds nothing
|
|
1496
|
+
// else: for it, candidates under cfgs of their own are each only maybe the answer, and all of
|
|
1497
|
+
// them are (see withAlternatives), not the first in written order.
|
|
1498
|
+
const holds = (l, keys, custom) => l.many === undefined && (keys.has(leafKey(l)) || (l.alts === undefined ? l.neg && custom(l.key) : l.alts.some((alt) => alt.every((m) => holds(m, keys, custom)))))
|
|
1499
|
+
const entailed = (asker, set) => asker === undefined || set.key === '' || (!asker.contradictory && cached(asker.sure, set.key, () => set.leaves.every((l) => holds(l, asker.keys, asker.custom))))
|
|
1500
|
+
// The cfgs rustc and cargo set: anything else in a positive leaf is a custom `--cfg` (`loom`,
|
|
1501
|
+
// `docsrs`, `tokio_unstable`, mio's `mio_unsupported_force_poll_poll`), off in a default build
|
|
1502
|
+
// unless a build script or the rustflags set it -- so a candidate under one is `doubtful`: taken
|
|
1503
|
+
// after a compatible candidate under none (never ruled out: the loader doesn't know the build's
|
|
1504
|
+
// flags). One the asker's package may set (its build script prints `cargo:rustc-cfg=<name>`, a
|
|
1505
|
+
// rustflags `--cfg` names it: cargo.js cfgsSetFor) is no custom cfg for it: neither doubtful nor
|
|
1506
|
+
// presumed off; nor is any when its build script may print one the loader can't read (`asker.
|
|
1507
|
+
// custom`, see customFor). An `any` leaf is doubtful when each alternative is, or can't hold with
|
|
1508
|
+
// the asker. A gate macro's leaf (`cfg_x!`) and a `mod` variant's are neither, nor is a leaf the
|
|
1509
|
+
// asker holds itself (a file under `#[cfg(loom)]` takes the loom candidates).
|
|
1510
|
+
const KNOWN_CFG_KEYS = new Set([...TARGET_CFG_KEYS, 'false', 'feature', 'test', 'doctest', 'doc', 'debug_assertions', 'overflow_checks', 'panic', 'proc_macro', 'miri', 'sanitize', 'target_feature', 'target_thread_local', 'ub_checks', 'relocation_model', 'fmt_debug', 'clippy', 'rustfmt', 'variant'])
|
|
1511
|
+
const customKey = (key) => !KNOWN_CFG_KEYS.has(key) && !key.endsWith('!')
|
|
1512
|
+
// The custom-cfg test of an asker whose build may set `settable` (`{ names, any }`), one per object.
|
|
1513
|
+
const customMemo = new WeakMap()
|
|
1514
|
+
const NO_CUSTOM = () => false
|
|
1515
|
+
const customFor = (settable) => {
|
|
1516
|
+
if (settable === undefined || settable === null) return customKey
|
|
1517
|
+
if (settable.any) return NO_CUSTOM
|
|
1518
|
+
if (settable.names.size === 0) return customKey
|
|
1519
|
+
if (!customMemo.has(settable)) customMemo.set(settable, (key) => customKey(key) && !settable.names.has(key))
|
|
1520
|
+
return customMemo.get(settable)
|
|
1521
|
+
}
|
|
1522
|
+
const doubtfulLeaf = (l, askerLeaves, custom) => (l.alts === undefined ? !l.neg && custom(l.key) : l.alts.every((alt) => cfgExclusive(alt, askerLeaves) || alt.some((m) => doubtfulLeaf(m, askerLeaves, custom))))
|
|
1523
|
+
// Whether a leaf list has any custom cfg at all -- else no asker finds it doubtful (kept on the
|
|
1524
|
+
// set for the plain test, which most askers share).
|
|
1525
|
+
const hasCustom = (leaves, custom) => leaves.some((l) => (l.alts === undefined ? !l.neg && custom(l.key) : l.alts.some((alt) => hasCustom(alt, custom))))
|
|
1526
|
+
const doubtful = (asker, set) => {
|
|
1527
|
+
if (asker === undefined || set.key === '') return false
|
|
1528
|
+
if (set.custom === undefined) set.custom = hasCustom(set.leaves, customKey)
|
|
1529
|
+
return set.custom && cached(asker.doubt, set.key, () => set.leaves.some((l) => !holds(l, asker.keys, asker.custom) && doubtfulLeaf(l, asker.set.leaves, asker.custom)))
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// --- memoization -----------------------------------------------------------------------------
|
|
161
1533
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
1534
|
+
// Memoization with cycle detection (globClosure; the same bookkeeping serves globSource, pick and
|
|
1535
|
+
// the candidate lists). A query asked again while it is being computed -- an import cycle: a
|
|
1536
|
+
// glob into a module whose own globs lead back -- reads as `empty`, and the entry in progress is
|
|
1537
|
+
// marked hit (`ctx.hits`, by its depth on the stack of computations under way, `ctx.walking`).
|
|
1538
|
+
// Whatever is computed on top of such a read may be short and is not kept, so a later query from
|
|
1539
|
+
// elsewhere in the cycle computes it afresh. The entry that was hit is kept once complete, but
|
|
1540
|
+
// may be short itself (it read nothing for its own name) -- it is marked `cyclic`, with its
|
|
1541
|
+
// `compute`, for stabilizeClosures to run again once every entry is in place.
|
|
1542
|
+
function memoized(memo, key, ctx, compute, { empty }) {
|
|
1543
|
+
const hit = memo.get(key)
|
|
1544
|
+
if (hit !== undefined) {
|
|
1545
|
+
if (hit.depth !== undefined) {
|
|
1546
|
+
ctx.hits.add(hit.depth)
|
|
1547
|
+
return empty
|
|
1548
|
+
}
|
|
1549
|
+
return hit.value
|
|
1550
|
+
}
|
|
1551
|
+
const depth = ctx.walking++
|
|
1552
|
+
memo.set(key, { depth })
|
|
1553
|
+
const value = compute()
|
|
1554
|
+
const cyclic = ctx.hits.delete(depth)
|
|
1555
|
+
ctx.walking--
|
|
1556
|
+
if (minHit(ctx) < depth) memo.delete(key)
|
|
1557
|
+
else memo.set(key, cyclic ? { value, cyclic, compute } : { value })
|
|
1558
|
+
return value
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// Recompute the glob closures that were computed short of a cycle (memoized), each against the
|
|
1562
|
+
// others as they now stand, until none grows: with every entry complete, a closure is the union
|
|
1563
|
+
// of its globs' targets and their settled closures, whatever the order the queries came in. The
|
|
1564
|
+
// lookups made meanwhile (glob sources, imports' answers, candidate lists) are dropped first, as
|
|
1565
|
+
// they may rest on the short closures. True when anything changed -- the paths resolved so far must be resolved
|
|
1566
|
+
// again.
|
|
1567
|
+
function stabilizeClosures(ctx) {
|
|
1568
|
+
const cyclic = []
|
|
1569
|
+
for (const memo of ctx.closures.values()) for (const entry of memo.values()) if (entry.cyclic === true) cyclic.push(entry)
|
|
1570
|
+
if (cyclic.length === 0) return false
|
|
1571
|
+
const reset = () => {
|
|
1572
|
+
ctx.provided.clear()
|
|
1573
|
+
ctx.opaque.clear()
|
|
1574
|
+
ctx.opaqueGlobs.clear()
|
|
1575
|
+
for (const byModule of ctx.imports.values()) {
|
|
1576
|
+
for (const of of byModule.values()) {
|
|
1577
|
+
for (const im of [...of.globs, ...[...of.named.values()].flat()]) {
|
|
1578
|
+
delete im.source
|
|
1579
|
+
delete im.answers
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
let changed = false
|
|
1585
|
+
for (let round = 0; round < MAX_STABILIZE_ROUNDS; round++) {
|
|
1586
|
+
reset()
|
|
1587
|
+
let grew = false
|
|
1588
|
+
for (const entry of cyclic) {
|
|
1589
|
+
const again = entry.compute()
|
|
1590
|
+
if (again.length !== entry.value.length) {
|
|
1591
|
+
entry.value = again
|
|
1592
|
+
grew = true
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (!grew) break
|
|
1596
|
+
changed = true
|
|
1597
|
+
}
|
|
1598
|
+
reset()
|
|
1599
|
+
return changed
|
|
1600
|
+
}
|
|
1601
|
+
const MAX_STABILIZE_ROUNDS = 8
|
|
1602
|
+
|
|
1603
|
+
// The shallowest computation under way that was cycled back to, Infinity for none.
|
|
1604
|
+
function minHit(ctx) {
|
|
1605
|
+
let min = Infinity
|
|
1606
|
+
for (const d of ctx.hits) if (d < min) min = d
|
|
1607
|
+
return min
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Number of leading module-path segments `a` and `b` share (`crate::a::b` / `crate::a::c` → 2).
|
|
1611
|
+
function commonDepth(a, b) {
|
|
1612
|
+
const n = Math.min(a.length, b.length)
|
|
1613
|
+
let i = 0
|
|
1614
|
+
let depth = 0
|
|
1615
|
+
while (i < n && a[i] === b[i]) {
|
|
1616
|
+
if (a[i] === ':' && a[i + 1] === ':') {
|
|
1617
|
+
depth++
|
|
1618
|
+
i += 2
|
|
1619
|
+
} else {
|
|
1620
|
+
i++
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
// The segment the match stopped in counts when it is whole in both: each ends there or goes on
|
|
1624
|
+
// with a `::`.
|
|
1625
|
+
const wholeA = i === a.length || a.startsWith('::', i)
|
|
1626
|
+
const wholeB = i === b.length || b.startsWith('::', i)
|
|
1627
|
+
return wholeA && wholeB ? depth + 1 : depth
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// What the imports written in module `at` make of `name`, as seen from module `from` (`from`
|
|
1631
|
+
// decides which imports apply: a `pub(in scope)` one to a path written inside `scope`, a plain
|
|
1632
|
+
// `use` to one written in `at` or below). A `use` binding the name outright is followed (bound);
|
|
1633
|
+
// else the modules of this crate the module's globs reach (globClosure) are searched for it, each
|
|
1634
|
+
// as seen from the module whose glob named it: as a child module, an item it defines, or through
|
|
1635
|
+
// a `use` of its own. A glob into the sysroot or into another crate provides nothing: it can't be
|
|
1636
|
+
// told what it brings in, and claiming everything would hide the crate's own edges. `asker` is
|
|
1637
|
+
// the file the path was written in, with its cfg leaves (see cfg compatibility): an import, item
|
|
1638
|
+
// or glob source exclusive with them is passed over. The candidates are listed once per (at,
|
|
1639
|
+
// name, what `from` may see) -- providedAll -- and the asker picks among them.
|
|
1640
|
+
function provided(root, at, name, from, ctx, asker, ns = null) {
|
|
1641
|
+
return pick(providedAll(root, at, name, commonDepth(from, at), ctx), root, ctx, asker, ns) // scopes are ancestors of `at`: `from` is inside those this deep or shallower
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// The answer of the first of `candidates` (see providedAll: in written order) whose cfgs the
|
|
1645
|
+
// asker's entail (entailed) -- else the first compatible with them under no custom cfg, else the
|
|
1646
|
+
// first compatible at all (doubtful) -- in the namespace `ns` asks for: for a path's lead or
|
|
1647
|
+
// prefix (`type`), a `fn log` can't lead `log::info`; for a macro (`macro`), an item the module
|
|
1648
|
+
// defines (`fn m`), a module, or an import of a module or crate is no `m!`. A child module,
|
|
1649
|
+
// item or macro answers as it is; an import answers what it leads to (followImport, on the
|
|
1650
|
+
// asker's behalf, so its cfgs hold along the whole chain), resolved when first asked and then
|
|
1651
|
+
// kept on the import per asker -- unless the answer came out of an import cycle still being
|
|
1652
|
+
// resolved (see memoized). An import being followed that is met again leads back to itself (`use
|
|
1653
|
+
// log;` in every platform file of a module asks the module for `log`, which is these imports):
|
|
1654
|
+
// nothing here. An import that leads nowhere the bundle can see -- when it is a path, not a crate
|
|
1655
|
+
// named outright (`use serde;`, passed over) -- answers where it is: the name is an item of its
|
|
1656
|
+
// file, as far as the bundle knows. A value where a type is wanted (a `fn` defined here, an
|
|
1657
|
+
// import of one) is passed over too, and when nothing else answers, the answer is VALUE_ONLY
|
|
1658
|
+
// rather than nothing: the name is taken, but not in this namespace.
|
|
1659
|
+
function pick(candidates, root, ctx, asker, ns = null) {
|
|
1660
|
+
const maybes = [] // the compatible answers under no doubt, for when none is entailed (see withAlternatives)
|
|
1661
|
+
let doubted = null // the first compatible answer under a doubtful cfg, for when there is nothing else
|
|
1662
|
+
let lastResort = null // the first under a cfg the asker's build can't compile (see deadUnder)
|
|
1663
|
+
let valueOnly = false // a candidate passed over for being a value where a type is wanted
|
|
1664
|
+
for (let i = 0; ; i++) {
|
|
1665
|
+
const c = candidates.get(i)
|
|
1666
|
+
if (c === undefined) break
|
|
1667
|
+
if (!compatible(asker, c.leaves)) continue
|
|
1668
|
+
if (ns === 'type' && c.ns === 'value') {
|
|
1669
|
+
valueOnly = true
|
|
1670
|
+
continue
|
|
1671
|
+
}
|
|
1672
|
+
if (ns === 'macro' ? (c.ns !== undefined && c.ns !== 'macro') || c.answer?.kind === 'module' : c.ns === 'macro') continue
|
|
1673
|
+
const dead = deadFor(asker, c.leaves)
|
|
1674
|
+
const doubt = !dead && doubtful(asker, c.leaves)
|
|
1675
|
+
const sure = !dead && !doubt && entailed(asker, c.leaves)
|
|
1676
|
+
if ((doubt || dead) && (maybes.length > 0 || doubted !== null)) continue
|
|
1677
|
+
if (dead && lastResort !== null) continue
|
|
1678
|
+
let r
|
|
1679
|
+
if (c.import === undefined) {
|
|
1680
|
+
r = c.answer
|
|
1681
|
+
} else {
|
|
1682
|
+
const im = c.import
|
|
1683
|
+
if (im.following !== undefined) {
|
|
1684
|
+
ctx.hits.add(im.following)
|
|
1685
|
+
continue
|
|
1686
|
+
}
|
|
1687
|
+
const key = `${asker?.set.key ?? ''}\0${ns}`
|
|
1688
|
+
r = im.answers?.get(key)
|
|
1689
|
+
if (r === undefined) {
|
|
1690
|
+
const depth = ctx.walking++
|
|
1691
|
+
im.following = depth
|
|
1692
|
+
r = followImport(im, root, ctx, asker, ns)
|
|
1693
|
+
// The name a macro an import leads to is defined by: the import's (`use dep::mac as m;`).
|
|
1694
|
+
if (ns === 'macro' && r?.kind === 'item' && r.macroName === undefined && im.segments.length > 0) r = { ...r, macroName: im.segments.at(-1) }
|
|
1695
|
+
im.following = undefined
|
|
1696
|
+
ctx.walking--
|
|
1697
|
+
ctx.hits.delete(depth)
|
|
1698
|
+
if (minHit(ctx) > depth) (im.answers ??= new Map()).set(key, r)
|
|
1699
|
+
}
|
|
1700
|
+
// Followed as a macro to a file defining no macro of the name (`pub use util::helper;` of
|
|
1701
|
+
// a `fn helper` beside the root's `#[macro_export] macro_rules! helper`), it binds no macro:
|
|
1702
|
+
// what it imports lives in another namespace.
|
|
1703
|
+
if (ns === 'macro' && r?.kind === 'item' && ctx.fileMacros.get(r.file)?.has(r.macroName) !== true) continue
|
|
1704
|
+
// An import leading out of the bundle (`pub(crate) use std::sync::atomic::AtomicU64;`, a
|
|
1705
|
+
// crate that isn't vendored) binds the name all the same: an item of its own file, as far
|
|
1706
|
+
// as the bundle knows, under its cfgs. One naming a crate outright (`use serde;`) answers
|
|
1707
|
+
// nothing: the lead is the crate's to report.
|
|
1708
|
+
if (r === null) {
|
|
1709
|
+
if (im.segments.length === 1) continue
|
|
1710
|
+
r = { kind: 'item', file: im.file }
|
|
1711
|
+
}
|
|
1712
|
+
if (r === VALUE_ONLY) {
|
|
1713
|
+
valueOnly = true
|
|
1714
|
+
continue
|
|
1715
|
+
}
|
|
1716
|
+
if (ns === 'macro' && (r.kind === 'module' || r.kind === 'crate')) continue
|
|
1717
|
+
}
|
|
1718
|
+
if (sure) return { ...r, through: c.through }
|
|
1719
|
+
if (dead) lastResort = { ...r, through: c.through }
|
|
1720
|
+
else if (doubt) doubted = { ...r, through: c.through }
|
|
1721
|
+
else maybes.push({ answer: { ...r, through: c.through }, leaves: c.leaves })
|
|
1722
|
+
}
|
|
1723
|
+
if (maybes.length > 0) return withAlternatives(maybes, asker)
|
|
1724
|
+
if (doubted !== null) return doubted
|
|
1725
|
+
if (lastResort !== null) return lastResort
|
|
1726
|
+
return valueOnly ? VALUE_ONLY : null
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// A leaf list as cfg text, for a key: `all(unix, feature = "std")`, `not(loom)`, `any(…)`, a gate
|
|
1730
|
+
// macro by name (`cfg_x!`); a `mod` variant's leaf by the key of the variant (its cfg, or its
|
|
1731
|
+
// macro), and only when nothing else tells the candidates apart. Null for no leaves.
|
|
1732
|
+
const leafText = (l) => {
|
|
1733
|
+
if (l.alts !== undefined) return `any(${l.alts.map((alt) => cfgTextOf(alt) ?? '*').join(', ')})`
|
|
1734
|
+
const plain = l.many !== undefined ? manyText(l) : (l.value === null ? l.key : `${l.key} = "${l.value}"`)
|
|
1735
|
+
return l.neg ? `not(${plain})` : plain
|
|
1736
|
+
}
|
|
1737
|
+
// An undecided leaf's text (see UNDECIDED_KEY): the `any(…)` of its alternatives, kept per leaf, or
|
|
1738
|
+
// its count when that runs long (undecided leaves within undecided leaves).
|
|
1739
|
+
const manyTexts = new WeakMap()
|
|
1740
|
+
const manyText = (l) => cached(manyTexts, l, () => {
|
|
1741
|
+
const text = `any(${l.many.map((alt) => cfgTextOf(alt) ?? '*').join(', ')})`
|
|
1742
|
+
return text.length > 4096 ? `any(${l.many.length} alternatives)` : text
|
|
1743
|
+
})
|
|
1744
|
+
function cfgTextOf(leaves) {
|
|
1745
|
+
const own = leaves.filter((l) => l.key !== 'variant')
|
|
1746
|
+
// What a variant is, rather than all it isn't: its positive leaves (a `cfg_if!` branch's own
|
|
1747
|
+
// cfg, not the negations of the branches before it), else its `any(…)` of positive ones.
|
|
1748
|
+
const positive = own.filter((l) => l.alts === undefined && !l.neg)
|
|
1749
|
+
const anyPositive = own.filter((l) => l.alts !== undefined && l.alts.every((alt) => alt.every((m) => m.alts === undefined && !m.neg)))
|
|
1750
|
+
const shown = positive.length + anyPositive.length > 0 ? [...positive, ...anyPositive] : (own.length > 0 ? own : leaves)
|
|
1751
|
+
return joinCfgs([...new Set(shown.map((l) => (l.key === 'variant' ? l.value : leafText(l))))].toSorted())
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// The answer of several candidates none of which the asker's cfgs entail (pick, definedIn): each
|
|
1755
|
+
// applies under cfgs of its own -- a file under no platform cfg asking libc's `crate::sockaddr`,
|
|
1756
|
+
// defined per platform -- so the first in written order is no answer. The first, carrying
|
|
1757
|
+
// `alternatives`: cfg key → file, each distinct file under the leaves of its first candidate
|
|
1758
|
+
// that the asker doesn't hold itself. A module first -- one a path may go on through -- carries
|
|
1759
|
+
// `branches` instead, cfg key → each distinct answer (its own included), for walkPath to follow
|
|
1760
|
+
// the rest of the path through each (`use unix as imp;` beside `use windows as imp;`). One file
|
|
1761
|
+
// or module: that answer alone.
|
|
1762
|
+
function withAlternatives(found, asker) {
|
|
1763
|
+
const [first] = found
|
|
1764
|
+
const idOf = (answer) => (answer.kind === 'module' ? `mod ${answer.modulePath}` : answer.file)
|
|
1765
|
+
if (found.length === 1 || new Set(found.map((f) => idOf(f.answer)).filter((id) => id !== undefined)).size < 2) return first.answer
|
|
1766
|
+
const keyed = new Map()
|
|
1767
|
+
const seen = new Set()
|
|
1768
|
+
for (const { answer, leaves } of found) {
|
|
1769
|
+
const id = idOf(answer)
|
|
1770
|
+
if (id === undefined || seen.has(id)) continue
|
|
1771
|
+
seen.add(id)
|
|
1772
|
+
// Kept per asker set (the text leaves out what the asker holds itself).
|
|
1773
|
+
const textOf = () => cfgTextOf(leaves.leaves.filter((l) => asker === undefined || !asker.keys.has(leafKey(l))))
|
|
1774
|
+
const base = cfgKey(asker === undefined ? textOf() : cached(asker.texts, leaves.key, textOf))
|
|
1775
|
+
let key = base
|
|
1776
|
+
for (let k = 2; keyed.has(key); k++) key = `${base}#${k}`
|
|
1777
|
+
keyed.set(key, answer)
|
|
1778
|
+
}
|
|
1779
|
+
if (first.answer.kind === 'module') return { ...first.answer, branches: keyed }
|
|
1780
|
+
return { ...first.answer, alternatives: new Map([...keyed].filter(([, answer]) => answer.file !== undefined).map(([key, answer]) => [key, answer.file])) }
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Every candidate module `at`'s imports offer for `name` as seen `seeing` levels into it, in
|
|
1784
|
+
// written order -- `{ answer | import, through, leaves }`: what its `use` items bind (as imports
|
|
1785
|
+
// to follow, see pick), then what each module its globs reach has (hasAll), under the glob's cfg
|
|
1786
|
+
// leaves too. The list depends on the crate's structure alone, so it is built once per (at,
|
|
1787
|
+
// name, seeing) and shared by every asker, who picks the first candidate compatible with its own
|
|
1788
|
+
// cfgs -- and built lazily (Lazy): a closure holds hundreds of modules, and the first few usually
|
|
1789
|
+
// answer. A list built while the closure was still being resolved (an import cycle, see
|
|
1790
|
+
// memoized) is not kept.
|
|
1791
|
+
function providedAll(root, at, name, seeing, ctx) {
|
|
1792
|
+
const memo = ctx.provided.get(root) ?? ctx.provided.set(root, new Map()).get(root)
|
|
1793
|
+
const key = `${at}\0${name}\0${seeing}`
|
|
1794
|
+
let list = memo.get(key)
|
|
1795
|
+
if (list === undefined) {
|
|
1796
|
+
const depth = ctx.walking++
|
|
1797
|
+
const closure = globClosure(root, at, seeing, ctx)
|
|
1798
|
+
ctx.walking--
|
|
1799
|
+
list = new Lazy(function* () {
|
|
1800
|
+
yield* importsOf(root, at, name, seeing, ctx)
|
|
1801
|
+
// At the crate root, a `#[macro_export]` macro is a binding of the name too, in the macro
|
|
1802
|
+
// namespace (`ns`): a `pub use inner::m;` of another build beside it (serde's docsrs-only
|
|
1803
|
+
// copy of serde_core's macros) is ranked against it by their cfgs.
|
|
1804
|
+
if (at === 'crate') for (const m of ctx.exportedMacros?.get(root)?.get(name) ?? NONE) yield { answer: { kind: 'item', file: m.file }, leaves: m.leaves, ns: 'macro' }
|
|
1805
|
+
// The module's own items rank with its named imports -- two explicit bindings of one name
|
|
1806
|
+
// are rustc's error unless their cfgs differ (tokio's `imp` re-exports std's `AtomicU64` in
|
|
1807
|
+
// one variant file and defines its own in the other) -- ahead of what globs bring in, which
|
|
1808
|
+
// never shadows them. The module's tree file first; visibility is the path's to break.
|
|
1809
|
+
const defs = ctx.defined.get(root)?.get(at)?.get(name)
|
|
1810
|
+
if (defs !== undefined) {
|
|
1811
|
+
const own = ctx.trees.get(root).get(at)
|
|
1812
|
+
for (const d of defs) if (d.file === own) yield { answer: { kind: 'item', file: d.file }, leaves: d.leaves, ns: d.ns }
|
|
1813
|
+
for (const d of defs) if (d.file !== own) yield { answer: { kind: 'item', file: d.file }, leaves: d.leaves, ns: d.ns }
|
|
1814
|
+
}
|
|
1815
|
+
// Of the closure's modules (hundreds, in libc), only the ones with something of the name
|
|
1816
|
+
// (`having`), in closure order.
|
|
1817
|
+
const having = ctx.having.get(root)?.get(name)
|
|
1818
|
+
if (having === undefined) return
|
|
1819
|
+
const positions = []
|
|
1820
|
+
for (const module of having) for (const i of closureIndex(closure).get(module) ?? NONE) positions.push(i)
|
|
1821
|
+
for (const i of positions.toSorted((x, y) => x - y)) {
|
|
1822
|
+
const reached = closure[i]
|
|
1823
|
+
for (const c of hasAll(root, reached.module, name, reached.seeing, ctx)) yield { ...c, through: reached.through, leaves: unionLeaves(reached.leaves, c.leaves, ctx.leafSets) }
|
|
1824
|
+
}
|
|
1825
|
+
}(), ctx)
|
|
1826
|
+
if (minHit(ctx) >= depth) memo.set(key, list)
|
|
1827
|
+
}
|
|
1828
|
+
return list
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// A closure's entries by module (one may be reached at several depths), kept on the closure.
|
|
1832
|
+
function closureIndex(closure) {
|
|
1833
|
+
if (closure.index === undefined) {
|
|
1834
|
+
closure.index = new Map()
|
|
1835
|
+
closure.forEach((e, i) => (closure.index.get(e.module) ?? closure.index.set(e.module, []).get(e.module)).push(i))
|
|
1836
|
+
}
|
|
1837
|
+
return closure.index
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// A list materialized from a generator as far as it is read (`get(i)`, undefined past the end).
|
|
1841
|
+
// Read again while its generator is producing an element (the element's own lookups led back to
|
|
1842
|
+
// the same question), it ends there for that reader, and the computation under way is marked hit
|
|
1843
|
+
// (see memoized) so nothing computed on that short view is kept.
|
|
1844
|
+
class Lazy {
|
|
1845
|
+
constructor(gen, ctx) {
|
|
1846
|
+
this.gen = gen
|
|
1847
|
+
this.ctx = ctx
|
|
1848
|
+
this.out = []
|
|
1849
|
+
this.running = -1
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
get(i) {
|
|
1853
|
+
if (this.running !== -1) {
|
|
1854
|
+
this.ctx.hits.add(this.running)
|
|
1855
|
+
return undefined
|
|
1856
|
+
}
|
|
1857
|
+
while (i >= this.out.length) {
|
|
1858
|
+
this.running = this.ctx.walking
|
|
1859
|
+
let next
|
|
1860
|
+
try {
|
|
1861
|
+
next = this.gen.next()
|
|
1862
|
+
} finally {
|
|
1863
|
+
this.running = -1
|
|
1864
|
+
}
|
|
1865
|
+
if (next.done) return undefined
|
|
1866
|
+
this.out.push(next.value)
|
|
1867
|
+
}
|
|
1868
|
+
return this.out[i]
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
* [Symbol.iterator]() {
|
|
1872
|
+
for (let i = 0; ; i++) {
|
|
1873
|
+
const c = this.get(i)
|
|
1874
|
+
if (c === undefined) return
|
|
1875
|
+
yield c
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Everything module `at` has under `name` for one seeing `seeing` levels into it -- what a glob
|
|
1881
|
+
// into the module brings in -- as candidates in written order: a child module (visible that far),
|
|
1882
|
+
// the items it defines (the module's own tree file first: an inline `mod imp { }` under one cfg
|
|
1883
|
+
// beside a `mod imp;` file under another may both define it), at the crate root a `#[macro_export]`
|
|
1884
|
+
// macro (an item of the root's namespace: zerocopy's `use super::*;` from the root brings
|
|
1885
|
+
// `into_inner!` in), and its `use` items binding the name (importsOf).
|
|
1886
|
+
function hasAll(root, at, name, seeing, ctx) {
|
|
1887
|
+
// Asked for every module a closure reaches, most of which have nothing of the name.
|
|
1888
|
+
const child = ctx.children.get(root)?.get(at)?.has(name) === true
|
|
1889
|
+
const defs = ctx.defined.get(root)?.get(at)?.get(name)
|
|
1890
|
+
const macros = at === 'crate' ? ctx.exportedMacros?.get(root)?.get(name) : undefined
|
|
1891
|
+
const imports = importsOf(root, at, name, seeing, ctx)
|
|
1892
|
+
if (!child && defs === undefined && macros === undefined && imports.length === 0) return NONE
|
|
1893
|
+
const out = []
|
|
1894
|
+
if (child) {
|
|
1895
|
+
const sub = `${at}::${name}`
|
|
1896
|
+
if ((ctx.modScope.get(root)?.get(sub) ?? 1) <= seeing) out.push({ answer: { kind: 'module', modulePath: sub, file: ctx.trees.get(root).get(sub) }, leaves: NO_LEAVES })
|
|
1897
|
+
}
|
|
1898
|
+
if (defs !== undefined) {
|
|
1899
|
+
const own = ctx.trees.get(root).get(at)
|
|
1900
|
+
for (const d of defs) if (d.scopeDepth <= seeing && d.file === own) out.push({ answer: { kind: 'item', file: d.file }, leaves: d.leaves, ns: d.ns })
|
|
1901
|
+
for (const d of defs) if (d.scopeDepth <= seeing && d.file !== own) out.push({ answer: { kind: 'item', file: d.file }, leaves: d.leaves, ns: d.ns })
|
|
1902
|
+
}
|
|
1903
|
+
for (const m of macros ?? NONE) out.push({ answer: { kind: 'item', file: m.file }, leaves: m.leaves })
|
|
1904
|
+
out.push(...imports)
|
|
1905
|
+
return out
|
|
1906
|
+
}
|
|
1907
|
+
const NONE = Object.freeze([])
|
|
1908
|
+
|
|
1909
|
+
// The file in which module `at` defines item `name` (a `struct`, `fn`, … under cfgs the asker's
|
|
1910
|
+
// allow; in the type namespace when `ns` is `type`, see pick), or null -- or `{ file,
|
|
1911
|
+
// alternatives }` when several files define it, none for certain (see withAlternatives; itemAt
|
|
1912
|
+
// makes an answer of either).
|
|
1913
|
+
function definedIn(root, at, name, ctx, asker, ns = null) {
|
|
1914
|
+
const defs = ctx.defined.get(root)?.get(at)?.get(name)
|
|
1915
|
+
if (defs === undefined) return null
|
|
1916
|
+
const own = ctx.trees.get(root).get(at)
|
|
1917
|
+
// Ranked as pick ranks candidates -- sure, then compatible under no custom cfg, then doubtful,
|
|
1918
|
+
// then dead in the asker's build -- the module's own tree file first within a rank, else the
|
|
1919
|
+
// first written.
|
|
1920
|
+
// Several of the middle rank, none sure: each applies under its own cfgs (see withAlternatives).
|
|
1921
|
+
let best = null
|
|
1922
|
+
let bestRank = Infinity
|
|
1923
|
+
const maybes = []
|
|
1924
|
+
for (const d of defs) {
|
|
1925
|
+
if (!compatible(asker, d.leaves) || (ns === 'type' && d.ns === 'value')) continue
|
|
1926
|
+
const tier = deadFor(asker, d.leaves) ? 3 : (doubtful(asker, d.leaves) ? 2 : (entailed(asker, d.leaves) ? 0 : 1))
|
|
1927
|
+
if (tier === 1) maybes.push({ answer: { kind: 'item', file: d.file }, leaves: d.leaves })
|
|
1928
|
+
const rank = tier * 2 + (d.file === own ? 0 : 1)
|
|
1929
|
+
if (rank < bestRank) {
|
|
1930
|
+
best = d.file
|
|
1931
|
+
bestRank = rank
|
|
1932
|
+
if (rank === 0) break
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (bestRank >= 2 && bestRank < 4 && maybes.length > 1) {
|
|
1936
|
+
const r = withAlternatives([...maybes.filter((m) => m.answer.file === best), ...maybes.filter((m) => m.answer.file !== best)], asker)
|
|
1937
|
+
if (r.alternatives !== undefined) return { file: best, alternatives: r.alternatives }
|
|
1938
|
+
}
|
|
1939
|
+
return best
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// What module `at` (spread over the files of cfg variants, `#[cfg_attr(unix, path = …)]`) makes
|
|
1943
|
+
// of `name` for a path written in `file`, that file's own imports first: its `use` binding the
|
|
1944
|
+
// name, then its own globs; only then the module's imports at large (providedAll). For a module
|
|
1945
|
+
// held in one file the two agree, so the shortcut is taken only when they may not. Memoized like
|
|
1946
|
+
// providedAll: a `use log;` in such a file asks for `log` in its own module, which is this import.
|
|
1947
|
+
function providedFrom(root, at, name, ctx, file, asker, ns = null) {
|
|
1948
|
+
const of = ctx.imports.get(root)?.get(at)
|
|
1949
|
+
// The files holding the module's imports, gathered once (they are all in place by now).
|
|
1950
|
+
const files = of === undefined ? null : (of.files ??= new Set([...of.globs, ...[...of.named.values()].flat()].map((im) => im.file)))
|
|
1951
|
+
if (files === null || files.size === (files.has(file) ? 1 : 0)) return provided(root, at, name, at, ctx, asker, ns)
|
|
1952
|
+
const memo = ctx.provided.get(root) ?? ctx.provided.set(root, new Map()).get(root)
|
|
1953
|
+
const seeing = at.split('::').length
|
|
1954
|
+
const key = `${at}\0${name}\0@${file}`
|
|
1955
|
+
let list = memo.get(key)
|
|
1956
|
+
if (list === undefined) {
|
|
1957
|
+
// Resolving this file's glob sources may ask this very question (`pub use sibling::*;` beside
|
|
1958
|
+
// `use super::*;`): while the list is in the making it reads as nothing, and is not kept then.
|
|
1959
|
+
const depth = ctx.walking++
|
|
1960
|
+
memo.set(key, { depth })
|
|
1961
|
+
const own = [] // this file's globs, with their sources
|
|
1962
|
+
for (const im of of.globs) {
|
|
1963
|
+
if (im.file !== file) continue
|
|
1964
|
+
const source = globSource(im, root, ctx)
|
|
1965
|
+
if (source?.kind === 'module') own.push([im, source.modulePath])
|
|
1966
|
+
}
|
|
1967
|
+
const cyclic = ctx.hits.delete(depth)
|
|
1968
|
+
ctx.walking--
|
|
1969
|
+
list = new Lazy(function* () {
|
|
1970
|
+
yield* importsOf(root, at, name, seeing, ctx, file)
|
|
1971
|
+
for (const [im, module] of own) {
|
|
1972
|
+
const sees = commonDepth(at, module)
|
|
1973
|
+
for (const c of hasAll(root, module, name, sees, ctx)) yield { ...c, through: im.file, leaves: unionLeaves(im.leaves, c.leaves, ctx.leafSets) }
|
|
1974
|
+
for (const c of providedAll(root, module, name, sees, ctx)) yield { ...c, through: im.file, leaves: unionLeaves(im.leaves, c.leaves, ctx.leafSets) }
|
|
1975
|
+
}
|
|
1976
|
+
yield* providedAll(root, at, name, seeing, ctx)
|
|
1977
|
+
}(), ctx)
|
|
1978
|
+
if (!cyclic && minHit(ctx) >= depth) memo.set(key, list)
|
|
1979
|
+
else memo.delete(key)
|
|
1980
|
+
} else if (list.depth !== undefined) {
|
|
1981
|
+
ctx.hits.add(list.depth)
|
|
1982
|
+
return null
|
|
1983
|
+
}
|
|
1984
|
+
return pick(list, root, ctx, asker, ns)
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// The `use` items of module `at` binding `name`, visible to one seeing `seeing` levels into it,
|
|
1988
|
+
// as candidates to follow (pick), in written order, each with the file holding the import as
|
|
1989
|
+
// `through`. Several may bind it (`#[cfg(unix)] use a::X;` beside `#[cfg(windows)] use b::X;`).
|
|
1990
|
+
// `onlyFile` restricts to the imports written in that file: the module may be spread over cfg
|
|
1991
|
+
// variants (`#[cfg_attr(unix, path = …)]`), each binding the name its own way, and a path written
|
|
1992
|
+
// in one of them means that one's.
|
|
1993
|
+
function importsOf(root, at, name, seeing, ctx, onlyFile) {
|
|
1994
|
+
const named = ctx.imports.get(root)?.get(at)?.named.get(name)
|
|
1995
|
+
if (named === undefined) return NONE
|
|
1996
|
+
const out = []
|
|
1997
|
+
for (const im of named) {
|
|
1998
|
+
if (im.scopeDepth > seeing || (onlyFile !== undefined && im.file !== onlyFile)) continue
|
|
1999
|
+
out.push({ import: im, through: im.file, leaves: im.leaves })
|
|
2000
|
+
}
|
|
2001
|
+
return out
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// The modules of this crate that the globs of module `at` visible to one seeing `seeing` levels
|
|
2005
|
+
// into it reach, transitively (a glob's target module's own globs, as seen from the module whose
|
|
2006
|
+
// glob named it), depth-first in written order, each `{ module, seeing, through }`: how deep the
|
|
2007
|
+
// module naming it sees into it, and the file holding the glob of `at` it was reached through.
|
|
2008
|
+
// Computed once per (at, seeing), a settled closure of a module reached standing in for walking
|
|
2009
|
+
// on from it; a closure asked for while it is being computed (a glob into a module whose own
|
|
2010
|
+
// imports name the glob's source) is a cycle, and reads as empty.
|
|
2011
|
+
function globClosure(root, at, seeing, ctx) {
|
|
2012
|
+
const memo = ctx.closures.get(root) ?? ctx.closures.set(root, new Map()).get(root)
|
|
2013
|
+
const key = `${at}\0${seeing}`
|
|
2014
|
+
return memoized(memo, key, ctx, () => {
|
|
2015
|
+
const modules = []
|
|
2016
|
+
const seen = new Map([[key, null]]) // (module, seeing) → its entry
|
|
2017
|
+
// `leaves`: the cfgs of the globs walked through to get here, all of which must hold (a leaf
|
|
2018
|
+
// set). A module reached again by another path (libc's `mod primitives`, glob re-exported
|
|
2019
|
+
// under a dozen cfgs) is reachable under either -- and so is everything its own globs reach,
|
|
2020
|
+
// so a path that widens an entry is walked on from it (`widened`); one that adds nothing ends.
|
|
2021
|
+
const add = (k, entry) => {
|
|
2022
|
+
const known = seen.get(k)
|
|
2023
|
+
if (known === undefined) {
|
|
2024
|
+
seen.set(k, entry)
|
|
2025
|
+
modules.push(entry)
|
|
2026
|
+
return 'new'
|
|
2027
|
+
}
|
|
2028
|
+
if (known === null) return 'same'
|
|
2029
|
+
const either = eitherLeaves(known.leaves, entry.leaves, ctx.leafSets)
|
|
2030
|
+
if (either === known.leaves) return 'same'
|
|
2031
|
+
known.leaves = either
|
|
2032
|
+
return 'widened'
|
|
2033
|
+
}
|
|
2034
|
+
const reach = (module, sees, through, leaves) => {
|
|
2035
|
+
const k = `${module}\0${sees}`
|
|
2036
|
+
if (add(k, { module, seeing: sees, through, leaves }) === 'same') return
|
|
2037
|
+
const settled = memo.get(k)
|
|
2038
|
+
if (settled !== undefined && settled.depth === undefined) {
|
|
2039
|
+
// Already transitive: no walking on from any of them.
|
|
2040
|
+
for (const m of settled.value) add(`${m.module}\0${m.seeing}`, { module: m.module, seeing: m.seeing, through, leaves: unionLeaves(leaves, m.leaves, ctx.leafSets) })
|
|
2041
|
+
return
|
|
2042
|
+
}
|
|
2043
|
+
reachGlobs(module, sees, through, leaves)
|
|
2044
|
+
}
|
|
2045
|
+
// The modules the globs of `module` visible to one seeing `sees` levels into it name, reached
|
|
2046
|
+
// through `through` (else each glob's file) under `leaves` and the glob's own.
|
|
2047
|
+
const reachGlobs = (module, sees, through, leaves) => {
|
|
2048
|
+
for (const im of ctx.imports.get(root)?.get(module)?.globs ?? []) {
|
|
2049
|
+
if (im.scopeDepth > sees) continue
|
|
2050
|
+
const source = globSource(im, root, ctx)
|
|
2051
|
+
if (source?.kind === 'module') reach(source.modulePath, commonDepth(module, source.modulePath), through ?? im.file, unionLeaves(leaves, im.leaves, ctx.leafSets))
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
reachGlobs(at, seeing, null, NO_LEAVES)
|
|
2055
|
+
return modules
|
|
2056
|
+
}, { empty: [] })
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// What a glob imports from (followImport), asked once per glob rather than once per closure it
|
|
2060
|
+
// is part of -- unless the answer came out of an import cycle still being resolved.
|
|
2061
|
+
function globSource(im, root, ctx) {
|
|
2062
|
+
if (im.source !== undefined) return im.source
|
|
2063
|
+
const depth = ctx.walking++
|
|
2064
|
+
const source = followImport(im, root, ctx)
|
|
2065
|
+
ctx.walking--
|
|
2066
|
+
if (minHit(ctx) >= depth) im.source = source
|
|
2067
|
+
return source
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// What an import leads to: its path resolved as written in its module, else the crate its lead
|
|
2071
|
+
// names -- the sysroot's (`use core::fmt`: an item, living here as far as the bundle knows), an
|
|
2072
|
+
// in-tree one, or null for one that isn't (nor anything the tree can model).
|
|
2073
|
+
function followImport(im, root, ctx, asker, ns = null) {
|
|
2074
|
+
if (!im.absolute) {
|
|
2075
|
+
const r = walkPath(im.segments, root, im.module, ctx, { file: im.file, asker, ns })
|
|
2076
|
+
// A path into another crate by a name the module binds to it -- the crate itself, through
|
|
2077
|
+
// its `use dep::{dep, mac}` too (`dep` the macro of the same name) -- followed as a macro:
|
|
2078
|
+
// the macro there (macroOfCrate).
|
|
2079
|
+
if (r !== null && ns === 'macro' && r.kind === 'crate' && r.rest?.length > 0) return macroOfCrate(r.file, r.rest, ctx) ?? r
|
|
2080
|
+
if (r !== null) return r
|
|
2081
|
+
}
|
|
2082
|
+
const head = im.segments[0]
|
|
2083
|
+
if (NON_CRATE_LEADS.has(head)) return { kind: 'item', file: im.file }
|
|
2084
|
+
const target = ctx.resolveCrate(head, im.file)
|
|
2085
|
+
if (!target) return null
|
|
2086
|
+
// A macro of another crate (`use dep::mac;`), followed as one: the file defining it (macroOfCrate).
|
|
2087
|
+
const macro = ns === 'macro' && im.segments.length > 1 ? macroOfCrate(target, im.segments.slice(1), ctx) : null
|
|
2088
|
+
return macro ?? { kind: 'crate', name: head, file: target }
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// The macro `rest` names in the in-tree crate whose root is `target` (`use dep::mac;`,
|
|
2092
|
+
// `dep::mac!(…)`, a `#[macro_use] extern crate`'s): looked up from that crate's root as a path
|
|
2093
|
+
// there is, in the macro namespace -- each `#[macro_export]` definition under its file's cfgs
|
|
2094
|
+
// (serde's docsrs-only copies), a `pub use inner::mac;` followed on into `inner` -- the cfgs
|
|
2095
|
+
// judged by that root's own build. `{ kind: 'item', file, macroName, alternatives? }`, the file
|
|
2096
|
+
// defining it by the name it defines it under, or null when none does.
|
|
2097
|
+
function macroOfCrate(target, rest, ctx) {
|
|
2098
|
+
if (!ctx.trees.has(target)) {
|
|
2099
|
+
const file = rest.length === 1 ? ctx.macros.get(target)?.get(rest[0]) : undefined
|
|
2100
|
+
return file === undefined ? null : { kind: 'item', file, macroName: rest[0] }
|
|
2101
|
+
}
|
|
2102
|
+
const r = walkPath(['crate', ...rest], target, 'crate', ctx, { ns: 'macro', file: target, asker: ctx.files.get(target)?.asker })
|
|
2103
|
+
if (r === null || r === VALUE_ONLY || r.kind !== 'item') return null
|
|
2104
|
+
const macroName = r.macroName ?? rest.at(-1)
|
|
2105
|
+
if (ctx.fileMacros.get(r.file)?.has(macroName) !== true) return null
|
|
2106
|
+
const alternatives = r.alternatives === undefined ? undefined : new Map([...r.alternatives].filter(([, f]) => ctx.fileMacros.get(f)?.has(macroName) === true))
|
|
2107
|
+
return { kind: 'item', file: r.file, macroName, ...(alternatives?.size > 1 ? { alternatives } : {}) }
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// An item answer for what definedIn found.
|
|
2111
|
+
const itemAt = (def) => (typeof def === 'string' ? { kind: 'item', file: def } : { kind: 'item', file: def.file, alternatives: def.alternatives })
|
|
2112
|
+
|
|
2113
|
+
// walkPath's answer for a path naming a value-namespace item (a `fn`, `const` or `static`) where
|
|
2114
|
+
// a type or module is wanted: as a prefix (`log::info!` beside a `fn log`), or as the target of
|
|
2115
|
+
// an import followed as one (`use crate::util::log; log::info!`). Nothing here -- and not an
|
|
2116
|
+
// item of the module either: the lead may still name a crate.
|
|
2117
|
+
const VALUE_ONLY = Object.freeze({ kind: 'value' })
|
|
2118
|
+
// Whether module `at` defines `name` only in the value namespace.
|
|
2119
|
+
const onlyValues = (root, at, name, ctx) => {
|
|
2120
|
+
const defs = ctx.defined.get(root)?.get(at)?.get(name)
|
|
2121
|
+
return defs !== undefined && defs.every((d) => d.ns === 'value')
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Resolve `segments`, a path written in module `from` of `root`'s crate, when its lead anchors it
|
|
2125
|
+
// there: `crate::`, `self::`, `super::`, a child module of `from`, a name `from`'s imports provide
|
|
2126
|
+
// (`use gen::consts; pub use consts::*;`, `extern crate serde_core as s; s::Value`), or -- for a
|
|
2127
|
+
// bare name -- a `macro_rules!` of the file (`macro_rules! helper { … } pub(crate) use helper;`).
|
|
2128
|
+
// Null when it doesn't (an item in scope, a crate: the caller looks the lead up as one).
|
|
2129
|
+
// Then module by module; a segment naming no module is looked up in the module reached, and the
|
|
2130
|
+
// walk goes on from a module that provides, or ends at whatever else it provides. A segment
|
|
2131
|
+
// nothing provides is an item of the module reached, in the file defining it when that is known.
|
|
2132
|
+
// At the crate root a final segment may name a `#[macro_export]` macro, which lives in the file
|
|
2133
|
+
// defining it: `$crate::name!` names the macro before anything else (`ns` is `macro`: a path
|
|
2134
|
+
// invoked), any other path the module or import of that name first. In the module the path is
|
|
2135
|
+
// written in, `file`'s own imports come first: the module's other files, if any, are cfg variants
|
|
2136
|
+
// (providedFrom). `asker` (see cfg compatibility) rules out what can't be compiled together with
|
|
2137
|
+
// the path.
|
|
2138
|
+
function walkPath(segments, root, from, ctx, options = {}) {
|
|
2139
|
+
const branches = []
|
|
2140
|
+
const r = walkOnce(segments, root, from, ctx, options, branches)
|
|
2141
|
+
return branches.length === 0 || r === null || r === VALUE_ONLY || r.file === undefined ? r : throughBranches(r, branches, root, from, ctx, options)
|
|
2142
|
+
}
|
|
2143
|
+
// The rest of a path through each module a segment may name (`branches`, see withAlternatives:
|
|
2144
|
+
// `{ keyed, rest }`, the segments after it): `r`, the answer through the first, with the files the
|
|
2145
|
+
// others lead to as its `alternatives` -- each module's walked on from the crate root, an item's
|
|
2146
|
+
// the item's file -- under the key of the module they go through.
|
|
2147
|
+
function throughBranches(r, branches, root, from, ctx, options) {
|
|
2148
|
+
const alternatives = new Map()
|
|
2149
|
+
const note = (key, answer) => {
|
|
2150
|
+
for (const [k, f] of answer.alternatives ?? [[null, answer.file]]) {
|
|
2151
|
+
if (f === undefined || [...alternatives.values()].includes(f)) continue
|
|
2152
|
+
const base = k === null ? key : `${key}, ${k}`
|
|
2153
|
+
let unique = base
|
|
2154
|
+
for (let n = 2; alternatives.has(unique); n++) unique = `${base}#${n}`
|
|
2155
|
+
alternatives.set(unique, f)
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
const [{ keyed: firstKeyed }] = branches
|
|
2159
|
+
note([...firstKeyed.keys()][0], r)
|
|
2160
|
+
for (const { keyed, rest } of branches) {
|
|
2161
|
+
for (const [key, answer] of [...keyed].slice(1)) {
|
|
2162
|
+
if (answer.kind !== 'module') note(key, answer)
|
|
2163
|
+
else {
|
|
2164
|
+
const there = walkPath(['crate', ...answer.modulePath.split('::').slice(1), ...rest], root, from, ctx, options)
|
|
2165
|
+
if (there !== null && there !== VALUE_ONLY) note(key, there)
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
return alternatives.size > 1 ? { ...r, alternatives } : r
|
|
2170
|
+
}
|
|
2171
|
+
function walkOnce(segments, root, from, ctx, { ns = null, file, asker } = {}, branches) {
|
|
2172
|
+
const tree = ctx.trees.get(root)
|
|
2173
|
+
const head = segments[0]
|
|
2174
|
+
// A segment with more after it names a module or type-namespace item: a `fn log` doesn't lead
|
|
2175
|
+
// `log::info!`, the crate does (`type`, see pick); the last one is in the namespace the caller
|
|
2176
|
+
// wants (`type` for an import followed as a path's prefix, `macro` for one invoked).
|
|
2177
|
+
const nsOf = (last) => (last ? ns : 'type')
|
|
2178
|
+
const isModule = (path, last) => tree.has(path) && !(last && ns === 'macro') // a `mod m` is no `m!`
|
|
2179
|
+
const lookup = (at, name, want) => (at === from && file !== undefined ? providedFrom(root, at, name, ctx, file, asker, want) : provided(root, at, name, from, ctx, asker, want))
|
|
2180
|
+
let cur = from.split('::')
|
|
2181
|
+
let i = 0
|
|
2182
|
+
let via
|
|
2183
|
+
if (head === 'crate') {
|
|
2184
|
+
cur = ['crate']
|
|
2185
|
+
i = 1
|
|
2186
|
+
} else if (head === 'self') {
|
|
2187
|
+
i = 1
|
|
2188
|
+
} else if (head !== 'super' && !isModule(`${from}::${head}`, segments.length === 1)) {
|
|
2189
|
+
const want = nsOf(segments.length === 1)
|
|
2190
|
+
const p = lookup(from, head, want)
|
|
2191
|
+
if (p === null || p === VALUE_ONLY) {
|
|
2192
|
+
// An item of the module itself (`enum Kind { … } use Kind::*;`), or a macro of the file.
|
|
2193
|
+
const own = definedIn(root, from, head, ctx, asker, want)
|
|
2194
|
+
if (own !== null) return itemAt(own)
|
|
2195
|
+
if (want === 'type' && (p === VALUE_ONLY || onlyValues(root, from, head, ctx))) return VALUE_ONLY
|
|
2196
|
+
return segments.length === 1 && file !== undefined && ctx.fileMacros.get(file)?.has(head) === true ? { kind: 'item', file } : null
|
|
2197
|
+
}
|
|
2198
|
+
via = { modulePath: from, consumed: 0, through: p.through }
|
|
2199
|
+
if (p.kind === 'crate') return { ...p, via, rest: segments.slice(1) } // what the path names in that crate (followImport)
|
|
2200
|
+
if (p.kind !== 'module') return { ...p, via }
|
|
2201
|
+
if (p.branches !== undefined) branches.push({ keyed: p.branches, rest: segments.slice(1) })
|
|
2202
|
+
cur = p.modulePath.split('::')
|
|
2203
|
+
i = 1
|
|
2204
|
+
}
|
|
2205
|
+
while (segments[i] === 'super') {
|
|
2206
|
+
cur.pop()
|
|
2207
|
+
i++
|
|
2208
|
+
if (cur.length === 0) return null
|
|
2209
|
+
}
|
|
2210
|
+
while (i < segments.length) {
|
|
2211
|
+
const at = cur.join('::')
|
|
2212
|
+
const name = segments[i]
|
|
2213
|
+
const last = i === segments.length - 1
|
|
2214
|
+
// (A macro is looked up as any name is: the crate root's `#[macro_export]` definitions are
|
|
2215
|
+
// among its bindings, each under its cfgs -- see providedAll.)
|
|
2216
|
+
const macro = at === 'crate' && last && ns !== 'macro' ? ctx.macros.get(root)?.get(name) : undefined
|
|
2217
|
+
const child = `${at}::${name}`
|
|
2218
|
+
const want = nsOf(last)
|
|
2219
|
+
let p
|
|
2220
|
+
if (isModule(child, last)) {
|
|
2221
|
+
// A child module the asker's build doesn't compile (under a cfg off in it) gives way to what
|
|
2222
|
+
// else the module has of the name, if anything; so does one it presumably doesn't (under a
|
|
2223
|
+
// custom cfg, doubtful: serde's docsrs-only `mod de` beside the `pub use serde_core::de` of
|
|
2224
|
+
// every other build; a `#[cfg(loom)] mod imp` beside the `use other::*` that brings `imp`
|
|
2225
|
+
// in), as a doubtful candidate gives way to any other: where the module isn't there, a
|
|
2226
|
+
// glob's name is the module's name. Only a module none of whose files may be there: one
|
|
2227
|
+
// with a variant the build compiles (`#[cfg_attr(loom, path = "loom.rs")] mod imp;` falls
|
|
2228
|
+
// back to imp.rs) is there, and shadows the glob.
|
|
2229
|
+
const sets = (ctx.moduleFiles?.get(root)?.get(child) ?? [tree.get(child)]).map((f) => ctx.files.get(f)?.leaves).filter((set) => set !== undefined)
|
|
2230
|
+
const away = sets.length > 0 && sets.every((set) => deadFor(asker, set) || doubtful(asker, set))
|
|
2231
|
+
p = away ? lookup(at, name, want) : null
|
|
2232
|
+
if (p === null || p === VALUE_ONLY) {
|
|
2233
|
+
// One file there only under cfgs the asker doesn't hold (`#[cfg(not(unix))] mod sys;`
|
|
2234
|
+
// beside `#[cfg(unix)] use fallback as sys;`): what else the module binds the name to is a
|
|
2235
|
+
// branch of the path too (see throughBranches), under no key of its own. (A `mod` with cfg
|
|
2236
|
+
// variants is taken to be there in every build: one of them, a `path` fallback's; and one
|
|
2237
|
+
// under a custom cfg the build may set is there, as above.)
|
|
2238
|
+
if (!away && sets.length === 1 && !entailed(asker, sets[0]) && !hasCustom(sets[0].leaves, customKey)) {
|
|
2239
|
+
const other = lookup(at, name, want)
|
|
2240
|
+
if (other !== null && other !== VALUE_ONLY && !(other.kind === 'module' && other.modulePath === child)) branches.push({ keyed: new Map([[cfgKey(cfgTextOf(sets[0].leaves)), { kind: 'module', modulePath: child, file: tree.get(child) }], ['*', other]]), rest: segments.slice(i + 1) })
|
|
2241
|
+
}
|
|
2242
|
+
cur.push(name)
|
|
2243
|
+
i++
|
|
2244
|
+
continue
|
|
2245
|
+
}
|
|
2246
|
+
} else p = lookup(at, name, want)
|
|
2247
|
+
if (p === null || p === VALUE_ONLY) {
|
|
2248
|
+
if (macro !== undefined) return { kind: 'item', file: macro, via }
|
|
2249
|
+
const def = definedIn(root, at, name, ctx, asker, want)
|
|
2250
|
+
if (def !== null) return { ...itemAt(def), via }
|
|
2251
|
+
if (want === 'type' && (p === VALUE_ONLY || onlyValues(root, at, name, ctx))) return VALUE_ONLY
|
|
2252
|
+
return { kind: 'item', file: tree.get(at), via }
|
|
2253
|
+
}
|
|
2254
|
+
via ??= { modulePath: at, consumed: i, through: p.through }
|
|
2255
|
+
if (p.kind === 'crate') return { ...p, via, rest: segments.slice(i + 1) }
|
|
2256
|
+
if (p.kind !== 'module') return { ...p, via }
|
|
2257
|
+
if (p.branches !== undefined) branches.push({ keyed: p.branches, rest: segments.slice(i + 1) })
|
|
2258
|
+
cur = p.modulePath.split('::')
|
|
2259
|
+
i++
|
|
2260
|
+
}
|
|
2261
|
+
const modulePath = cur.join('::')
|
|
2262
|
+
return { kind: 'module', modulePath, file: tree.get(modulePath), via }
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// Whether a glob in module `at`, or in a module its globs reach, may bring `lead` in without the
|
|
2266
|
+
// loader seeing it -- a glob into a crate that isn't in-tree (`use syn::*;` brings syn's
|
|
2267
|
+
// `punctuated` module in) -- or does bring it in from another in-tree crate: from the module the
|
|
2268
|
+
// glob's path names there (`use tokio::sync::*;` brings tokio's `sync::mpsc` in), what it has
|
|
2269
|
+
// (hasAll) or its own globs may bring in, in turn. Then a lead nothing here explains is not one to
|
|
2270
|
+
// report as a missing crate. A glob the loader sees through -- into a module of this crate, an
|
|
2271
|
+
// enum, the sysroot, or an in-tree crate's module without the name -- explains nothing, and no
|
|
2272
|
+
// glob brings in its own crate's name. Memoized per (at, lead); asked again while being answered
|
|
2273
|
+
// (globs between two crates leading back), it reads as no. Only the globs leading out of the
|
|
2274
|
+
// crate matter (opaqueGlobsOf), so a closure of hundreds costs a lookup per such glob.
|
|
2275
|
+
// The globs of `root`'s crate that lead out of it -- into a crate that isn't in-tree (source
|
|
2276
|
+
// null) or into another in-tree one -- by module, with their sources, found once per crate (and
|
|
2277
|
+
// again after stabilizeClosures): the few of a closure's hundreds of modules globMayProvide has
|
|
2278
|
+
// to look at.
|
|
2279
|
+
function opaqueGlobsOf(root, ctx) {
|
|
2280
|
+
return cached(ctx.opaqueGlobs, root, () => {
|
|
2281
|
+
const index = new Map()
|
|
2282
|
+
for (const [module, of] of ctx.imports.get(root) ?? []) {
|
|
2283
|
+
for (const im of of.globs) {
|
|
2284
|
+
if (NON_CRATE_LEADS.has(im.segments[0])) continue
|
|
2285
|
+
const source = globSource(im, root, ctx)
|
|
2286
|
+
if (source === null || source.kind === 'crate') (index.get(module) ?? index.set(module, []).get(module)).push({ im, source })
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return index
|
|
2290
|
+
})
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
function globMayProvide(root, at, lead, ctx) {
|
|
2294
|
+
const memo = ctx.opaque.get(root) ?? ctx.opaque.set(root, new Map()).get(root)
|
|
2295
|
+
const key = `${at}\0${lead}`
|
|
2296
|
+
if (memo.has(key)) return memo.get(key)
|
|
2297
|
+
memo.set(key, false)
|
|
2298
|
+
// The globs leading out of the crate that `at` sees -- its own and its closure's, at the depth
|
|
2299
|
+
// each module is seen (a module may be reached at several) -- gathered once per `at`: the ones
|
|
2300
|
+
// into crates not in-tree by the crate name they bring nothing of (their own), the ones into
|
|
2301
|
+
// in-tree crates with their sources.
|
|
2302
|
+
const reachKey = `${at}\0`
|
|
2303
|
+
let reach = memo.get(reachKey)
|
|
2304
|
+
if (reach === undefined) {
|
|
2305
|
+
const seeing = at.split('::').length
|
|
2306
|
+
const closure = globClosure(root, at, seeing, ctx)
|
|
2307
|
+
const positions = closureIndex(closure)
|
|
2308
|
+
reach = { unseen: new Set(), crates: [] }
|
|
2309
|
+
for (const [module, globs] of opaqueGlobsOf(root, ctx)) {
|
|
2310
|
+
let sees = module === at ? seeing : -1
|
|
2311
|
+
for (const i of positions.get(module) ?? NONE) sees = Math.max(sees, closure[i].seeing)
|
|
2312
|
+
if (sees === -1) continue
|
|
2313
|
+
for (const { im, source } of globs) {
|
|
2314
|
+
if (im.scopeDepth > sees) continue
|
|
2315
|
+
if (source === null) reach.unseen.add(im.segments[0])
|
|
2316
|
+
else reach.crates.push({ im, source })
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
memo.set(reachKey, reach)
|
|
2320
|
+
}
|
|
2321
|
+
// What another crate's module `module` (root `file`) makes visible under `lead`, as far as the
|
|
2322
|
+
// bundle sees: anything, for a crate not in-tree; else what the module has, what its `pub`
|
|
2323
|
+
// globs bring in from its own crate (providedAll), or may from a third.
|
|
2324
|
+
const inCrate = (file, module) => !ctx.trees.has(file) || hasAll(file, module, lead, 1, ctx).length > 0 || providedAll(file, module, lead, 1, ctx).get(0) !== undefined || globMayProvide(file, module, lead, ctx)
|
|
2325
|
+
let answer = reach.unseen.size > (reach.unseen.has(lead) ? 1 : 0)
|
|
2326
|
+
for (const { im, source } of reach.crates) {
|
|
2327
|
+
if (answer || im.segments[0] === lead) break
|
|
2328
|
+
// The glob's path on from the crate it names (`sync` of `use tokio::sync::*;`), when the
|
|
2329
|
+
// lead itself named the crate (no `via`: the path wasn't anchored in this crate first).
|
|
2330
|
+
const rest = source.via === undefined ? im.segments.slice(1) : []
|
|
2331
|
+
const there = rest.length === 0 || !ctx.trees.has(source.file) ? { kind: 'module', modulePath: 'crate' } : walkPath(['crate', ...rest], source.file, 'crate', ctx)
|
|
2332
|
+
if ((there?.kind === 'module' && inCrate(source.file, there.modulePath)) || (there?.kind === 'crate' && inCrate(there.file, 'crate'))) answer = true
|
|
2333
|
+
}
|
|
2334
|
+
memo.set(key, answer)
|
|
2335
|
+
return answer
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// Resolve one path reference made in `file` to what it names in the bundle (see walkPath): a
|
|
2339
|
+
// module or item of the same crate, an in-tree crate (`{ kind: 'crate', name, file }`, through
|
|
2340
|
+
// `resolveCrate` when the lead names one, by its own name or as the crate root's `extern crate x
|
|
2341
|
+
// as y;` aliases it, `::y::…` included -- this crate's own root for `extern crate self as y;`),
|
|
2342
|
+
// an exported macro named by a one-segment path written at the crate root, where such a macro is
|
|
2343
|
+
// an item (`pub use anyhow as format_err;` in the anyhow crate's lib.rs; a child module reaches it
|
|
2344
|
+
// as `crate::anyhow`), or `{ kind: 'unresolved', name }` when the lead could name a crate
|
|
2345
|
+
// (crateLead) but nothing in-tree did; null for anything else (an item in scope, std, a path
|
|
2346
|
+
// above the crate root, a name a glob into another crate may well provide). `bindings` are the names `file`'s imports bind -- in whichever namespace: one bound
|
|
2347
|
+
// only as a value (`use crate::util::log;` of a `fn log`) doesn't keep `log::info!` from naming
|
|
2348
|
+
// the crate.
|
|
2349
|
+
const NO_BINDINGS = new Set()
|
|
2350
|
+
function resolvePathRef(ref, file, bindings, ctx) {
|
|
2351
|
+
const { segments, absolute, inlinePath } = ref
|
|
2352
|
+
if (segments.length === 0) return null
|
|
2353
|
+
const here = ctx.files.get(file)
|
|
2354
|
+
const from = here ? [here.modulePath, ...inlinePath].join('::') : null
|
|
2355
|
+
let valueOnly = false
|
|
2356
|
+
if (!absolute && here) {
|
|
2357
|
+
const r = walkPath(segments, here.root, from, ctx, { ns: ref.macroCall ? 'macro' : null, file, asker: here.asker })
|
|
2358
|
+
if (r === VALUE_ONLY) valueOnly = true // the lead is bound here, but as a value: `log::…` names the crate
|
|
2359
|
+
else if (r !== null && r.kind === 'crate' && ref.macroCall && r.rest?.length > 0) return { ...r, macro: macroOfCrate(r.file, r.rest, ctx) ?? undefined } // `d::mac!(…)` with `use dep as d;`
|
|
2360
|
+
else if (r !== null) return r
|
|
2361
|
+
}
|
|
2362
|
+
const lead = crateLead(ref, valueOnly ? NO_BINDINGS : bindings)
|
|
2363
|
+
if (lead === null) return null
|
|
2364
|
+
const name = ctx.externPrelude.get(here?.root)?.get(lead) ?? lead
|
|
2365
|
+
if (name === 'self') {
|
|
2366
|
+
const r = walkPath(['crate', ...segments.slice(1)], here.root, from, ctx, { ns: ref.macroCall ? 'macro' : null, file, asker: here.asker })
|
|
2367
|
+
return r === VALUE_ONLY ? null : r
|
|
2368
|
+
}
|
|
2369
|
+
const target = ctx.resolveCrate(name, file)
|
|
2370
|
+
// `dep::mac!(…)`: the crate, and the macro it names there (macroOfCrate).
|
|
2371
|
+
if (target) return { kind: 'crate', name, file: target, alternatives: ctx.crateAlternatives?.(name, file), macro: ref.macroCall && segments.length > 1 ? macroOfCrate(target, segments.slice(1), ctx) ?? undefined : undefined }
|
|
2372
|
+
const macro = segments.length === 1 && from === 'crate' ? ctx.macros.get(here.root)?.get(lead) : undefined
|
|
2373
|
+
if (macro !== undefined) return { kind: 'item', file: macro }
|
|
2374
|
+
// A glob into a crate that isn't in-tree (`use serde::*;`) may bring the lead in -- unless the
|
|
2375
|
+
// package declares a dependency of that name, which the lead then is (see declaresCrate): one
|
|
2376
|
+
// missing from the bundle is reported whatever the globs beside it.
|
|
2377
|
+
return here && !ctx.declaresCrate(name, file) && globMayProvide(here.root, from, lead, ctx) ? null : { kind: 'unresolved', name }
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// `map`: root → name → Set<module>, noting `module` under (root, name).
|
|
2381
|
+
const note = (map, root, name, module) => {
|
|
2382
|
+
const byName = map.get(root) ?? map.set(root, new Map()).get(root)
|
|
2383
|
+
;(byName.get(name) ?? byName.set(name, new Set()).get(name)).add(module)
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// Bundle import keys can't contain '/'; a cfg predicate practically never does, but fail safe.
|
|
2387
|
+
const cfgKey = (cfg) => (cfg ?? '*').replaceAll('/', '|')
|
|
2388
|
+
|
|
2389
|
+
// Whether `rel` is compiled with `cfg(test)`: a file of a test/bench target (`tests/*.rs`,
|
|
2390
|
+
// `benches/*.rs` and their modules), judged against its package dir when a Cargo context knows it.
|
|
2391
|
+
const isTestTarget = (rel, ctx) => (ctx ? ctx.isTestTarget(rel) : isTestTargetPath('.', rel))
|
|
2392
|
+
|
|
2393
|
+
// The files a macro is in effect invoked from, by name: those invoking it by bare name
|
|
2394
|
+
// (`invokers`: name → files), and -- since a bare call in a `macro_rules!` template is made where
|
|
2395
|
+
// that macro is invoked (`templateCalls`: name → the names its definitions' templates call) --
|
|
2396
|
+
// those invoking a macro whose template calls it, on up: what rustc resolves the macro's include
|
|
2397
|
+
// paths against, the outermost call site.
|
|
2398
|
+
function effectiveInvokers(name, invokers, templateCalls, seen = new Set()) {
|
|
2399
|
+
if (seen.has(name)) return new Set()
|
|
2400
|
+
seen.add(name)
|
|
2401
|
+
const out = new Set(invokers.get(name) ?? [])
|
|
2402
|
+
for (const [caller, called] of templateCalls) if (called.has(name)) for (const f of effectiveInvokers(caller, invokers, templateCalls, seen)) out.add(f)
|
|
2403
|
+
return out
|
|
2404
|
+
}
|
|
2405
|
+
// Note what file `path`'s scan (`items`) invokes, by bare name or by path (`crate::m!()`, matched by
|
|
2406
|
+
// name: a `$crate::m!` resolves where the macro is invoked as surely as `m!`), and what its
|
|
2407
|
+
// `macro_rules!` templates call, for effectiveInvokers.
|
|
2408
|
+
function noteMacroCalls(invokers, templateCalls, path, items) {
|
|
2409
|
+
for (const name of [...items.invocations, ...items.pathInvocations ?? []]) (invokers.get(name) ?? invokers.set(name, new Set()).get(name)).add(path)
|
|
2410
|
+
for (const m of items.macros) for (const name of [...m.calls, ...m.pathCalls ?? []]) (templateCalls.get(m.name) ?? templateCalls.set(m.name, new Set()).get(m.name)).add(name)
|
|
2411
|
+
}
|
|
2412
|
+
// The files in effect invoking macro `name` where a `mod` its template declares is declared: of
|
|
2413
|
+
// `from`'s package (`packageOf`: file → package) -- a macro of the same name elsewhere is likely
|
|
2414
|
+
// another -- or, for a `#[macro_export]` one (`packageOf` null), of any (see buildRustTree).
|
|
2415
|
+
const hostsOf = (name, from, invokers, templateCalls, packageOf) => [...effectiveInvokers(name, invokers, templateCalls)].filter((f) => packageOf === null || packageOf(f) === packageOf(from))
|
|
2416
|
+
// The copies of template `mod` `decl` (written in a `macro_rules!` body, see scanRustItems) a file
|
|
2417
|
+
// invoking the macro declares (`items`: its scan): one per inline module it invokes the macro, or a
|
|
2418
|
+
// macro whose template calls it, from -- the `mod` in that module, inside the inline modules the
|
|
2419
|
+
// body itself opens -- at that invocation (`offset`, for textual macro scope).
|
|
2420
|
+
function hostedCopies(decl, items, templateCalls) {
|
|
2421
|
+
const names = new Set([decl.template])
|
|
2422
|
+
for (let grew = true; grew;) {
|
|
2423
|
+
grew = false
|
|
2424
|
+
for (const [caller, called] of templateCalls) if (!names.has(caller) && [...called].some((n) => names.has(n))) grew = names.add(caller)
|
|
2425
|
+
}
|
|
2426
|
+
const sites = [...names].flatMap((n) => items.callSites?.get(n) ?? []).toSorted((a, b) => a.offset - b.offset)
|
|
2427
|
+
const byModule = new Map()
|
|
2428
|
+
for (const site of sites) if (!byModule.has(site.inlinePath.join('::'))) byModule.set(site.inlinePath.join('::'), site)
|
|
2429
|
+
if (byModule.size === 0) byModule.set('', { offset: Infinity, inlinePath: [], inlineDirs: [] })
|
|
2430
|
+
const own = (list) => list.slice(decl.templateDepth ?? 0)
|
|
2431
|
+
return [...byModule.values()].map((site) => ({ ...decl, inlinePath: [...site.inlinePath, ...own(decl.inlinePath)], inlineDirs: [...site.inlineDirs, ...own(decl.inlineDirs)], offset: site.offset }))
|
|
2432
|
+
}
|
|
2433
|
+
// TEMPLATE_MACROS less the ones a package defines itself (`own`), one object per set of them.
|
|
2434
|
+
const templatesLessMemo = new Map()
|
|
2435
|
+
const templatesLess = (own) => {
|
|
2436
|
+
const key = [...own].toSorted().join()
|
|
2437
|
+
return templatesLessMemo.get(key) ?? templatesLessMemo.set(key, new Set([...TEMPLATE_MACROS].filter((name) => !own.has(name)))).get(key)
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// The compile units a file is compiled as (see cargo.js unitKey): what `units` recorded, else
|
|
2441
|
+
// the target's alone.
|
|
2442
|
+
const DEFAULT_UNITS = new Set([TARGET_UNIT])
|
|
2443
|
+
const unitsOf = (units, rel) => units?.get(rel) ?? DEFAULT_UNITS
|
|
2444
|
+
// Add each of `more` to `map`'s set for `file` (the units it is compiled as, the crates it is
|
|
2445
|
+
// compiled in); whether that is news.
|
|
2446
|
+
function addTo(map, file, more) {
|
|
2447
|
+
const have = map.get(file)
|
|
2448
|
+
if (have === undefined) {
|
|
2449
|
+
map.set(file, new Set(more))
|
|
2450
|
+
return true
|
|
2451
|
+
}
|
|
2452
|
+
if (more.isSubsetOf(have)) return false
|
|
2453
|
+
for (const u of more) have.add(u)
|
|
2454
|
+
return true
|
|
2455
|
+
}
|
|
2456
|
+
// `units` (a new map for none) with each entry it doesn't name compiled as its package is
|
|
2457
|
+
// (unitOfCrate): a proc-macro crate's for the host, as cargo builds it, any other's for the target.
|
|
2458
|
+
function withEntryUnits(units, entries, ctx) {
|
|
2459
|
+
const out = units ?? new Map()
|
|
2460
|
+
for (const e of entries) if (!out.has(e)) out.set(e, new Set([ctx?.unitOfCrate(e, TARGET_UNIT) ?? TARGET_UNIT]))
|
|
2461
|
+
return out
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// The build `rel` is scanned under (scanRustItems' options): per unit it is compiled as, its
|
|
2465
|
+
// crate's features in that unit's feature context -- on for certain, and on maybe -- and the cfg
|
|
2466
|
+
// set of the platform the unit compiles for, as far as the Cargo context `ctx` knows them; and
|
|
2467
|
+
// whether it is test code. A build script is compiled for the host, whose cfgs the loader knows
|
|
2468
|
+
// only when the target is the host; a crate built both for the target and, as a
|
|
2469
|
+
// build-dependency, for the host is scanned under both. Interned per (package, test flag,
|
|
2470
|
+
// units), so a scan of the same build is reused (cachedScan compares identity).
|
|
2471
|
+
const buildMemo = new WeakMap() // ctx -> key -> build
|
|
2472
|
+
const NO_CTX = {}
|
|
2473
|
+
function buildOf(rel, ctx, units = null) {
|
|
2474
|
+
const test = isTestTarget(rel, ctx)
|
|
2475
|
+
const set = unitsOf(units, rel)
|
|
2476
|
+
const key = `${ctx?.packageInfo(rel)?.dir ?? ''}\0${test}\0${[...set].toSorted().join()}`
|
|
2477
|
+
const memo = buildMemo.get(ctx ?? NO_CTX) ?? buildMemo.set(ctx ?? NO_CTX, new Map()).get(ctx ?? NO_CTX)
|
|
2478
|
+
return cached(memo, key, () => {
|
|
2479
|
+
const one = (unit) => ({ features: ctx?.featuresFor(rel, unit) ?? null, maybeFeatures: ctx?.maybeFeaturesFor(rel, unit) ?? null, target: ctx?.platformOf(unit)?.cfgs ?? null })
|
|
2480
|
+
const each = [...set].map(one)
|
|
2481
|
+
return each.length === 1 ? { ...each[0], test } : { units: each, test }
|
|
2482
|
+
})
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// Scan results per `sources` map, so the tree pass reuses the walk's scan of each file when it
|
|
2486
|
+
// ran under the same build (one interned object, see buildOf) on the same content (a caller may
|
|
2487
|
+
// edit a file in a map it reuses).
|
|
2488
|
+
const scanCache = new WeakMap()
|
|
2489
|
+
function cachedScan(sources, path, content, build, templates = undefined) {
|
|
2490
|
+
const cache = cached(scanCache, sources, () => new Map())
|
|
2491
|
+
const key = templates === undefined ? path : `${path}\0${[...templates].join()}` // a rescan with fewer templates, kept beside the plain scan
|
|
2492
|
+
const hit = cache.get(key)
|
|
2493
|
+
if (hit && hit.content === content && hit.build === build) return hit.items
|
|
2494
|
+
const items = scanRustItems(content, templates === undefined ? build : { ...build, templates })
|
|
2495
|
+
cache.set(key, { build, content, items })
|
|
2496
|
+
return items
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// Build the triple from already-loaded sources. Two-phase: resolve `mod` edges (defining each
|
|
2500
|
+
// crate root's module tree), then path references against it. An unconditional `mod` with no
|
|
2501
|
+
// file → `missing` (fatal); cfg-gated/unresolved/self refs are omitted. `roots` are the entries
|
|
2502
|
+
// (crate roots by role; main.rs/lib.rs are roots by name regardless); `baseDir` enables crate
|
|
2503
|
+
// resolution through Cargo.toml (own lib, path deps), else only vendored crates already in
|
|
2504
|
+
// `sources` resolve. Spec shapes in `resolutions`: `mod <name>` (`mod a::<name>` inside inline
|
|
2505
|
+
// module `a`) → file, or a Map<cfg predicate | '*', file> when `#[cfg_attr(…, path)]` variants or
|
|
2506
|
+
// same-name `#[cfg]`-gated declarations name different files; `<path as written>` → module file;
|
|
2507
|
+
// `use <crate>` → crate root. `unresolvedCrates` names the crates `use`/`extern crate` referenced
|
|
2508
|
+
// that nothing in-tree satisfied (registry deps that weren't vendored, typically) --
|
|
2509
|
+
// informational, never fatal. `wantedRoots` are in-tree crate roots the paths named that
|
|
2510
|
+
// `sources` lacks (the walk took their names for bound items): loaded and built again, they
|
|
2511
|
+
// complete the bundle. A `cargo` context (cargo.js) may be passed in to share one
|
|
2512
|
+
// between the walk and this pass; it also carries each crate's resolved features, which decide
|
|
2513
|
+
// `#[cfg(feature = …)]`, and the build target's cfg set when one was given, which decides
|
|
2514
|
+
// `#[cfg(unix)]` and the like. `units`: file → the compile units it is compiled as (see
|
|
2515
|
+
// collectRustFilesFromDisk); `wantedUnits` gives each wanted root's, for the walk.
|
|
2516
|
+
export function buildRustTree(sources, { roots = [], baseDir = null, cargo = null, formats = null, units: given = null } = {}) {
|
|
2517
|
+
const ctx = cargo ?? (baseDir ? createCargoContext(baseDir, { entries: roots }) : null)
|
|
2518
|
+
const units = withEntryUnits(given, roots, ctx)
|
|
2519
|
+
const rootSet = crateRoots(sources, roots, ctx)
|
|
166
2520
|
const resolutions = new Map()
|
|
167
2521
|
const missing = []
|
|
2522
|
+
const scanned = new Map()
|
|
168
2523
|
for (const [path, content] of sources) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
2524
|
+
if (formats?.has(path)) continue // an `include_str!` / `include_bytes!` asset: carried, not Rust
|
|
2525
|
+
scanned.set(path, cachedScan(sources, path, content, buildOf(path, ctx, units)))
|
|
2526
|
+
}
|
|
2527
|
+
// A package with a `macro_rules!` of a template macro's name (its own `quote!`, in whichever of
|
|
2528
|
+
// its files) invokes that one everywhere in the package: no template there, so the files of the
|
|
2529
|
+
// package that skipped a body of that name are scanned again with the name dropped from the set
|
|
2530
|
+
// (the package: the Cargo context's; without one, a vendored crate's `vendor/<dir>`, else the
|
|
2531
|
+
// bundle's).
|
|
2532
|
+
const packages = new Map() // path → its package, asked for every import and item (gatesOf)
|
|
2533
|
+
const packageOf = (path) => cached(packages, path, () => (ctx ? ctx.packageInfo(path)?.dir ?? '.' : path.startsWith(`${VENDOR_DIR}/`) ? path.split('/', 2).join('/') : '.'))
|
|
2534
|
+
const ownTemplates = new Map() // package → the template names it defines
|
|
2535
|
+
for (const [path, items] of scanned) {
|
|
2536
|
+
for (const m of items.macros) if (TEMPLATE_MACROS.has(m.name)) cached(ownTemplates, packageOf(path), () => new Set()).add(m.name)
|
|
2537
|
+
}
|
|
2538
|
+
for (const [path, items] of scanned) {
|
|
2539
|
+
const own = items.skipped.size > 0 ? ownTemplates.get(packageOf(path)) : undefined
|
|
2540
|
+
if (own === undefined || own.isDisjointFrom(items.skipped)) continue
|
|
2541
|
+
scanned.set(path, cachedScan(sources, path, sources.get(path), buildOf(path, ctx, units), templatesLess(own)))
|
|
2542
|
+
}
|
|
2543
|
+
// Bare macro invocations per file, for what a `macro_rules!` body holds: rustc expands it where
|
|
2544
|
+
// the macro is invoked -- through the templates that call it, the outermost (effectiveInvokers).
|
|
2545
|
+
const invokers = new Map()
|
|
2546
|
+
const templateCalls = new Map()
|
|
2547
|
+
for (const [path, items] of scanned) noteMacroCalls(invokers, templateCalls, path, items)
|
|
2548
|
+
// A `mod` a `macro_rules!` template declares is declared in each module invoking the macro, and
|
|
2549
|
+
// its file found from there (serde_core's src/crate_root.rs holds `crate_root! { … pub mod de;
|
|
2550
|
+
// … }`, which lib.rs invokes: src/de/mod.rs), in the inline module the invocation is in, by
|
|
2551
|
+
// bare name or by path (`crate::m!()`, `$crate::m!()` in another template). Only its own
|
|
2552
|
+
// package's invocations count -- a macro of the same name elsewhere is likely another -- unless
|
|
2553
|
+
// the macro is `#[macro_export]`ed, and one nothing invokes stays beside its definition. A
|
|
2554
|
+
// hosted `mod` stands at the host's invocation (`offset`), for textual macro scope, and keeps the file defining it
|
|
2555
|
+
// (`definedIn`): the host's cfgs on mounting that file are its own too (serde's lib.rs mounts
|
|
2556
|
+
// core/crate_root.rs under `#[cfg(docsrs)]`).
|
|
2557
|
+
const hosted = new Map() // file → the template `mod`s it declares by invoking their macros
|
|
2558
|
+
const hostedAway = new Set() // the template `mod`s that moved to their invokers
|
|
2559
|
+
for (const [path, items] of scanned) {
|
|
2560
|
+
for (const decl of items.mods) {
|
|
2561
|
+
if (decl.template === null) continue
|
|
2562
|
+
const exported = items.macros.some((m) => m.name === decl.template && m.exported)
|
|
2563
|
+
const hosts = hostsOf(decl.template, path, invokers, templateCalls, exported ? null : packageOf)
|
|
2564
|
+
if (hosts.length === 0) continue
|
|
2565
|
+
hostedAway.add(decl)
|
|
2566
|
+
for (const f of hosts) for (const copy of hostedCopies(decl, scanned.get(f), templateCalls)) (hosted.get(f) ?? hosted.set(f, []).get(f)).push({ ...copy, definedIn: path })
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
const modsOf = (path) => [...scanned.get(path).mods.filter((d) => !hostedAway.has(d)), ...(hosted.get(path) ?? [])]
|
|
2570
|
+
// An `include!`d file's items join the includer's module (buildModuleTrees), but a `mod` it
|
|
2571
|
+
// declares resolves beside the included file itself, as rustc does: like a `#[path]`-loaded
|
|
2572
|
+
// file, it owns its directory (pathLoaded).
|
|
2573
|
+
const pathLoaded = new Set()
|
|
2574
|
+
for (const [path, items] of scanned) {
|
|
2575
|
+
for (const inc of items.includes) {
|
|
2576
|
+
if (inc.kind !== 'include') continue
|
|
2577
|
+
const file = includeTarget(inc, path, ctx)
|
|
2578
|
+
if (file !== null && file !== path && sources.has(file)) pathLoaded.add(file)
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
// Files a `#[path]` names own their directory too (see getModuleDir), so find those before
|
|
2582
|
+
// resolving any default `mod` lookup; a path-loaded file may itself hold inline modules with
|
|
2583
|
+
// `#[path]`s, hence the loop to a fixed point.
|
|
2584
|
+
const modOpts = { knownSources: sources, roots: rootSet, pathLoaded }
|
|
2585
|
+
const declsOf = (path, decl) => resolveModDecl(decl, path, { ...modOpts, boundary: boundaryOf(path, ctx) })
|
|
2586
|
+
for (let grew = true; grew;) {
|
|
2587
|
+
grew = false
|
|
2588
|
+
for (const path of scanned.keys()) {
|
|
2589
|
+
for (const decl of modsOf(path)) {
|
|
2590
|
+
if (decl.paths.length === 0) continue
|
|
2591
|
+
for (const t of declsOf(path, decl)) {
|
|
2592
|
+
if (t.explicit && !pathLoaded.has(t.file)) {
|
|
2593
|
+
pathLoaded.add(t.file)
|
|
2594
|
+
grew = true
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
// The include macros a `macro_rules!` body holds name files relative to each file in effect
|
|
2601
|
+
// invoking the macro, recorded there, as `include_str <path>`. Every definition of a name counts
|
|
2602
|
+
// (two `macro_rules! m` in two files: both bodies' assets).
|
|
2603
|
+
const includeEdges = new Map() // file → [{ kind, path, base, conditional }] from macro templates it invokes
|
|
2604
|
+
for (const [, items] of scanned) {
|
|
2605
|
+
for (const m of items.macros) {
|
|
2606
|
+
if (m.includes.length === 0) continue
|
|
2607
|
+
for (const f of effectiveInvokers(m.name, invokers, templateCalls)) {
|
|
2608
|
+
const list = includeEdges.get(f) ?? includeEdges.set(f, []).get(f)
|
|
2609
|
+
for (const inc of m.includes) list.push({ ...inc, conditional: true })
|
|
177
2610
|
}
|
|
178
|
-
// conditional && unresolved: cfg-gated module, may be compiled out — tolerated.
|
|
179
2611
|
}
|
|
2612
|
+
}
|
|
2613
|
+
// file → spec → target file → the declarations mounting it, each its cfg (null for an ungated
|
|
2614
|
+
// one), the macro body it sits in and -- for a hosted template `mod` -- the declarations here
|
|
2615
|
+
// mounting the file defining it (`via`), for the cfg leaves each file carries (buildModuleTrees).
|
|
2616
|
+
const mountCfgs = new Map()
|
|
2617
|
+
for (const [path, items] of scanned) {
|
|
2618
|
+
// spec -> Map<cfg key, file>: each declaration's files under its cfg predicate ('*' when
|
|
2619
|
+
// ungated; `<macro>!` for one a macro emits under a cfg the loader can't see, tokio's
|
|
2620
|
+
// `cfg_has_atomic_u64! { mod imp; }`), so `#[cfg(unix)] #[path = "u.rs"] mod imp;` beside
|
|
2621
|
+
// `#[cfg(windows)] #[path = "w.rs"] mod imp;` keeps both files. The first declaration of a
|
|
2622
|
+
// key wins; another file under the same key gets the key numbered (`*#2`) rather than lost.
|
|
2623
|
+
const byCfg = new Map()
|
|
2624
|
+
const cfgsHere = mountCfgs.set(path, new Map()).get(path)
|
|
2625
|
+
for (const decl of modsOf(path)) {
|
|
2626
|
+
const spec = `mod ${[...decl.inlinePath, decl.name].join('::')}`
|
|
2627
|
+
const targets = declsOf(path, decl)
|
|
2628
|
+
if (targets.length === 0) {
|
|
2629
|
+
// conditional && unresolved: cfg-gated module, may be compiled out -- tolerated.
|
|
2630
|
+
if (!decl.conditional) {
|
|
2631
|
+
console.warn(`[loader.rust] Missing module: ${decl.name} from ${path}`)
|
|
2632
|
+
missing.push({ spec, from: path })
|
|
2633
|
+
}
|
|
2634
|
+
continue
|
|
2635
|
+
}
|
|
2636
|
+
const keyed = byCfg.get(spec) ?? byCfg.set(spec, new Map()).get(spec)
|
|
2637
|
+
const byFile = cfgsHere.get(spec) ?? cfgsHere.set(spec, new Map()).get(spec)
|
|
2638
|
+
for (const t of targets) {
|
|
2639
|
+
const cfg = t.cfg ?? decl.cfg ?? null
|
|
2640
|
+
const base = cfgKey(cfg ?? (decl.macro === null ? null : `${decl.macro}!`))
|
|
2641
|
+
let key = base
|
|
2642
|
+
for (let k = 2; keyed.has(key) && keyed.get(key) !== t.file; k++) key = `${base}#${k}`
|
|
2643
|
+
if (!keyed.has(key)) keyed.set(key, t.file)
|
|
2644
|
+
const via = decl.definedIn === undefined ? undefined : [...cfgsHere.values()].flatMap((m) => m.get(decl.definedIn) ?? [])
|
|
2645
|
+
;(byFile.get(t.file) ?? byFile.set(t.file, []).get(t.file)).push({ cfg, macro: decl.macro, via })
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
// A single file is a flat string -- also when every cfg names the same file (libc's `mod
|
|
2649
|
+
// primitives` under a dozen); cfg variants stay a Map.
|
|
2650
|
+
const specMap = new Map()
|
|
2651
|
+
for (const [spec, keyed] of byCfg) {
|
|
2652
|
+
const distinct = new Set(keyed.values())
|
|
2653
|
+
specMap.set(spec, distinct.size === 1 ? distinct.values().next().value : keyed)
|
|
2654
|
+
}
|
|
2655
|
+
// `include!("generated/consts.rs")` and the asset macros name a file relative to this one (or
|
|
2656
|
+
// to the package root), the ones in a macro body it invokes relative to this one too.
|
|
2657
|
+
for (const inc of [...items.includes, ...(includeEdges.get(path) ?? [])]) {
|
|
2658
|
+
const file = includeTarget(inc, path, ctx)
|
|
2659
|
+
if (file !== null && file !== path && sources.has(file)) specMap.set(`${inc.kind} ${inc.path}`, file)
|
|
2660
|
+
else if (!inc.conditional) console.warn(`[loader.rust] Missing include: ${inc.path} from ${path}`)
|
|
2661
|
+
}
|
|
2662
|
+
if (items.unfollowed > 0) console.warn(`[loader.rust] ${items.unfollowed} include${items.unfollowed === 1 ? '' : 's'} in ${path} name${items.unfollowed === 1 ? 's' : ''} a path the loader can't follow (not a literal)`)
|
|
180
2663
|
resolutions.set(path, specMap)
|
|
181
2664
|
}
|
|
182
2665
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
2666
|
+
// Each package's gate macros (`cfg_x!`), by name: the cfg its definition wraps items in, or null
|
|
2667
|
+
// when that can't be read -- or when the package defines the name twice, differently.
|
|
2668
|
+
const gatesByPackage = new Map()
|
|
2669
|
+
for (const [path, items] of scanned) {
|
|
2670
|
+
for (const m of items.macros) {
|
|
2671
|
+
if (m.gate === undefined) continue
|
|
2672
|
+
const gates = cached(gatesByPackage, packageOf(path), () => new Map())
|
|
2673
|
+
gates.set(m.name, gates.has(m.name) && gates.get(m.name) !== m.gate ? null : m.gate)
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
const gatesOf = (path) => gatesByPackage.get(packageOf(path)) ?? null
|
|
2677
|
+
const { trees, files } = buildModuleTrees(sources, resolutions, rootSet, new Map([...scanned].map(([p, items]) => [p, items.inlineModules])), mountCfgs, gatesOf)
|
|
2678
|
+
// What each file asks as: itself, with the cfg leaves it was mounted under (see cfg
|
|
2679
|
+
// compatibility), as an interned leaf set (leafSets: every set of this tree, by key).
|
|
2680
|
+
const leafSets = new Map()
|
|
2681
|
+
const askerSets = new Map() // per leaf set, build and settable cfgs: what every asker of those shares
|
|
2682
|
+
const ids = new WeakMap()
|
|
2683
|
+
let nextId = 0
|
|
2684
|
+
const idOf = (object) => ids.get(object) ?? ids.set(object, nextId++).get(object)
|
|
2685
|
+
// With a file's platform known, its cfgs hold for the file: a `unix` asker never takes a
|
|
2686
|
+
// `windows` candidate, a `target_os = "linux"` one prefers the `any(android, linux, …)` branch
|
|
2687
|
+
// (see compatible). The platform is the target's for code compiled for it, the host's for a
|
|
2688
|
+
// build script (known only when the target is the host); a file compiled for both platforms
|
|
2689
|
+
// asks under the cfgs they share, which the loader knows only when they are one.
|
|
2690
|
+
const platformLeaves = new Map() // cfg set -> its leaf set
|
|
2691
|
+
const leavesOfCfgs = (cfgs) => platformLeaves.get(cfgs) ?? platformLeaves.set(cfgs, leafSet([...cfgs].flatMap(cfgLeaves).filter((l) => TARGET_CFG_KEYS.has(l.key)), leafSets)).get(cfgs)
|
|
2692
|
+
const platformCfgsOf = (path) => {
|
|
2693
|
+
const each = new Set([...unitsOf(units, path)].map((u) => ctx?.platformOf(u)?.cfgs ?? null))
|
|
2694
|
+
return each.size === 1 ? [...each][0] : null
|
|
2695
|
+
}
|
|
2696
|
+
for (const [path, f] of files) {
|
|
2697
|
+
f.leaves = leafSet(f.leaves, leafSets)
|
|
2698
|
+
const cfgs = platformCfgsOf(path)
|
|
2699
|
+
const joined = cfgs === null ? f.leaves : unionLeaves(f.leaves, leavesOfCfgs(cfgs), leafSets)
|
|
2700
|
+
// A file the target rules out (tokio's atomic_u64_as_mutex.rs and its submodules under a
|
|
2701
|
+
// 64-bit target) asks as it would where it is compiled: under its own cfgs, which the target's
|
|
2702
|
+
// contradict. One whose own contradict each other (under `not(all(test, loom))` and `all(loom,
|
|
2703
|
+
// test)` both) is compiled nowhere: every candidate is compatible with it, none certain
|
|
2704
|
+
// (`contradictory`), so its paths map all of them.
|
|
2705
|
+
const set = joined !== f.leaves && cfgExclusive(joined.leaves, joined.leaves) && !cfgExclusive(f.leaves.leaves, f.leaves.leaves) ? f.leaves : joined
|
|
2706
|
+
// What the askers of one leaf set, build and settable cfgs share (one object each).
|
|
2707
|
+
const build = buildOf(path, ctx, units)
|
|
2708
|
+
const settable = ctx?.cfgsSetFor(path) ?? null
|
|
2709
|
+
const sharedKey = `${set.key}\0${idOf(build)}\0${settable === null ? '' : idOf(settable)}`
|
|
2710
|
+
const shared = askerSets.get(sharedKey) ?? askerSets.set(sharedKey, { keys: new Set(set.leaves.map(leafKey)), compat: new Map(), sure: new Map(), doubt: new Map(), dead: new Map(), texts: new Map(), build, deadHere: deadUnder(set.leaves, build), contradictory: cfgExclusive(set.leaves, set.leaves), custom: customFor(settable) }).get(sharedKey)
|
|
2711
|
+
f.asker = { file: path, set, ...shared }
|
|
2712
|
+
}
|
|
2713
|
+
// Per crate root, each module's child modules by name (every module's parent is in its tree).
|
|
2714
|
+
const children = new Map()
|
|
2715
|
+
for (const [root, tree] of trees) {
|
|
2716
|
+
const byParent = new Map()
|
|
2717
|
+
for (const modulePath of tree.keys()) {
|
|
2718
|
+
const cut = modulePath.lastIndexOf('::')
|
|
2719
|
+
if (cut === -1) continue
|
|
2720
|
+
const parent = modulePath.slice(0, cut)
|
|
2721
|
+
;(byParent.get(parent) ?? byParent.set(parent, new Set()).get(parent)).add(modulePath.slice(cut + 2))
|
|
2722
|
+
}
|
|
2723
|
+
children.set(root, byParent)
|
|
2724
|
+
}
|
|
2725
|
+
// Per crate root, each module's files: a `mod` with cfg variants has several, any of which a
|
|
2726
|
+
// build may compile (walkPath asks whether one of them may be there at all).
|
|
2727
|
+
const moduleFiles = new Map()
|
|
2728
|
+
for (const [path, f] of files) {
|
|
2729
|
+
const byModule = moduleFiles.get(f.root) ?? moduleFiles.set(f.root, new Map()).get(f.root)
|
|
2730
|
+
;(byModule.get(f.modulePath) ?? byModule.set(f.modulePath, []).get(f.modulePath)).push(path)
|
|
2731
|
+
}
|
|
2732
|
+
// Per crate root, what a path may continue along where a segment names no module: the imports
|
|
2733
|
+
// of each module (by module path: `named` by the name each binds, and `globs`; each with the
|
|
2734
|
+
// `file` holding it, its `module`, how deep in the tree the module its visibility reaches sits,
|
|
2735
|
+
// `scopeDepth`: 1 for a `pub`, the module's own depth for a plain `use`, and its cfg `leaves`:
|
|
2736
|
+
// its file's plus its own cfg's), the items each module defines (`defined`: module → name →
|
|
2737
|
+
// [{ file, scopeDepth, ns, leaves }], `ns` the namespace: `value` for a fn/const/static, which no
|
|
2738
|
+
// path continues past, `type` for the rest), how far each module's own visibility reaches (`modScope`:
|
|
2739
|
+
// module → scopeDepth, for what a glob may bring in) and the `#[macro_export]` macros (by name →
|
|
2740
|
+
// the first file; `exportedMacros`: name → each definition, `{ file, leaves }`, its file's leaves
|
|
2741
|
+
// and its own cfg's -- serde's docsrs-only copies of serde_core's macros, a crate's per-platform
|
|
2742
|
+
// ones). `fileMacros` is file → the `macro_rules!` names it defines. `externPrelude` (root →
|
|
2743
|
+
// alias → crate) holds the root's `extern crate x as y;`, in scope crate-wide.
|
|
2744
|
+
const imports = new Map()
|
|
2745
|
+
const defined = new Map()
|
|
2746
|
+
const modScope = new Map()
|
|
2747
|
+
const macros = new Map()
|
|
2748
|
+
const exportedMacros = new Map() // root -> name -> every `#[macro_export]` definition, `{ file, leaves }`
|
|
2749
|
+
const fileMacros = new Map()
|
|
2750
|
+
const externPrelude = new Map()
|
|
2751
|
+
const perRoot = (map, root) => map.get(root) ?? map.set(root, new Map()).get(root)
|
|
2752
|
+
// An import's or item's leaves: those its file (`here`) was mounted under, its own cfg's, and
|
|
2753
|
+
// its gate macro's.
|
|
2754
|
+
const itemLeaves = (here, x, path) => unionLeaves(here.leaves, leafSet([...cfgLeaves(x.cfg), ...gateLeaves(x.macro, gatesOf(path))], leafSets), leafSets)
|
|
2755
|
+
const scopeDepthOf = (vis, modulePath) => visibilityScope(vis, modulePath).split('::').length
|
|
2756
|
+
// Bare `name!` invocations resolve in textual scope, as rustc has it: a `macro_rules!` is in
|
|
2757
|
+
// scope from its definition to the end of its file, in the files the `mod`s after it mount (and
|
|
2758
|
+
// theirs, on down), and -- when its module is mounted with `#[macro_use]` -- in the mounting
|
|
2759
|
+
// file after that `mod`, and on from there. `mounts` is file → `{ parent, offset }`, the `mod`
|
|
2760
|
+
// (or `include!`) that first mounted it; `macroUseMods` file → its `#[macro_use] mod`s, each
|
|
2761
|
+
// `{ offset, files }`.
|
|
2762
|
+
const mounts = new Map()
|
|
2763
|
+
const macroUseMods = new Map()
|
|
2764
|
+
const filesOf = (target) => (target instanceof Map ? [...target.values()] : [target])
|
|
2765
|
+
// In tree order (`files`: each crate's, from its root down, the variants of a `mod` in the order
|
|
2766
|
+
// declared), whatever order the sources came in: among several files defining or importing a
|
|
2767
|
+
// name for one module, the first is the tree's own, and the answers are the same for any order
|
|
2768
|
+
// the caller lists the files in.
|
|
2769
|
+
for (const [path, here] of files) {
|
|
2770
|
+
const items = scanned.get(path)
|
|
2771
|
+
if (items === undefined) continue
|
|
2772
|
+
for (const im of items.imports) {
|
|
2773
|
+
// A `use $crate::…` in a `macro_rules!` template is the invoking module's, wherever that is.
|
|
2774
|
+
// (A `use` without `$crate` in one is kept: serde wraps its whole root in a macro, whose
|
|
2775
|
+
// `mod lib { pub use core::fmt; … }` is what every file's `use crate::lib::*` names.)
|
|
2776
|
+
if (im.template === true) continue
|
|
2777
|
+
const modulePath = [here.modulePath, ...im.inlinePath].join('::')
|
|
2778
|
+
const byModule = perRoot(imports, here.root)
|
|
2779
|
+
const of = byModule.get(modulePath) ?? byModule.set(modulePath, { named: new Map(), globs: [] }).get(modulePath)
|
|
2780
|
+
const entry = { ...im, file: path, module: modulePath, scopeDepth: scopeDepthOf(im.vis, modulePath), leaves: itemLeaves(here, im, path) }
|
|
2781
|
+
if (im.glob) of.globs.push(entry)
|
|
2782
|
+
else if (im.binding !== null) (of.named.get(im.binding) ?? of.named.set(im.binding, []).get(im.binding)).push(entry)
|
|
2783
|
+
}
|
|
2784
|
+
for (const d of items.defined) {
|
|
2785
|
+
const modulePath = [here.modulePath, ...d.inlinePath].join('::')
|
|
2786
|
+
const byModule = perRoot(defined, here.root)
|
|
2787
|
+
const names = byModule.get(modulePath) ?? byModule.set(modulePath, new Map()).get(modulePath)
|
|
2788
|
+
;(names.get(d.name) ?? names.set(d.name, []).get(d.name)).push({ file: path, scopeDepth: scopeDepthOf(d.vis, modulePath), ns: d.ns, leaves: itemLeaves(here, d, path) })
|
|
2789
|
+
}
|
|
2790
|
+
const scopes = perRoot(modScope, here.root)
|
|
188
2791
|
const specMap = resolutions.get(path)
|
|
189
|
-
for (const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
2792
|
+
for (const decl of modsOf(path)) {
|
|
2793
|
+
const parent = [here.modulePath, ...decl.inlinePath].join('::')
|
|
2794
|
+
const modulePath = `${parent}::${decl.name}`
|
|
2795
|
+
if (!scopes.has(modulePath)) scopes.set(modulePath, scopeDepthOf(decl.vis, parent))
|
|
2796
|
+
const target = specMap.get(`mod ${[...decl.inlinePath, decl.name].join('::')}`)
|
|
2797
|
+
if (target === undefined) continue
|
|
2798
|
+
const mountedFiles = filesOf(target).filter((f) => f !== path)
|
|
2799
|
+
const mount = { parent: path, offset: decl.offset, at: decl.at }
|
|
2800
|
+
for (const f of mountedFiles) if (!mounts.has(f)) mounts.set(f, mount)
|
|
2801
|
+
if (decl.macroUse && mountedFiles.length > 0) (macroUseMods.get(mount.parent) ?? macroUseMods.set(mount.parent, []).get(mount.parent)).push({ offset: mount.offset, at: mount.at, files: mountedFiles })
|
|
2802
|
+
}
|
|
2803
|
+
for (const [spec, target] of specMap) {
|
|
2804
|
+
if (spec.startsWith('include ') && !mounts.has(target) && target !== path) mounts.set(target, { parent: path, offset: Infinity, at: Infinity })
|
|
2805
|
+
}
|
|
2806
|
+
for (const [inline, vis] of items.inlineModuleVis) {
|
|
2807
|
+
const modulePath = `${here.modulePath}::${inline}`
|
|
2808
|
+
if (!scopes.has(modulePath)) scopes.set(modulePath, scopeDepthOf(vis, modulePath.slice(0, modulePath.lastIndexOf('::'))))
|
|
2809
|
+
}
|
|
2810
|
+
fileMacros.set(path, new Set(items.macros.map((m) => m.name)))
|
|
2811
|
+
for (const m of items.macros) {
|
|
2812
|
+
if (!m.exported) continue
|
|
2813
|
+
const byName = perRoot(macros, here.root)
|
|
2814
|
+
if (!byName.has(m.name)) byName.set(m.name, path)
|
|
2815
|
+
const defs = perRoot(exportedMacros, here.root)
|
|
2816
|
+
;(defs.get(m.name) ?? defs.set(m.name, []).get(m.name)).push({ file: path, leaves: itemLeaves(here, m, path) })
|
|
2817
|
+
}
|
|
2818
|
+
if (here.modulePath === 'crate') {
|
|
2819
|
+
const prelude = perRoot(externPrelude, here.root)
|
|
2820
|
+
for (const e of items.externCrates) {
|
|
2821
|
+
// `extern crate self as x;` puts this crate's root in the prelude as `x` (kept as `self`).
|
|
2822
|
+
const named = e.name === 'self' ? e.alias !== null : !NON_CRATE_LEADS.has(e.name)
|
|
2823
|
+
if (e.inlinePath.length === 0 && named && !prelude.has(e.alias ?? e.name)) prelude.set(e.alias ?? e.name, e.name)
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
// The macros a file mounted with `#[macro_use]` hands the file mounting it: its own and, in
|
|
2828
|
+
// turn, its own `#[macro_use] mod`s' (name → defining file).
|
|
2829
|
+
const exportsMemo = new Map()
|
|
2830
|
+
const macroExports = (file, guard = new Set()) => {
|
|
2831
|
+
if (exportsMemo.has(file)) return exportsMemo.get(file)
|
|
2832
|
+
if (guard.has(file)) return new Map()
|
|
2833
|
+
guard.add(file)
|
|
2834
|
+
const out = new Map()
|
|
2835
|
+
for (const { files: mountedFiles } of macroUseMods.get(file) ?? []) for (const f of mountedFiles) for (const [name, where] of macroExports(f, guard)) out.set(name, where)
|
|
2836
|
+
for (const name of fileMacros.get(file) ?? []) out.set(name, file)
|
|
2837
|
+
exportsMemo.set(file, out)
|
|
2838
|
+
return out
|
|
2839
|
+
}
|
|
2840
|
+
// A file's macro events in source order, each `{ offset, at, macros }` (name → defining file):
|
|
2841
|
+
// its own `macro_rules!` and what its `#[macro_use] mod`s hand it. `offset` is where an item
|
|
2842
|
+
// stands, `at` where it is written: the same for most, the invocation and the place in the
|
|
2843
|
+
// template for what a `macro_rules!` emits (serde's `crate_root! { … macro_rules! tri { … } …
|
|
2844
|
+
// pub mod de; … }`: `tri` is in scope in `de`, both standing at the invocation).
|
|
2845
|
+
const eventsMemo = new Map()
|
|
2846
|
+
const fileEvents = (file) => cached(eventsMemo, file, () => [
|
|
2847
|
+
...(scanned.get(file)?.macros ?? []).map((m) => ({ offset: m.offset, at: m.at, macros: new Map([[m.name, file]]) })),
|
|
2848
|
+
...(macroUseMods.get(file) ?? []).map((u) => ({ offset: u.offset, at: u.at, macros: new Map(u.files.flatMap((f) => [...macroExports(f)])) })),
|
|
2849
|
+
].toSorted((a, b) => a.offset - b.offset || a.at - b.at))
|
|
2850
|
+
const before = (e, offset, at) => e.offset < offset || (e.offset === offset && e.at < at)
|
|
2851
|
+
// The macros in textual scope in `file` before position (`offset`, `at`) -- (Infinity,
|
|
2852
|
+
// Infinity): its end -- on top of `scope`, what it sees from above: the later definition of a
|
|
2853
|
+
// name shadows the earlier, whichever kind each is.
|
|
2854
|
+
const textualScope = (file, offset, at, scope) => {
|
|
2855
|
+
const out = new Map(scope)
|
|
2856
|
+
for (const e of fileEvents(file)) {
|
|
2857
|
+
if (!before(e, offset, at)) break
|
|
2858
|
+
for (const [name, where] of e.macros) out.set(name, where)
|
|
2859
|
+
}
|
|
2860
|
+
return out
|
|
2861
|
+
}
|
|
2862
|
+
// The file defining the macro `name` in scope at (`offset`, `at`) in `file`: the last event
|
|
2863
|
+
// before that point defining it, else what the file sees from above (`scope`); a definition
|
|
2864
|
+
// later in the file doesn't reach an earlier call.
|
|
2865
|
+
const macroAt = (file, name, offset, at, scope) => {
|
|
2866
|
+
const events = fileEvents(file)
|
|
2867
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2868
|
+
if (!before(events[i], offset, at)) continue
|
|
2869
|
+
const where = events[i].macros.get(name)
|
|
2870
|
+
if (where !== undefined) return where
|
|
2871
|
+
}
|
|
2872
|
+
return scope.get(name)
|
|
2873
|
+
}
|
|
2874
|
+
// What a file sees from above: what its parent sees from above, and the parent's textual scope
|
|
2875
|
+
// at the `mod` mounting it.
|
|
2876
|
+
const inheritedMemo = new Map()
|
|
2877
|
+
const inherited = (file, guard = new Set()) => {
|
|
2878
|
+
if (inheritedMemo.has(file)) return inheritedMemo.get(file)
|
|
2879
|
+
const mount = mounts.get(file)
|
|
2880
|
+
if (mount === undefined || guard.has(file)) return new Map()
|
|
2881
|
+
guard.add(file)
|
|
2882
|
+
const out = textualScope(mount.parent, mount.offset, mount.at, inherited(mount.parent, guard))
|
|
2883
|
+
inheritedMemo.set(file, out)
|
|
2884
|
+
return out
|
|
2885
|
+
}
|
|
2886
|
+
// Crate names resolve per package (a path dep's, a rename) and per the code asking (a build
|
|
2887
|
+
// script's tables, the platforms it is compiled for), so per file; the same (name, file) comes up
|
|
2888
|
+
// once per reference to the crate, and every glob import's source resolution asks. `{ file,
|
|
2889
|
+
// alternatives }`: the crate root, and -- when tables that may each apply name different
|
|
2890
|
+
// packages (cargo.js crateCandidates) -- each of them by the table's platform.
|
|
2891
|
+
const crates = new Map()
|
|
2892
|
+
const crateOf = (name, from) => cached(crates, `${name}\0${from}`, () => {
|
|
2893
|
+
const found = ctx ? ctx.crateCandidates(name, from, { roots: files.get(from)?.roots ?? null, units: unitsOf(units, from) }) : []
|
|
2894
|
+
const file = found[0]?.file ?? resolveVendoredCrate(name, { knownSources: sources })
|
|
2895
|
+
return { file, alternatives: found.length > 1 ? new Map(found.map((c) => [cfgKey(c.key), c.file])) : undefined }
|
|
2896
|
+
})
|
|
2897
|
+
const resolveCrate = (name, from) => crateOf(name, from).file
|
|
2898
|
+
const crateAlternatives = (name, from) => crateOf(name, from).alternatives
|
|
2899
|
+
// The crates a crate root's `#[macro_use] extern crate`s bring the exported macros of into every
|
|
2900
|
+
// module's scope, as their roots.
|
|
2901
|
+
const macroUse = new Map()
|
|
2902
|
+
const macroUseCrates = (root) => cached(macroUse, root, () => (scanned.get(root)?.externCrates ?? []).filter((e) => e.macroUse && !NON_CRATE_LEADS.has(e.name)).map((e) => resolveCrate(e.name, root)).filter(Boolean))
|
|
2903
|
+
// One context for every path of every file: `provided` memoizes across them (see walkPath).
|
|
2904
|
+
// Per crate root, name → the modules with anything of the name (a child module, an item, an
|
|
2905
|
+
// import binding it; the root for an exported macro): what hasAll may answer for.
|
|
2906
|
+
const having = new Map()
|
|
2907
|
+
for (const [root, byParent] of children) for (const [parent, names] of byParent) for (const name of names) note(having, root, name, parent)
|
|
2908
|
+
for (const [root, byModule] of defined) for (const [module, names] of byModule) for (const name of names.keys()) note(having, root, name, module)
|
|
2909
|
+
for (const [root, byModule] of imports) for (const [module, of] of byModule) for (const name of of.named.keys()) note(having, root, name, module)
|
|
2910
|
+
for (const [root, byName] of macros) for (const name of byName.keys()) note(having, root, name, 'crate')
|
|
2911
|
+
const declaresCrate = (name, from) => ctx?.declaresCrate?.(name, from) === true
|
|
2912
|
+
const pathCtx = { trees, children, moduleFiles, files, resolveCrate, crateAlternatives, declaresCrate, imports, defined, modScope, macros, exportedMacros, fileMacros, externPrelude, leafSets, having, provided: new Map(), closures: new Map(), opaque: new Map(), opaqueGlobs: new Map(), walking: 0, hits: new Set() }
|
|
2913
|
+
// Every path of every file, on top of the `mod` / include edges (`resolutions`): once, and once
|
|
2914
|
+
// more when the first pass left glob closures short of an import cycle (stabilizeClosures).
|
|
2915
|
+
const resolveAll = () => {
|
|
2916
|
+
const out = new Map()
|
|
2917
|
+
const unresolved = new Set()
|
|
2918
|
+
const wanted = new Map() // root -> the units it is wanted as
|
|
2919
|
+
for (const [path, items] of scanned) {
|
|
2920
|
+
const specMap = new Map(resolutions.get(path))
|
|
2921
|
+
out.set(path, specMap)
|
|
2922
|
+
resolveFile(path, items, specMap, unresolved, wanted)
|
|
2923
|
+
}
|
|
2924
|
+
return { out, unresolved, wanted }
|
|
2925
|
+
}
|
|
2926
|
+
const resolveFile = (path, items, specMap, unresolved, wanted) => {
|
|
2927
|
+
// Only edges to bundled files, never to self, first spelling wins. A target with
|
|
2928
|
+
// `alternatives` (withAlternatives) is recorded as the cfg-keyed map of them -- as a `mod`
|
|
2929
|
+
// with cfg variants is -- less the asking file and files not bundled; one file left is a plain edge.
|
|
2930
|
+
const add = (spec, target, alternatives) => {
|
|
2931
|
+
if (specMap.has(spec)) return
|
|
2932
|
+
if (alternatives !== undefined) {
|
|
2933
|
+
const keyed = new Map([...alternatives].filter(([, f]) => f !== path && sources.has(f)))
|
|
2934
|
+
const distinct = new Set(keyed.values())
|
|
2935
|
+
if (distinct.size > 1) {
|
|
2936
|
+
specMap.set(spec, keyed)
|
|
2937
|
+
return
|
|
2938
|
+
}
|
|
2939
|
+
if (distinct.size === 1) [target] = distinct
|
|
193
2940
|
}
|
|
2941
|
+
if (target && target !== path && sources.has(target)) specMap.set(spec, target)
|
|
194
2942
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
2943
|
+
// A crate resolved to a root the walk didn't load (its name was bound in the walking file as
|
|
2944
|
+
// a value, see resolvePathRef), or loaded as fewer units than this file names it as: the
|
|
2945
|
+
// caller may load it and build again.
|
|
2946
|
+
const addCrate = (name, target, alternatives) => {
|
|
2947
|
+
for (const file of alternatives === undefined ? [target] : alternatives.values()) {
|
|
2948
|
+
if (file && ctx) {
|
|
2949
|
+
const want = new Set([...unitsOf(units, path)].map((u) => ctx.unitOfCrate(file, u)))
|
|
2950
|
+
const have = sources.has(file) ? unitsOf(units, file) : new Set()
|
|
2951
|
+
if (!want.isSubsetOf(have)) for (const u of want) (wanted.get(file) ?? wanted.set(file, new Set()).get(file)).add(u)
|
|
2952
|
+
} else if (file && !sources.has(file)) wanted.set(file, DEFAULT_UNITS)
|
|
2953
|
+
}
|
|
2954
|
+
add(`use ${name}`, target, alternatives)
|
|
2955
|
+
}
|
|
2956
|
+
// A crate name nothing in-tree satisfied -- sysroot crates aside, and an uppercase lead is an
|
|
2957
|
+
// `Enum::Variant` import, not a crate.
|
|
2958
|
+
const noteUnresolved = (name) => {
|
|
2959
|
+
if (!OTHER_SYSROOT_CRATES.has(name) && /^[a-z_]/u.test(name)) unresolved.add(name)
|
|
2960
|
+
}
|
|
2961
|
+
const here = files.get(path)
|
|
2962
|
+
const via = new Map() // file → the prefix of a path that went through an import of a module there
|
|
2963
|
+
for (const ref of items.refs) {
|
|
2964
|
+
const r = resolvePathRef(ref, path, items.bindings, pathCtx)
|
|
2965
|
+
if (r === null) continue
|
|
2966
|
+
if (r.kind === 'crate') {
|
|
2967
|
+
addCrate(r.name, r.file, r.alternatives)
|
|
2968
|
+
if (r.macro !== undefined) add(`${ref.spec}!`, r.macro.file, r.macro.alternatives)
|
|
2969
|
+
}
|
|
2970
|
+
else if (r.kind === 'unresolved') {
|
|
2971
|
+
// Only a `use` reliably says its lead names a crate (see scanRustItems' `fromUse`) -- or
|
|
2972
|
+
// the package declaring a dependency of that name (`serde_json::to_string(…)`), which it
|
|
2973
|
+
// then is.
|
|
2974
|
+
if (ref.fromUse || declaresCrate(r.name, path)) noteUnresolved(r.name)
|
|
2975
|
+
continue
|
|
2976
|
+
} else add(ref.macroCall ? `${ref.spec}!` : ref.spec, r.file, r.alternatives) // `crate::helper!()` beside `crate::helper()`: two edges
|
|
2977
|
+
// The module an import was followed from, when the path's own segments name it (`super`,
|
|
2978
|
+
// `crate::util`) and its file is neither this one nor the crate root, which every file of
|
|
2979
|
+
// the crate hangs off anyway.
|
|
2980
|
+
const v = r.via
|
|
2981
|
+
if (v && v.consumed > 0 && v.through !== path && v.through !== here.root && v.through !== r.file && !via.has(v.through)) {
|
|
2982
|
+
via.set(v.through, ref.segments.slice(0, v.consumed).join('::'))
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
for (const { name } of items.externCrates) {
|
|
2986
|
+
if (NON_CRATE_LEADS.has(name)) continue // `extern crate self as x;` / `extern crate alloc;`
|
|
2987
|
+
const { file: target, alternatives } = crateOf(name, path)
|
|
2988
|
+
if (target) addCrate(name, target, alternatives)
|
|
2989
|
+
else noteUnresolved(name)
|
|
2990
|
+
}
|
|
2991
|
+
// A macro invoked by bare name and defined by `macro_rules!` in textual scope (see mounts):
|
|
2992
|
+
// this file's own, what the files above hand down (bytes' `fmt_impl!`, from fmt/mod.rs in
|
|
2993
|
+
// fmt/debug.rs), what a `#[macro_use] mod` of this file brings in (tokio's `ready!`, from
|
|
2994
|
+
// macros/ready.rs), and -- in the crate root's file, whose items they are -- the exported ones;
|
|
2995
|
+
// else one the module's imports bring in by path (`use crate::combinator::dispatch;`, a `use
|
|
2996
|
+
// super::*` from the root). (A `macro_rules! vec` in some sibling file of the crate is not
|
|
2997
|
+
// what `vec!` names.)
|
|
2998
|
+
if (here !== undefined) {
|
|
2999
|
+
const above = inherited(path)
|
|
3000
|
+
const exported = here.modulePath === 'crate' ? macros.get(here.root) : undefined
|
|
3001
|
+
// The macro a bare call of `name` at `offset` names, `{ file, name }` (the file defining it,
|
|
3002
|
+
// by the name it defines it under): one in textual scope; else one an import brings in --
|
|
3003
|
+
// a file that defines a macro of that name, not a `fn write` beside `write!` -- or a
|
|
3004
|
+
// `#[macro_use] extern crate` of the crate root does.
|
|
3005
|
+
const macroFileAt = (name, offset) => {
|
|
3006
|
+
const inScope = macroAt(path, name, offset, offset, above) ?? exported?.get(name)
|
|
3007
|
+
if (inScope !== undefined) return { file: inScope, name }
|
|
3008
|
+
const byPath = walkPath([name], here.root, here.modulePath, pathCtx, { ns: 'macro', file: path, asker: here.asker })
|
|
3009
|
+
const as = byPath?.macroName ?? name
|
|
3010
|
+
if (byPath?.file !== undefined && fileMacros.get(byPath.file)?.has(as) === true) {
|
|
3011
|
+
const alternatives = byPath.alternatives === undefined ? undefined : new Map([...byPath.alternatives].filter(([, f]) => fileMacros.get(f)?.has(as) === true))
|
|
3012
|
+
return { file: byPath.file, name: as, alternatives: alternatives?.size > 1 ? alternatives : undefined }
|
|
3013
|
+
}
|
|
3014
|
+
for (const crate of macroUseCrates(here.root)) {
|
|
3015
|
+
const macro = macroOfCrate(crate, [name], pathCtx)
|
|
3016
|
+
if (macro !== null) return { file: macro.file, name: macro.macroName, alternatives: macro.alternatives }
|
|
3017
|
+
}
|
|
3018
|
+
return undefined
|
|
3019
|
+
}
|
|
3020
|
+
for (const [name, offsets] of items.calls) {
|
|
3021
|
+
// Scope is decided at each call: a `macro_rules!` later in the file doesn't shadow what an
|
|
3022
|
+
// earlier `#[macro_use] mod` handed an earlier call. The bare calls in the invoked
|
|
3023
|
+
// macro's template, and in theirs on down, are made here too -- resolved in this file's
|
|
3024
|
+
// scope at this call, as rustc expands them.
|
|
3025
|
+
const found = new Map() // `name!\0file` -> the cfg-keyed files, when several may define it
|
|
3026
|
+
for (const offset of offsets) {
|
|
3027
|
+
const seen = new Set()
|
|
3028
|
+
const expand = (called) => {
|
|
3029
|
+
if (seen.has(called)) return
|
|
3030
|
+
seen.add(called)
|
|
3031
|
+
const macro = macroFileAt(called, offset)
|
|
3032
|
+
if (macro === undefined) return
|
|
3033
|
+
found.set(`${called}!\0${macro.file}`, macro.alternatives)
|
|
3034
|
+
for (const m of scanned.get(macro.file)?.macros ?? []) if (m.name === macro.name) for (const inner of m.calls) expand(inner)
|
|
3035
|
+
}
|
|
3036
|
+
expand(name)
|
|
3037
|
+
}
|
|
3038
|
+
for (const [edge, alternatives] of found) {
|
|
3039
|
+
const cut = edge.indexOf('\0')
|
|
3040
|
+
add(edge.slice(0, cut), edge.slice(cut + 1), alternatives)
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
199
3043
|
}
|
|
3044
|
+
// A module whose import a path went through is a dependency of the file too, whatever the
|
|
3045
|
+
// import led to (bitflags' `$crate::__private::serde::Serialize` needs the root's `__private`
|
|
3046
|
+
// as much as serde). When nothing else in the file points at that module's file, the path's
|
|
3047
|
+
// prefix naming the module records it (`crate::__private` → lib.rs); the bare crate root is
|
|
3048
|
+
// implied by the module tree.
|
|
3049
|
+
const targets = new Set()
|
|
3050
|
+
for (const t of specMap.values()) for (const f of t instanceof Map ? t.values() : [t]) targets.add(f)
|
|
3051
|
+
for (const [owner, spec] of via) if (!targets.has(owner)) add(spec, owner)
|
|
200
3052
|
}
|
|
201
|
-
|
|
3053
|
+
let { out, unresolved, wanted } = resolveAll()
|
|
3054
|
+
if (stabilizeClosures(pathCtx)) ({ out, unresolved, wanted } = resolveAll())
|
|
3055
|
+
return { sources, resolutions: out, missing, unresolvedCrates: unresolved, wantedRoots: [...wanted.keys()], wantedUnits: wanted }
|
|
202
3056
|
}
|
|
203
3057
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
3058
|
+
// --- Disk walk --------------------------------------------------------------------------
|
|
3059
|
+
|
|
3060
|
+
// Walk from `entries` (the crate roots), following `mod` declarations and references to in-tree
|
|
3061
|
+
// crates, reading each reachable file once; a missing file warns and is skipped without aborting
|
|
3062
|
+
// the walk (buildRustTree decides what is fatal). `cargo` (cargo.js) resolves crate names and
|
|
3063
|
+
// carries each crate's features; one is created for `entries` when not passed in. Pass `sources`
|
|
3064
|
+
// to walk further roots into an earlier walk's map (files already there are not read again, but
|
|
3065
|
+
// are gone through again when they are now compiled as more units). `units`: file → the compile
|
|
3066
|
+
// units it is compiled as (cargo.js unitKey), filled in as the walk goes -- an entry the map
|
|
3067
|
+
// doesn't name is the target's; what a file declares or includes is compiled as that file is,
|
|
3068
|
+
// and a crate it names as the Cargo context says (unitOfCrate: a proc-macro crate, or a crate
|
|
3069
|
+
// named from code built for the host, is built for the host). `crateRoots`: file → the crate roots
|
|
3070
|
+
// it is compiled in, filled in the same way (an entry is its own; a build script's modules are the
|
|
3071
|
+
// script's, and name the crates of its `[build-dependencies]`). `unloaded` gets the files the walk
|
|
3072
|
+
// refused (a link out of their package) or found gone, each warned about.
|
|
3073
|
+
export async function collectRustFilesFromDisk(baseDir, entries, { cargo = null, formats = null, sources = new Map(), units = new Map(), crateRoots: rootsOf = new Map(), unloaded = new Set() } = {}) {
|
|
208
3074
|
const realBase = realpathSync(baseDir)
|
|
3075
|
+
const roots = new Set(entries)
|
|
3076
|
+
const pathLoaded = new Set() // files a `#[path]` named: their submodules sit beside them
|
|
3077
|
+
const assets = new Map() // files an `include_str!` / `include_bytes!` names: carried as they are, not scanned
|
|
3078
|
+
const rustFiles = new Set(entries) // files named as Rust source (a `mod`, an `include!`): scanned, never an asset
|
|
3079
|
+
const ctx = cargo ?? createCargoContext(baseDir, { entries })
|
|
3080
|
+
const modOpts = { baseDir, roots, pathLoaded }
|
|
3081
|
+
// file → the directory it must really lie in (boundaryOf, from the file that named it): a
|
|
3082
|
+
// vendored crate's files stay in its package, symlinks included.
|
|
3083
|
+
const boundaries = new Map(entries.map((e) => [e, boundaryOf(e, ctx)]))
|
|
3084
|
+
const realDirs = new Map() // those directories' real paths
|
|
3085
|
+
// An include macro in a `macro_rules!` body names a file relative to each file invoking the
|
|
3086
|
+
// macro -- through the templates that call it, the outermost (effectiveInvokers): definitions
|
|
3087
|
+
// and invocations arrive in any order, so each side is kept and paired. Every definition of a
|
|
3088
|
+
// name counts (two `macro_rules! m` in two files: both bodies' assets).
|
|
3089
|
+
const templateIncludes = new Map() // macro name → its bodies' includes
|
|
3090
|
+
const templateMods = new Map() // macro name → its bodies' `mod`s, each `{ decl, from }` (see buildRustTree)
|
|
3091
|
+
const templateCalls = new Map() // macro name → the names its bodies call by bare name
|
|
3092
|
+
const invokers = new Map() // macro name → the files invoking it, by bare name or by path
|
|
3093
|
+
const scansOf = new Map() // file → its scan, for where it invokes what (hostedCopies)
|
|
3094
|
+
// A package defining a `macro_rules!` of a template macro's name (its own `quote!`) invokes that
|
|
3095
|
+
// one everywhere in the package (see buildRustTree): its files are scanned without the name in
|
|
3096
|
+
// the template set -- again, for the ones scanned before the definition was met.
|
|
3097
|
+
const packageOf = (rel) => ctx.packageInfo(rel)?.dir ?? '.'
|
|
3098
|
+
const ownTemplates = new Map() // package → the template names it defines
|
|
3099
|
+
const skippedIn = new Map() // package → its files that skipped a template body
|
|
3100
|
+
const scanFor = (relPath, content) => {
|
|
3101
|
+
const own = ownTemplates.get(packageOf(relPath))
|
|
3102
|
+
return cachedScan(sources, relPath, content, buildOf(relPath, ctx, units), own === undefined ? undefined : templatesLess(own))
|
|
3103
|
+
}
|
|
3104
|
+
withEntryUnits(units, entries, ctx)
|
|
3105
|
+
for (const e of entries) addTo(rootsOf, e, new Set([e]))
|
|
3106
|
+
// Files already walked that are now compiled as more units, or in more crates: gone through
|
|
3107
|
+
// again (their build changed, and so may what they name, and as what).
|
|
3108
|
+
const revisit = new Set(entries.filter((e) => sources.has(e) && !formats?.has(e)))
|
|
3109
|
+
|
|
3110
|
+
// What a Rust file names, queued for the next wave: compiled as the naming file is -- as its
|
|
3111
|
+
// units, a crate as unitOfCrate says (`as`); in its crates, or the crate it is the root of
|
|
3112
|
+
// (`crate`) -- and, for a vendored crate's, within its package (`within`).
|
|
3113
|
+
let next = []
|
|
3114
|
+
const queue = (from, file, { rust, within = boundaryOf(from, ctx), as = unitsOf(units, from), crate = null }) => {
|
|
3115
|
+
if (rust) {
|
|
3116
|
+
rustFiles.add(file)
|
|
3117
|
+
// Named as Rust source after being carried as an asset: read again, as source.
|
|
3118
|
+
if (assets.delete(file) && sources.has(file)) {
|
|
3119
|
+
sources.delete(file)
|
|
3120
|
+
formats?.delete(file)
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (!boundaries.has(file)) boundaries.set(file, within)
|
|
3124
|
+
const inCrates = addTo(rootsOf, file, crate === null ? rootsOf.get(from) ?? new Set([from]) : new Set([crate]))
|
|
3125
|
+
const grew = addTo(units, file, as) || inCrates
|
|
3126
|
+
if (!sources.has(file)) next.push(file)
|
|
3127
|
+
else if (grew && rust && !formats?.has(file)) revisit.add(file)
|
|
3128
|
+
}
|
|
3129
|
+
// An include macro's file, relative to `from` (the file it is written in, or one invoking the
|
|
3130
|
+
// macro whose body it is in).
|
|
3131
|
+
const includeFrom = (inc, from) => {
|
|
3132
|
+
const file = includeTarget(inc, from, ctx)
|
|
3133
|
+
if (file === null || file === from) return
|
|
3134
|
+
// A FIFO or device named would hang the read; a missing file is the tree pass's to report.
|
|
3135
|
+
if (!sources.has(file) && !isFile(join(baseDir, file))) return
|
|
3136
|
+
if (inc.kind === 'include') {
|
|
3137
|
+
pathLoaded.add(file) // its own `mod`s resolve beside it, as rustc has it
|
|
3138
|
+
queue(from, file, { rust: true })
|
|
3139
|
+
} else if (!rustFiles.has(file) && !sources.has(file)) {
|
|
3140
|
+
assets.set(file, inc.kind)
|
|
3141
|
+
queue(from, file, { rust: false })
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
// A `mod` declaration's files, relative to `from` (the file it is written in, or one invoking
|
|
3145
|
+
// the macro whose body it is in).
|
|
3146
|
+
const modFrom = (decl, from) => {
|
|
3147
|
+
for (const { file, explicit } of resolveModDecl(decl, from, { ...modOpts, boundary: boundaryOf(from, ctx) })) {
|
|
3148
|
+
if (explicit) pathLoaded.add(file)
|
|
3149
|
+
queue(from, file, { rust: true })
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
// The include macros of every template, relative to every file in effect invoking it; its
|
|
3153
|
+
// `mod`s relative to those of its own package.
|
|
3154
|
+
const expandTemplates = () => {
|
|
3155
|
+
for (const [name, incs] of templateIncludes) {
|
|
3156
|
+
for (const f of effectiveInvokers(name, invokers, templateCalls)) for (const inc of incs) includeFrom(inc, f)
|
|
3157
|
+
}
|
|
3158
|
+
for (const [name, decls] of templateMods) {
|
|
3159
|
+
for (const { decl, from, exported } of decls.values()) for (const f of hostsOf(name, from, invokers, templateCalls, exported ? null : packageOf)) for (const copy of hostedCopies(decl, scansOf.get(f), templateCalls)) modFrom(copy, f)
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// A template's `mod`s that nothing in its package invokes by bare name stand beside its
|
|
3164
|
+
// definition (see buildRustTree): walked once nothing else is left to, and what they name after.
|
|
3165
|
+
const placed = new Set()
|
|
3166
|
+
const placeUninvoked = () => {
|
|
3167
|
+
for (const [name, decls] of templateMods) {
|
|
3168
|
+
for (const { decl, from, exported } of decls.values()) {
|
|
3169
|
+
if (placed.has(decl) || hostsOf(name, from, invokers, templateCalls, exported ? null : packageOf).length > 0) continue
|
|
3170
|
+
placed.add(decl)
|
|
3171
|
+
modFrom(decl, from)
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
209
3175
|
|
|
210
3176
|
const processWave = async (wave) => {
|
|
211
3177
|
const toLoad = [...new Set(wave)].filter((p) => !sources.has(p))
|
|
212
|
-
if (toLoad.length === 0) return
|
|
3178
|
+
if (toLoad.length === 0 && revisit.size === 0) return
|
|
213
3179
|
const reads = await Promise.all(
|
|
214
3180
|
toLoad.map(async (relPath) => {
|
|
215
3181
|
try {
|
|
216
3182
|
assertRealPathWithinBase(realBase, baseDir, relPath)
|
|
217
|
-
|
|
3183
|
+
if (!withinRealDir(baseDir, relPath, boundaries.get(relPath) ?? '.', { realDirs })) {
|
|
3184
|
+
unloaded.add(relPath)
|
|
3185
|
+
return null
|
|
3186
|
+
}
|
|
3187
|
+
// Rust source (a module, a build script, an `include!`d file) and an `include_str!` asset
|
|
3188
|
+
// are UTF-8 text, or rustc rejects them: one that isn't is refused, never carried with
|
|
3189
|
+
// its bytes replaced. An `include_bytes!` asset is any bytes, carried as base64 if need be.
|
|
3190
|
+
const kind = assets.get(relPath)
|
|
3191
|
+
const buf = await readFile(join(baseDir, relPath))
|
|
3192
|
+
ctx.checkVendoredFile?.(relPath, buf)
|
|
3193
|
+
const utf8 = isUtf8(buf)
|
|
3194
|
+
if (!utf8 && kind !== 'include_bytes') throw new Error(`${kind === undefined ? 'Rust source' : 'include_str! file'} is not valid UTF-8: ${relPath}`)
|
|
3195
|
+
if (kind === undefined) return [relPath, buf.toString('utf8'), false]
|
|
3196
|
+
formats?.set(relPath, utf8 ? 'resource' : 'resource:base64')
|
|
3197
|
+
return [relPath, utf8 ? buf.toString('utf8') : buf.toString('base64'), true]
|
|
218
3198
|
} catch (err) {
|
|
219
3199
|
if (err.code === 'ENOENT') {
|
|
220
3200
|
console.warn(`[loader.rust] Missing file: ${relPath}`)
|
|
3201
|
+
unloaded.add(relPath)
|
|
221
3202
|
return null
|
|
222
3203
|
}
|
|
223
3204
|
// resolveModPath gates on isFile so a dir is normally never queued; guard EISDIR anyway.
|
|
@@ -229,40 +3210,127 @@ export async function collectRustFilesFromDisk(baseDir, entries) {
|
|
|
229
3210
|
}
|
|
230
3211
|
})
|
|
231
3212
|
)
|
|
232
|
-
|
|
3213
|
+
next = []
|
|
3214
|
+
// What a Rust file names, queued (see queue). Run again for a file whose package turned out
|
|
3215
|
+
// to define a template macro (below): the file's `quote!` bodies are its own then.
|
|
3216
|
+
const handle = (relPath, content) => {
|
|
3217
|
+
const here = unitsOf(units, relPath)
|
|
3218
|
+
const items = scanFor(relPath, content)
|
|
3219
|
+
const { mods, refs, externCrates, bindings, includes, macros } = items
|
|
3220
|
+
const pkg = packageOf(relPath)
|
|
3221
|
+
if (items.skipped.size > 0) (skippedIn.get(pkg) ?? skippedIn.set(pkg, new Set()).get(pkg)).add(relPath)
|
|
3222
|
+
for (const m of macros) {
|
|
3223
|
+
if (!TEMPLATE_MACROS.has(m.name)) continue
|
|
3224
|
+
const own = ownTemplates.get(pkg) ?? ownTemplates.set(pkg, new Set()).get(pkg)
|
|
3225
|
+
if (own.has(m.name)) continue
|
|
3226
|
+
own.add(m.name)
|
|
3227
|
+
for (const f of skippedIn.get(pkg) ?? []) if (f !== relPath) handle(f, sources.get(f))
|
|
3228
|
+
}
|
|
3229
|
+
// An `include!`d file's `mod` declarations resolve beside that file (it is in `pathLoaded`
|
|
3230
|
+
// from when it was queued), within its package.
|
|
3231
|
+
// A template's `mod`s wait for the files invoking it (expandTemplates).
|
|
3232
|
+
for (const decl of mods) {
|
|
3233
|
+
if (decl.template === null) modFrom(decl, relPath)
|
|
3234
|
+
else (templateMods.get(decl.template) ?? templateMods.set(decl.template, new Map()).get(decl.template)).set(`${relPath}\0${decl.at}`, { decl, from: relPath, exported: macros.some((m) => m.name === decl.template && m.exported) })
|
|
3235
|
+
}
|
|
3236
|
+
// `include!("x.rs")` splices Rust source (scanned next wave, under this module); the asset
|
|
3237
|
+
// macros name a file to carry as it is. A build-output path (`concat!(env!("OUT_DIR"), …)`)
|
|
3238
|
+
// never gets here. A template's include macros are resolved once the wave is in
|
|
3239
|
+
// (expandTemplates), against the files invoking it.
|
|
3240
|
+
for (const inc of includes) includeFrom(inc, relPath)
|
|
3241
|
+
for (const m of macros) {
|
|
3242
|
+
if (m.includes.length === 0) continue
|
|
3243
|
+
const list = templateIncludes.get(m.name) ?? templateIncludes.set(m.name, []).get(m.name)
|
|
3244
|
+
for (const inc of m.includes) if (!list.some((x) => x.kind === inc.kind && x.path === inc.path && x.base === inc.base)) list.push(inc)
|
|
3245
|
+
}
|
|
3246
|
+
noteMacroCalls(invokers, templateCalls, relPath, items)
|
|
3247
|
+
scansOf.set(relPath, items)
|
|
3248
|
+
// A reference to an in-tree crate pulls its root in (a crate root by role, whatever its
|
|
3249
|
+
// name -- `[lib] path` may point anywhere); the root's own `mod` edges follow next wave.
|
|
3250
|
+
const leads = new Set()
|
|
3251
|
+
for (const e of externCrates) if (!NON_CRATE_LEADS.has(e.name)) leads.add(e.name)
|
|
3252
|
+
for (const r of refs) {
|
|
3253
|
+
const name = crateLead(r, bindings)
|
|
3254
|
+
if (name !== null) leads.add(name)
|
|
3255
|
+
}
|
|
3256
|
+
for (const name of leads) {
|
|
3257
|
+
for (const { file: lib } of ctx.crateCandidates(name, relPath, { roots: rootsOf.get(relPath) ?? null, units: here })) {
|
|
3258
|
+
roots.add(lib)
|
|
3259
|
+
queue(relPath, lib, { rust: true, within: boundaryOf(lib, ctx), as: new Set([...here].map((u) => ctx.unitOfCrate(lib, u))), crate: lib })
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
233
3263
|
for (const entry of reads) {
|
|
234
3264
|
if (!entry) continue
|
|
235
|
-
const [relPath, content] = entry
|
|
3265
|
+
const [relPath, content, asset] = entry
|
|
236
3266
|
sources.set(relPath, content)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
3267
|
+
if (!asset) handle(relPath, content)
|
|
3268
|
+
}
|
|
3269
|
+
for (;;) {
|
|
3270
|
+
expandTemplates()
|
|
3271
|
+
if (revisit.size === 0) break
|
|
3272
|
+
const again = [...revisit]
|
|
3273
|
+
revisit.clear()
|
|
3274
|
+
for (const relPath of again) handle(relPath, sources.get(relPath))
|
|
246
3275
|
}
|
|
247
|
-
|
|
3276
|
+
if (next.length === 0) placeUninvoked()
|
|
3277
|
+
const following = next
|
|
3278
|
+
await processWave(following)
|
|
248
3279
|
}
|
|
249
3280
|
|
|
250
3281
|
await processWave(entries)
|
|
251
3282
|
return sources
|
|
252
3283
|
}
|
|
253
3284
|
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
3285
|
+
// The files of a Rust bundle of `entries`, from disk, and its tree: the walk
|
|
3286
|
+
// (collectRustFilesFromDisk) and -- with `buildScripts` -- the build script of every package it
|
|
3287
|
+
// reaches, each a crate root of its own, until no package is new; then the tree pass
|
|
3288
|
+
// (buildRustTree), again with the in-tree crate roots it names that the walk left out (a crate
|
|
3289
|
+
// whose name a file also binds as a value, `use crate::util::log;` beside `log::info!`) or walked
|
|
3290
|
+
// as fewer units than it is named as (`wantedRoots`), until none is new. Each file is compiled as
|
|
3291
|
+
// the units `units` records (cargo.js unitKey): a build script for the host, with its package's
|
|
3292
|
+
// features, and so what it reaches; a proc-macro crate and what it depends on for the host too.
|
|
3293
|
+
// A root the walk was asked for and didn't load -- refused (a link out of its package) or gone --
|
|
3294
|
+
// is not asked for again: the walk has said why. Returns `{ sources, formats, tree }`.
|
|
3295
|
+
export async function collectRustBundle(baseDir, entries, { cargo, buildScripts = false }) {
|
|
3296
|
+
const formats = new Map()
|
|
3297
|
+
const sources = new Map()
|
|
3298
|
+
const units = new Map()
|
|
3299
|
+
const rootsOf = new Map()
|
|
3300
|
+
const unloaded = new Set() // what the walk refused or found gone: said once
|
|
3301
|
+
const roots = [...entries]
|
|
3302
|
+
const walk = async (wave) => {
|
|
3303
|
+
await collectRustFilesFromDisk(baseDir, wave, { cargo, formats, sources, units, crateRoots: rootsOf, unloaded })
|
|
3304
|
+
if (!buildScripts) return
|
|
3305
|
+
const scripts = new Set()
|
|
3306
|
+
for (const path of sources.keys()) {
|
|
3307
|
+
const script = formats.has(path) ? null : cargo.buildScriptOf(path)
|
|
3308
|
+
if (script === null) continue
|
|
3309
|
+
const as = new Set([...unitsOf(units, path)].map((u) => cargo.buildScriptUnit(u)))
|
|
3310
|
+
if (addTo(units, script, as)) scripts.add(script)
|
|
3311
|
+
}
|
|
3312
|
+
if (scripts.size === 0) return
|
|
3313
|
+
for (const s of scripts) if (!roots.includes(s)) roots.push(s)
|
|
3314
|
+
await walk([...scripts])
|
|
3315
|
+
}
|
|
3316
|
+
await walk(entries)
|
|
3317
|
+
const tried = new Set()
|
|
3318
|
+
const complete = async () => {
|
|
3319
|
+
const tree = buildRustTree(sources, { roots, baseDir, cargo, formats, units })
|
|
3320
|
+
const wanted = tree.wantedRoots.filter((r) => !unloaded.has(r) && (addTo(units, r, tree.wantedUnits.get(r)) || (!sources.has(r) && !tried.has(r))))
|
|
3321
|
+
if (wanted.length === 0) return tree
|
|
3322
|
+
for (const r of wanted) {
|
|
3323
|
+
tried.add(r)
|
|
3324
|
+
if (!roots.includes(r)) roots.push(r)
|
|
3325
|
+
}
|
|
3326
|
+
await walk(wanted)
|
|
3327
|
+
return complete()
|
|
261
3328
|
}
|
|
3329
|
+
return { sources, formats, tree: await complete() }
|
|
262
3330
|
}
|
|
263
3331
|
|
|
264
|
-
// High-level entry: reads a `.rs.txt` listing of crate
|
|
265
|
-
//
|
|
3332
|
+
// High-level entry: reads a `.rs.txt` listing of crate roots (relative to the listing), then
|
|
3333
|
+
// walks `mod` declarations and in-tree crate references from there.
|
|
266
3334
|
export async function loadRust(rsTxtFile) {
|
|
267
3335
|
const baseDir = dirname(resolve(rsTxtFile))
|
|
268
3336
|
const listing = await readFile(rsTxtFile, 'utf8')
|
|
@@ -275,6 +3343,6 @@ export async function loadRust(rsTxtFile) {
|
|
|
275
3343
|
}
|
|
276
3344
|
for (const line of lines) assertWithinBase(baseDir, line, 'Entry path')
|
|
277
3345
|
|
|
278
|
-
const
|
|
279
|
-
return
|
|
3346
|
+
const { tree, formats } = await collectRustBundle(baseDir, lines, { cargo: createCargoContext(baseDir, { entries: lines }) })
|
|
3347
|
+
return { ...tree, formats }
|
|
280
3348
|
}
|