1use std::collections::BTreeSet;
6use std::fmt::{self, Display};
7
8use serde::{Deserialize, Serialize};
9
10use crate::model::app_manifest::ManifestUnreadable;
11use crate::model::macro_decl::MacroKey;
12use crate::model::names::{
13 ApplicationName, Arity, BehaviourName, ConfKey, FieldName, Mfa, ModuleName, RecordName,
14 SpecText, TypeName, ValidatorName, vocabulary,
15};
16use crate::model::non_empty::NonEmpty;
17use crate::model::normalised::{NormalisedDefault, NormalisedMacroBody};
18use crate::model::schema_files::PartialRef;
19use crate::model::snapshot::{FunArity, HeaderRef};
20
21vocabulary!(
22 #[derive(PartialOrd, Ord, Hash)]
24 pub enum JigsawFindingKind: "finding kind" {
25 ModuleMissing => "module_missing",
26 FunctionMissing => "function_missing",
27 ReturnShapeChanged => "return_shape_changed",
28 HeaderMissing => "header_missing",
29 RecordMissing => "record_missing",
30 RecordFieldsChanged => "record_fields_changed",
31 MacroMissing => "macro_missing",
32 MacroChanged => "macro_changed",
33 CallbackRequired => "callback_required",
34 BehaviourMissing => "behaviour_missing",
35 TypeMissing => "type_missing",
36 PartialMissing => "partial_missing",
37 PartialKeyMissing => "partial_key_missing",
38 ValidatorMissing => "validator_missing",
39 AppMissing => "app_missing",
40 }
41);
42
43vocabulary!(
44 #[derive(PartialOrd, Ord, Hash)]
46 pub enum JigsawWarningKind: "warning kind" {
47 BundledAppShippedByHost => "bundled_app_shipped_by_host",
48 RecordDefaultChanged => "record_default_changed",
49 MacroTextChanged => "macro_text_changed",
50 ManifestUnreadable => "manifest_unreadable",
51 AppNotTracked => "app_not_tracked",
52 }
53);
54
55vocabulary!(
56 #[derive(PartialOrd, Ord, Hash)]
58 pub enum WithheldReason: "withheld reason" {
59 ConditionalMacro => "conditional_macro",
60 UnknownSpec => "unknown_spec",
61 UnreadableManifest => "unreadable_manifest",
62 UntrackedApp => "untracked_app",
63 ExcludedOtpApp => "excluded_otp_app",
64 }
65);
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub enum Stop {
69 UnreadableManifest,
70 UntrackedApp,
71}
72
73impl Stop {
74 #[must_use]
75 pub fn reason(self) -> WithheldReason {
76 match self {
77 Self::UnreadableManifest => WithheldReason::UnreadableManifest,
78 Self::UntrackedApp => WithheldReason::UntrackedApp,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
85pub enum WithheldSymbol {
86 ConditionalMacro { header: HeaderRef, key: MacroKey },
87 UnknownSpec(Mfa),
88 ExcludedOtpApp(ApplicationName),
89}
90
91impl WithheldSymbol {
92 #[must_use]
93 pub fn reason(&self) -> WithheldReason {
94 match self {
95 Self::ConditionalMacro { .. } => WithheldReason::ConditionalMacro,
96 Self::UnknownSpec(_) => WithheldReason::UnknownSpec,
97 Self::ExcludedOtpApp(_) => WithheldReason::ExcludedOtpApp,
98 }
99 }
100}
101
102impl fmt::Display for WithheldSymbol {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 Self::ConditionalMacro { header, key } => write!(f, "?{key} in {header}"),
106 Self::UnknownSpec(mfa) => write!(f, "{mfa}"),
107 Self::ExcludedOtpApp(app) => write!(f, "{app}"),
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
113pub enum Withheld {
114 Symbol(WithheldSymbol),
115 Stopped(Stop),
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
121pub enum Finding {
122 ModuleMissing {
123 module: ModuleName,
124 },
125 FunctionMissing {
126 mfa: Mfa,
127 other_arities: BTreeSet<Arity>,
128 },
129 ReturnShapeChanged {
130 mfa: Mfa,
131 build_spec: SpecText,
132 target_spec: SpecText,
133 },
134 HeaderMissing {
135 header: HeaderRef,
136 },
137 RecordMissing {
138 header: HeaderRef,
139 name: RecordName,
140 },
141 RecordFieldsChanged {
142 header: HeaderRef,
143 name: RecordName,
144 build: Vec<FieldName>,
145 target: Vec<FieldName>,
146 },
147 MacroMissing {
148 header: HeaderRef,
149 key: MacroKey,
150 },
151 MacroChanged {
152 header: HeaderRef,
153 key: MacroKey,
154 build: NormalisedMacroBody,
155 target: NormalisedMacroBody,
156 },
157 CallbackRequired {
158 behaviour: BehaviourName,
159 implementer: ModuleName,
160 callback: FunArity,
161 },
162 BehaviourMissing {
163 behaviour: BehaviourName,
164 },
165 TypeMissing {
166 module: ModuleName,
167 name: TypeName,
168 arity: Arity,
169 },
170 PartialMissing {
171 partial: PartialRef,
172 },
173 PartialKeyMissing {
174 partial: PartialRef,
175 key: ConfKey,
176 },
177 ValidatorMissing {
178 name: ValidatorName,
179 },
180 AppMissing {
181 app: ApplicationName,
182 required_by: NonEmpty<ApplicationName>,
183 },
184}
185
186impl Finding {
187 #[must_use]
188 pub fn kind(&self) -> JigsawFindingKind {
189 match self {
190 Self::ModuleMissing { .. } => JigsawFindingKind::ModuleMissing,
191 Self::FunctionMissing { .. } => JigsawFindingKind::FunctionMissing,
192 Self::ReturnShapeChanged { .. } => JigsawFindingKind::ReturnShapeChanged,
193 Self::HeaderMissing { .. } => JigsawFindingKind::HeaderMissing,
194 Self::RecordMissing { .. } => JigsawFindingKind::RecordMissing,
195 Self::RecordFieldsChanged { .. } => JigsawFindingKind::RecordFieldsChanged,
196 Self::MacroMissing { .. } => JigsawFindingKind::MacroMissing,
197 Self::MacroChanged { .. } => JigsawFindingKind::MacroChanged,
198 Self::CallbackRequired { .. } => JigsawFindingKind::CallbackRequired,
199 Self::BehaviourMissing { .. } => JigsawFindingKind::BehaviourMissing,
200 Self::TypeMissing { .. } => JigsawFindingKind::TypeMissing,
201 Self::PartialMissing { .. } => JigsawFindingKind::PartialMissing,
202 Self::PartialKeyMissing { .. } => JigsawFindingKind::PartialKeyMissing,
203 Self::ValidatorMissing { .. } => JigsawFindingKind::ValidatorMissing,
204 Self::AppMissing { .. } => JigsawFindingKind::AppMissing,
205 }
206 }
207}
208
209impl Finding {
210 #[must_use]
212 pub fn symbol(&self) -> String {
213 match self {
214 Self::ModuleMissing { module } => module.to_string(),
215 Self::FunctionMissing { mfa, .. } | Self::ReturnShapeChanged { mfa, .. } => {
216 mfa.to_string()
217 }
218 Self::HeaderMissing { header } => header.to_string(),
219 Self::RecordMissing { header, name }
220 | Self::RecordFieldsChanged { header, name, .. } => {
221 format!("#{name}{{}} in {header}")
222 }
223 Self::MacroMissing { header, key } | Self::MacroChanged { header, key, .. } => {
224 format!("?{key} in {header}")
225 }
226 Self::CallbackRequired {
227 behaviour,
228 callback,
229 ..
230 } => format!("{behaviour}:{callback}"),
231 Self::BehaviourMissing { behaviour } => behaviour.to_string(),
232 Self::TypeMissing {
233 module,
234 name,
235 arity,
236 } => format!("{module}:{name}/{arity}"),
237 Self::PartialMissing { partial } => partial.to_string(),
238 Self::PartialKeyMissing { partial, key } => format!("{partial} key {key}"),
239 Self::ValidatorMissing { name } => name.to_string(),
240 Self::AppMissing { app, .. } => app.to_string(),
241 }
242 }
243
244 #[must_use]
246 pub fn detail(&self) -> Option<String> {
247 match self {
248 Self::FunctionMissing { other_arities, .. } if !other_arities.is_empty() => {
249 let arities: Vec<String> = other_arities.iter().map(|a| format!("/{a}")).collect();
250 Some(format!("target has {}", arities.join(", ")))
251 }
252 Self::ReturnShapeChanged {
253 build_spec,
254 target_spec,
255 ..
256 } => Some(format!("build: {build_spec}; target: {target_spec}")),
257 Self::RecordFieldsChanged { build, target, .. } => Some(format!(
258 "build: {}; target: {}",
259 join(build.iter()),
260 join(target.iter())
261 )),
262 Self::MacroChanged { build, target, .. } => {
263 Some(format!("build: {build}; target: {target}"))
264 }
265 Self::CallbackRequired { implementer, .. } => {
266 Some(format!("{implementer} does not export it"))
267 }
268 Self::AppMissing { required_by, .. } => {
269 Some(format!("required by {}", join(required_by.iter())))
270 }
271 Self::ModuleMissing { .. }
272 | Self::FunctionMissing { .. }
273 | Self::HeaderMissing { .. }
274 | Self::RecordMissing { .. }
275 | Self::MacroMissing { .. }
276 | Self::BehaviourMissing { .. }
277 | Self::TypeMissing { .. }
278 | Self::PartialMissing { .. }
279 | Self::PartialKeyMissing { .. }
280 | Self::ValidatorMissing { .. } => None,
281 }
282 }
283}
284
285fn join<T: Display>(items: impl Iterator<Item = T>) -> String {
286 items
287 .map(|item| item.to_string())
288 .collect::<Vec<_>>()
289 .join(", ")
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
295pub enum Warning {
296 BundledAppShippedByHost {
297 app: ApplicationName,
298 },
299 RecordDefaultChanged {
300 header: HeaderRef,
301 record: RecordName,
302 field: FieldName,
303 build: Option<NormalisedDefault>,
304 target: Option<NormalisedDefault>,
305 },
306 MacroTextChanged {
307 header: HeaderRef,
308 key: MacroKey,
309 build: NormalisedMacroBody,
310 target: NormalisedMacroBody,
311 },
312 ManifestUnreadable {
313 app: ApplicationName,
314 reason: ManifestUnreadable,
315 },
316 AppNotTracked {
317 app: ApplicationName,
318 },
319}
320
321impl Warning {
322 #[must_use]
323 pub fn kind(&self) -> JigsawWarningKind {
324 match self {
325 Self::BundledAppShippedByHost { .. } => JigsawWarningKind::BundledAppShippedByHost,
326 Self::RecordDefaultChanged { .. } => JigsawWarningKind::RecordDefaultChanged,
327 Self::MacroTextChanged { .. } => JigsawWarningKind::MacroTextChanged,
328 Self::ManifestUnreadable { .. } => JigsawWarningKind::ManifestUnreadable,
329 Self::AppNotTracked { .. } => JigsawWarningKind::AppNotTracked,
330 }
331 }
332}
333
334impl Warning {
335 #[must_use]
336 pub fn symbol(&self) -> String {
337 match self {
338 Self::BundledAppShippedByHost { app } => app.to_string(),
339 Self::RecordDefaultChanged { record, field, .. } => format!("#{record}.{field}"),
340 Self::MacroTextChanged { header, key, .. } => format!("?{key} in {header}"),
341 Self::ManifestUnreadable { app, .. } | Self::AppNotTracked { app } => app.to_string(),
342 }
343 }
344
345 #[must_use]
346 pub fn detail(&self) -> Option<String> {
347 match self {
348 Self::BundledAppShippedByHost { .. } => None,
349 Self::RecordDefaultChanged { build, target, .. } => {
350 let show = |default: &Option<NormalisedDefault>| {
351 default
352 .as_ref()
353 .map_or_else(|| "undefined".to_owned(), ToString::to_string)
354 };
355 Some(format!("build: {}; target: {}", show(build), show(target)))
356 }
357 Self::MacroTextChanged { build, target, .. } => {
358 Some(format!("build: {build}; target: {target}"))
359 }
360 Self::ManifestUnreadable { reason, .. } => Some(match reason {
361 ManifestUnreadable::Missing => {
362 "no manifest file found: check the project's `manifest` setting".to_owned()
363 }
364 ManifestUnreadable::UnexpandedVariable(variable) => {
365 format!("the manifest uses the unexpanded variable {variable}")
366 }
367 ManifestUnreadable::ParseFailed => "the manifest could not be parsed".to_owned(),
368 }),
369 Self::AppNotTracked { .. } => {
370 Some("no `[[project]]` for the dependency that provides it".to_owned())
371 }
372 }
373 }
374}
375
376#[derive(Debug, Clone, Default, PartialEq, Eq)]
378pub struct Checked {
379 pub findings: Vec<Finding>,
380 pub warnings: Vec<Warning>,
381 pub withheld: BTreeSet<Withheld>,
382}
383
384impl Checked {
385 #[must_use]
386 pub fn satisfied() -> Self {
387 Self::default()
388 }
389
390 #[must_use]
391 pub fn broken(finding: Finding) -> Self {
392 Self {
393 findings: vec![finding],
394 ..Self::default()
395 }
396 }
397
398 #[must_use]
399 pub fn withheld(withheld: Withheld) -> Self {
400 Self {
401 withheld: BTreeSet::from([withheld]),
402 ..Self::default()
403 }
404 }
405
406 pub fn merge(&mut self, other: Self) {
407 self.findings.extend(other.findings);
408 self.warnings.extend(other.warnings);
409 self.withheld.extend(other.withheld);
410 }
411}