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}