Skip to main content

pedant_core/resolution/rust/snapshot/
error.rs

1//! Typed failures returned while building a target or resolution snapshot.
2//!
3//! Authority refusals are returned before any source is read. Closure failures
4//! carry the site that declared the step, the path that was attempted while it
5//! remained inside the repository root, and one owning message.
6
7use std::fmt;
8
9/// Which configured ceiling a closure crossed.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ResolutionLimit {
12    /// Resolution units one snapshot may hold.
13    Units,
14    /// Distinct source files one snapshot may hold.
15    SourceFiles,
16    /// Bytes one source file may hold.
17    SourceFileBytes,
18    /// Bytes all distinct source text may hold.
19    TotalSourceBytes,
20    /// Rust module nesting depth followed inside one unit.
21    ModuleDepth,
22    /// Rust module instances one unit may hold.
23    ModuleInstances,
24    /// Cargo dependency depth followed from the root target.
25    DependencyDepth,
26    /// Syntax nesting depth accepted while parsing.
27    SyntaxDepth,
28}
29
30impl fmt::Display for ResolutionLimit {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        formatter.write_str(self.field_name())
33    }
34}
35
36impl ResolutionLimit {
37    /// The `ResolutionLimits` field this ceiling belongs to.
38    fn field_name(&self) -> &'static str {
39        match self {
40            Self::Units => "max_units",
41            Self::SourceFiles => "max_source_files",
42            Self::SourceFileBytes => "max_source_file_bytes",
43            Self::TotalSourceBytes => "max_total_source_bytes",
44            Self::ModuleDepth => "max_module_depth",
45            Self::ModuleInstances => "max_module_instances",
46            Self::DependencyDepth => "max_dependency_depth",
47            Self::SyntaxDepth => "max_syntax_depth",
48        }
49    }
50}
51
52/// Which closure step failed.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum SourceClosureFailureKind {
55    /// A target's entry point could not be read.
56    EntryRead,
57    /// A declared module's source could not be read.
58    ModuleRead,
59    /// A reached source is not valid Rust.
60    SourceParse,
61    /// A `mod` declaration names no existing source.
62    MissingModule,
63    /// A `mod` declaration matches both `name.rs` and `name/mod.rs`.
64    AmbiguousModule,
65    /// An in-repository dependency package declares no library target.
66    MissingDependencyLibraryTarget,
67    /// An in-repository dependency names a library target the project that
68    /// issued it no longer holds.
69    UnresolvedDependencyLibraryTarget,
70    /// A path the closure already interned names no source in the store.
71    MissingStoredSource,
72    /// One package repeats inside a single dependency chain.
73    DependencyCycle,
74    /// A module path or symlink resolves outside the repository root.
75    OutOfRoot,
76    /// A reached source is not valid UTF-8.
77    InvalidUtf8,
78    /// A configured ceiling was crossed.
79    LimitExceeded(ResolutionLimit),
80}
81
82/// Where a closure failure was declared.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum ClosureSite {
85    /// A Cargo target's entry point.
86    Target {
87        /// The Cargo target name.
88        name: Box<str>,
89        /// The repository-relative entry path.
90        entry: Box<str>,
91    },
92    /// A `mod` declaration inside one source.
93    Module {
94        /// The repository-relative source that declares the module.
95        file: Box<str>,
96        /// The declared module name.
97        module: Box<str>,
98    },
99    /// A Cargo dependency edge.
100    Dependency {
101        /// The package that declares the edge.
102        package: Box<str>,
103        /// The namespace-local dependency name.
104        dependency: Box<str>,
105    },
106}
107
108impl ClosureSite {
109    /// The read-failure kind this site owns: a target owns its entry point,
110    /// and every other site reads a declared module.
111    pub(super) fn read_kind(&self) -> SourceClosureFailureKind {
112        match self {
113            Self::Target { .. } => SourceClosureFailureKind::EntryRead,
114            _ => SourceClosureFailureKind::ModuleRead,
115        }
116    }
117}
118
119impl fmt::Display for ClosureSite {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            Self::Target { name, entry } => write!(formatter, "target {name} entry {entry}"),
123            Self::Module { file, module } => write!(formatter, "{file} declares mod {module}"),
124            Self::Dependency {
125                package,
126                dependency,
127            } => write!(formatter, "{package} depends on {dependency}"),
128        }
129    }
130}
131
132/// One typed closure failure with its declaring site and attempted path.
133#[derive(Debug, thiserror::Error)]
134#[error("{message}")]
135pub struct SourceClosureFailure {
136    kind: SourceClosureFailureKind,
137    site: ClosureSite,
138    attempted: Option<Box<str>>,
139    message: Box<str>,
140}
141
142impl SourceClosureFailure {
143    /// Record one failure with the evidence its kind can carry.
144    pub(super) fn new(
145        kind: SourceClosureFailureKind,
146        evidence: (ClosureSite, Option<Box<str>>),
147        message: Box<str>,
148    ) -> Self {
149        let (site, attempted) = evidence;
150        Self {
151            kind,
152            site,
153            attempted,
154            message,
155        }
156    }
157
158    /// Which closure step failed.
159    pub fn kind(&self) -> SourceClosureFailureKind {
160        self.kind
161    }
162
163    /// The site that declared the failed step. Every closure step is declared
164    /// by a target entry, a `mod` item, or a Cargo edge, so there is always one.
165    pub fn site(&self) -> &ClosureSite {
166        &self.site
167    }
168
169    /// The attempted repository-relative path, when it stayed inside the root.
170    pub fn attempted(&self) -> Option<&str> {
171        self.attempted.as_deref()
172    }
173
174    /// The single-owner reason this step failed.
175    pub fn message(&self) -> &str {
176        &self.message
177    }
178
179    /// Whether this failure ends the traversal rather than one branch of it.
180    ///
181    /// A crossed ceiling makes every later step meaningless, so reporting the
182    /// same limit once per remaining source would bury the one real cause.
183    pub(super) fn is_fatal(&self) -> bool {
184        matches!(self.kind, SourceClosureFailureKind::LimitExceeded(_))
185    }
186}
187
188/// Every failure one closure attempt collected, beside the paths it did reach.
189#[derive(Debug, thiserror::Error)]
190#[error("source closure failed after reaching {} path(s): {}", .reached.len(), render(.failures))]
191pub struct SourceClosureError {
192    reached: Box<[Box<str>]>,
193    failures: Box<[SourceClosureFailure]>,
194}
195
196impl SourceClosureError {
197    /// Bind sorted reached paths to the failures that stopped the closure.
198    pub(super) fn new(reached: Box<[Box<str>]>, failures: Box<[SourceClosureFailure]>) -> Self {
199        Self { reached, failures }
200    }
201
202    /// The sorted paths this attempt reached, as evidence only. They are not a
203    /// partial snapshot and must never be hashed as a complete closure.
204    pub fn reached(&self) -> &[Box<str>] {
205        &self.reached
206    }
207
208    /// Every typed failure this attempt collected.
209    pub fn failures(&self) -> &[SourceClosureFailure] {
210        &self.failures
211    }
212}
213
214fn render(failures: &[SourceClosureFailure]) -> String {
215    failures
216        .iter()
217        .map(|failure| failure.message.to_string())
218        .collect::<Vec<_>>()
219        .join("; ")
220}
221
222/// Failure returned by either snapshot operation.
223#[derive(Debug, thiserror::Error)]
224pub enum RustSnapshotError {
225    /// The package identity was issued by a project rooted somewhere else.
226    #[error("the package identity was issued by another repository root")]
227    ForeignPackage,
228    /// The package identity was issued for another revision of this repository.
229    #[error("the package identity was issued for another manifest revision")]
230    StalePackage,
231    /// The package identity names a local index this project never issued.
232    #[error("no package occupies local index {index}")]
233    UnknownPackage {
234        /// The absent local index.
235        index: u32,
236    },
237    /// The identity was issued by a project rooted somewhere else.
238    #[error("the target identity was issued by another repository root")]
239    ForeignTarget,
240    /// The identity was issued for another revision of this repository.
241    #[error("the target identity was issued for another manifest revision")]
242    StaleTarget,
243    /// The identity names a local index this project never issued.
244    #[error("no target occupies local index {index}")]
245    UnknownTarget {
246        /// The absent local index.
247        index: u32,
248    },
249    /// A participating manifest changed after the project was loaded.
250    #[error("manifest {manifest} changed since the project was loaded")]
251    ProjectManifestsChanged {
252        /// The repository-relative manifest that no longer matches.
253        manifest: Box<str>,
254    },
255    /// A participating manifest could not be read while its revision was
256    /// re-checked, so whether it still matches is unknown rather than false.
257    #[error("manifest {manifest} could not be re-read: {source}")]
258    ManifestUnreadable {
259        /// The repository-relative manifest that could not be read.
260        manifest: Box<str>,
261        /// The underlying I/O failure.
262        #[source]
263        source: std::io::Error,
264    },
265    /// The target's source closure could not be completed.
266    #[error(transparent)]
267    SourceClosure(#[from] SourceClosureError),
268}