pedant-core 0.24.1

Analysis engine for pedant: IR extraction, style checks, and capability detection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! The shared source store: which sources a snapshot holds, keyed by their
//! normalized repository-relative paths.
//!
//! The store no longer opens a file. It asks a provider for one normalized
//! path, and the provider answers with the exact bytes, the digest, and the one
//! inventory that path states — reading and parsing it only if this is the
//! first request in that provider's life. Several units inside one snapshot,
//! and several snapshots that share one provider, therefore all instantiate the
//! same parse.
//!
//! The interning table, the key buffer, the file-count ceiling, the byte
//! charge, and the sorted finish belong to the shared
//! [`SnapshotStoreOf`](crate::resolution::snapshot_store::SnapshotStoreOf).
//! What this module states is the Rust half: the nesting ceiling and the
//! edition compatibility a retained record owes, the `mod` items each stored
//! source declares, and the closure vocabulary every refusal is published in.
//!
//! The provider is a parameter rather than a field. Everything else this store
//! answers — which paths it holds, what they declare, what they cost — is
//! answered from what it already holds, and only [`SourceStore::intern`] can
//! need a reader.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use pedant_types::SourceRecord;

use crate::resolution::confinement::{ConfinementFault, canonical_present};
use crate::resolution::path_normalization::RelativePathError;
use crate::resolution::read::{ByteCeilings, ReadFault};
use crate::resolution::rust::edition::CargoEdition;
use crate::resolution::rust::fault::RustSourceFault;
use crate::resolution::rust::inventory::RustFileInventory;
use crate::resolution::rust::limits::{ResolutionLimits, admits_depth};
use crate::resolution::rust::paths;
use crate::resolution::snapshot_rules::SnapshotRules;
use crate::resolution::snapshot_store::{Interned, SnapshotStoreOf};
use crate::resolution::supply::SourceSupply;

use super::declaration::{ModuleDeclaration, declarations_by_scope};
use super::error::{
    ClosureSite, ResolutionLimit, RustSnapshotError, SourceClosureError, SourceClosureFailure,
    SourceClosureFailureKind,
};
use super::failure::{at_path, failure, limit_failure, read_failure};
use super::source::RustSource;

/// One instantiated source beside the `mod` items it declares, projected once.
struct StoredSource {
    source: RustSource,
    declarations: BTreeMap<u32, Arc<[ModuleDeclaration]>>,
}

/// Every distinct source one snapshot reached, keyed by normalized path.
pub(super) struct SourceStore {
    store: SnapshotStoreOf<RustSnapshotRules>,
    /// The answer for a scope that declares no module, shared rather than
    /// allocated once per childless scope.
    empty: Arc<[ModuleDeclaration]>,
}

impl SourceStore {
    /// An empty store rooted at one canonical repository root.
    pub(super) fn new(root: &Path, limits: ResolutionLimits) -> Self {
        Self {
            store: SnapshotStoreOf::new(RustSnapshotRules {
                root: root.to_path_buf(),
                limits,
            }),
            empty: Arc::from(Vec::new()),
        }
    }

    /// The canonical repository root every read must stay inside.
    pub(super) fn root(&self) -> &Path {
        self.store.rules().root()
    }

    /// The ceilings this store enforces.
    pub(super) fn limits(&self) -> ResolutionLimits {
        self.store.rules().limits
    }

    /// Resolve `path` to its canonical form and refuse anything outside the
    /// repository root, following symlinks before the decision.
    pub(super) fn canonical_inside(
        &self,
        path: &Path,
        site: &ClosureSite,
    ) -> Result<PathBuf, SourceClosureFailure> {
        canonical_present(self.root(), path).map_err(|fault| match fault {
            ConfinementFault::Unreadable(source) => failure(
                site.read_kind(),
                (site, self.relative_of(path)),
                format!("{site}: the source could not be resolved: {source}"),
            ),
            // The failure's path stays the request, because that is what a
            // caller can act on and the canonical target has no
            // repository-relative spelling by definition. Where it landed is
            // named in the sentence instead: a request that escaped through a
            // link is spelled inside the root, so the request alone reads as a
            // contradiction.
            ConfinementFault::OutOfRoot { canonical } => {
                outside_root(site, self.relative_of(path), &paths::path_text(&canonical))
            }
        })
    }

    /// Instantiate `canonical` in this snapshot unless it is already held, and
    /// return its normalized repository-relative path.
    ///
    /// A source already held answers with the key retained by the store.
    ///
    /// Whether a source this snapshot already holds may also be compiled on
    /// `edition` is asked on the branch that has to ask it. A source admitted by
    /// this request was checked before it was retained, so only the held branch
    /// asks again, from the position the one lookup already produced.
    pub(super) fn intern<P: SourceSupply<RustFileInventory, RustSourceFault>>(
        &mut self,
        provider: &mut P,
        canonical: &Path,
        site: &ClosureSite,
        edition: CargoEdition,
    ) -> Result<Arc<str>, SourceClosureFailure> {
        let ceiling = self.limits().max_syntax_depth;
        let interned = self
            .store
            .intern(provider, canonical, site, |path, record| {
                retained(record, (site, path), (edition, ceiling))
            })?;
        match interned {
            Interned::Admitted(placement) => Ok(placement.path),
            Interned::Held(placement) => {
                self.check_held_edition(placement.index, edition, (site, &placement.path))?;
                Ok(placement.path)
            }
        }
    }

    /// Whether a source this snapshot already holds may also be compiled by a
    /// unit on `edition`.
    ///
    /// Takes the position [`Self::intern`]'s one lookup already produced rather
    /// than searching for the path again. The remaining absence is a slice
    /// index rather than a map miss: it names an index the store issued and no
    /// longer holds, which is the store disagreeing with itself.
    fn check_held_edition(
        &self,
        index: usize,
        edition: CargoEdition,
        at: (&ClosureSite, &str),
    ) -> Result<(), SourceClosureFailure> {
        let (site, relative) = at;
        let stored = self
            .store
            .stored(index)
            .ok_or_else(|| missing_stored_source(site, relative))?;
        compatible(stored.source.legacy_callable_trait_error(), edition, at)
    }

    /// The `mod` items one stored source declares inside one of its scopes.
    ///
    /// A path this store never interned is a store inconsistency rather than a
    /// source that declares nothing, so it is reported instead of answered.
    pub(super) fn declarations(
        &self,
        request: (&str, u32),
        site: &ClosureSite,
    ) -> Result<Arc<[ModuleDeclaration]>, SourceClosureFailure> {
        let (path, scope) = request;
        let stored = self
            .store
            .stored_at(path)
            .ok_or_else(|| missing_stored_source(site, path))?;
        Ok(stored
            .declarations
            .get(&scope)
            .map(Arc::clone)
            .unwrap_or_else(|| Arc::clone(&self.empty)))
    }

    /// The sorted normalized paths this store reached, as failure evidence.
    ///
    /// The evidence shares the keys retained by this store.
    pub(super) fn reached(&self) -> Box<[Arc<str>]> {
        self.store.keys()
    }

    /// Every stored source, sorted by normalized path.
    pub(super) fn finish(self) -> Box<[RustSource]> {
        self.store
            .finish()
            .into_vec()
            .into_iter()
            .map(|stored| stored.source)
            .collect()
    }

    /// One path as failure evidence, when it has a repository-relative
    /// spelling.
    ///
    /// Owned rather than shared: every reader of this is a failure payload that
    /// states the path once and is dropped with the refusal.
    pub(super) fn relative_of(&self, path: &Path) -> Option<Box<str>> {
        paths::relative_text(self.root(), path).ok()
    }
}

/// The Rust half of one snapshot retention: the root it names paths against,
/// the ceilings it holds sources to, and the vocabulary it refuses in.
struct RustSnapshotRules {
    root: PathBuf,
    limits: ResolutionLimits,
}

/// The byte ceilings, taken from the limits this snapshot was taken under.
impl ByteCeilings for RustSnapshotRules {
    fn source_bytes(&self) -> u64 {
        self.limits.source_bytes()
    }

    fn total_bytes(&self) -> u64 {
        self.limits.total_bytes()
    }
}

impl SnapshotRules for RustSnapshotRules {
    type Inventory = RustFileInventory;
    type Fault = RustSourceFault;
    type Refusal = SourceClosureFailure;
    /// A Rust refusal names the target entry, `mod` item, or Cargo edge that
    /// asked for the source, which is what tells a caller which declaration
    /// leads to it.
    type Site = ClosureSite;
    type Stored = StoredSource;

    fn root(&self) -> &Path {
        &self.root
    }

    fn max_source_files(&self) -> u32 {
        self.limits.max_source_files
    }

    fn stored_path(stored: &StoredSource) -> &str {
        stored.source.path()
    }

    fn unrelative(
        &self,
        error: RelativePathError,
        (site, canonical): (&ClosureSite, &Path),
    ) -> SourceClosureFailure {
        let source = paths::unrelative(error, &self.root, canonical);
        failure(
            SourceClosureFailureKind::OutOfRoot,
            (site, None),
            format!("{site}: the source path is not repository-relative: {source}"),
        )
    }

    fn source_files(&self, at: (&ClosureSite, &str)) -> SourceClosureFailure {
        crossed(
            ResolutionLimit::SourceFiles,
            self.max_source_files().into(),
            at,
        )
    }

    fn unnormalized(&self, at: (&ClosureSite, &str)) -> SourceClosureFailure {
        let (site, relative) = at;
        at_path(
            SourceClosureFailureKind::OutOfRoot,
            at,
            format!("{site}: {relative} is not a normalized repository-relative path"),
        )
    }

    fn refused(&self, fault: RustSourceFault, at: (&ClosureSite, &str)) -> SourceClosureFailure {
        refusal(fault, at)
    }

    fn oversized(&self, fault: ReadFault, at: (&ClosureSite, &str)) -> SourceClosureFailure {
        refusal(RustSourceFault::of_read(fault, self.limits), at)
    }
}

/// Hold one supplied record to this snapshot's own nesting ceiling and edition,
/// then project the `mod` items it declares.
///
/// The byte charge belongs to the shared store; these two are Rust's own, and
/// they are checked before the record is retained so a snapshot that refuses
/// holds nothing it would have to unwind and names no path in
/// [`SourceStore::reached`] that it did not complete.
fn retained(
    record: &SourceRecord<RustFileInventory>,
    at: (&ClosureSite, &Arc<str>),
    admits: (CargoEdition, u32),
) -> Result<StoredSource, SourceClosureFailure> {
    let (site, relative) = at;
    let (edition, max_syntax_depth) = admits;
    within_depth(record.facts().syntax_depth(), max_syntax_depth, at)?;
    compatible(
        record.facts().legacy_callable_trait_error(),
        edition,
        (site, &**relative),
    )?;
    let source = RustSource::of_record(relative, record);
    Ok(StoredSource {
        declarations: declarations_by_scope(source.ir()),
        source,
    })
}

/// The nesting one retained source may reach under this snapshot's ceiling.
///
/// A provider may parse beneath a looser ceiling than the snapshot that asked,
/// so a record another snapshot already parsed is measured exactly as a first
/// parse is.
fn within_depth(
    depth: u32,
    ceiling: u32,
    at: (&ClosureSite, &Arc<str>),
) -> Result<(), SourceClosureFailure> {
    let (site, relative) = at;
    match admits_depth(depth, ceiling) {
        true => Ok(()),
        false => Err(limit_failure(
            ResolutionLimit::SyntaxDepth,
            (site, Some(Box::from(&**relative))),
            ceiling.into(),
        )),
    }
}

/// This seam's own failure for one refused provider request.
fn refusal(fault: RustSourceFault, at: (&ClosureSite, &str)) -> SourceClosureFailure {
    let (site, relative) = at;
    match fault {
        RustSourceFault::Unreadable(source) => read_failure(site, relative, source),
        RustSourceFault::OutOfRoot { path, landing } => outside_root(site, Some(path), &landing),
        RustSourceFault::SourceFiles { ceiling } => {
            crossed(ResolutionLimit::SourceFiles, ceiling, at)
        }
        RustSourceFault::SourceBytes { ceiling } => {
            crossed(ResolutionLimit::SourceFileBytes, ceiling, at)
        }
        RustSourceFault::TotalBytes { ceiling } => {
            crossed(ResolutionLimit::TotalSourceBytes, ceiling, at)
        }
        RustSourceFault::SyntaxDepth { ceiling } => {
            crossed(ResolutionLimit::SyntaxDepth, ceiling, at)
        }
        RustSourceFault::InvalidUtf8 { reason } => at_path(
            SourceClosureFailureKind::InvalidUtf8,
            at,
            format!("{relative} is not valid UTF-8: {reason}"),
        ),
        RustSourceFault::Unparsed { reason } => at_path(
            SourceClosureFailureKind::SourceParse,
            at,
            format!("{relative} is not valid Rust: {reason}"),
        ),
        // The provider states the path it remembers refusing; this closure
        // states the site that asked for it again, which is what tells a
        // caller which `mod` item leads to the source that cannot be read.
        RustSourceFault::Refused { path } => at_path(
            site.read_kind(),
            at,
            format!("{path} was already refused by the provider reading for this snapshot"),
        ),
    }
}

/// One source that resolved outside the repository root, and where it landed.
///
/// The path is optional because the seam that measured the escape may hold no
/// repository-relative spelling for it. Naming it when it is known is what tells
/// a caller which source to act on, rather than only that some source escaped.
fn outside_root(site: &ClosureSite, path: Option<Box<str>>, landing: &str) -> SourceClosureFailure {
    let message = match &path {
        Some(relative) => {
            format!("{site}: {relative} resolves outside the repository root, at {landing}")
        }
        None => format!("{site}: the source resolves outside the repository root, at {landing}"),
    };
    failure(SourceClosureFailureKind::OutOfRoot, (site, path), message)
}

/// A ceiling the provider crossed, named against the path that crossed it.
fn crossed(limit: ResolutionLimit, ceiling: u64, at: (&ClosureSite, &str)) -> SourceClosureFailure {
    let (site, relative) = at;
    limit_failure(limit, (site, Some(Box::from(relative))), ceiling)
}

/// Whether a tree that needed pre-2021 repair may be compiled on `edition`.
///
/// One owner for both the first instantiation and every later one: the provider
/// parses permissively so a source reached from two editions is read once, and
/// this is the single place that decides which units may then use it.
fn compatible(
    legacy_error: Option<&str>,
    edition: CargoEdition,
    at: (&ClosureSite, &str),
) -> Result<(), SourceClosureFailure> {
    let (site, relative) = at;
    match (legacy_error, edition.permits_bare_callable_traits()) {
        (None, _) | (Some(_), true) => Ok(()),
        (Some(strict_error), false) => Err(failure(
            SourceClosureFailureKind::SourceParse,
            (site, Some(Box::from(relative))),
            format!("{relative} is not valid Rust: {strict_error}"),
        )),
    }
}

fn missing_stored_source(site: &ClosureSite, path: &str) -> SourceClosureFailure {
    failure(
        SourceClosureFailureKind::MissingStoredSource,
        (site, Some(Box::from(path))),
        format!("{site}: {path} is instantiated but holds no stored source"),
    )
}

/// Refuse a snapshot, binding the paths that were reached to the failures that
/// stopped the closure.
pub(super) fn refuse(
    store: &SourceStore,
    failures: Vec<SourceClosureFailure>,
) -> RustSnapshotError {
    RustSnapshotError::SourceClosure(SourceClosureError::new(
        store.reached(),
        failures.into_boxed_slice(),
    ))
}