aiken_project/
error.rs

1use crate::{blueprint, deps::manifest::Package, package_name::PackageName};
2use aiken_lang::{
3    ast::{self, Span},
4    error::ExtraData,
5    parser::error::ParseError,
6    test_framework::{BenchmarkResult, PropertyTestResult, TestResult, UnitTestResult},
7    tipo,
8};
9use hex::FromHexError;
10use indoc::formatdoc;
11use miette::{
12    Diagnostic, EyreContext, LabeledSpan, MietteHandler, MietteHandlerOpts, NamedSource, RgbColors,
13    SourceCode,
14};
15use ordinal::Ordinal;
16use owo_colors::{
17    OwoColorize,
18    Stream::{Stderr, Stdout},
19};
20use pallas_addresses::ScriptHash;
21use std::{
22    collections::BTreeSet,
23    fmt::{self, Debug, Display},
24    io,
25    path::{Path, PathBuf},
26};
27use zip::result::ZipError;
28
29pub enum TomlLoadingContext {
30    Project,
31    Manifest,
32    Package,
33    Workspace,
34}
35
36impl fmt::Display for TomlLoadingContext {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            TomlLoadingContext::Project => write!(f, "project"),
40            TomlLoadingContext::Manifest => write!(f, "manifest"),
41            TomlLoadingContext::Package => write!(f, "package"),
42            TomlLoadingContext::Workspace => write!(f, "workspace"),
43        }
44    }
45}
46
47#[allow(dead_code)]
48#[derive(thiserror::Error)]
49pub enum Error {
50    #[error("I just found two modules with the same name: '{}'", module.if_supports_color(Stderr, |s| s.yellow()))]
51    DuplicateModule {
52        module: String,
53        first: PathBuf,
54        second: PathBuf,
55    },
56
57    #[error("Some operation on the file-system did fail.")]
58    FileIo { error: io::Error, path: PathBuf },
59
60    #[error("I found some files with incorrectly formatted source code.")]
61    Format { problem_files: Vec<Unformatted> },
62
63    #[error(transparent)]
64    Blueprint(#[from] Box<blueprint::Error>),
65
66    #[error(transparent)]
67    StandardIo(#[from] io::Error),
68
69    #[error(transparent)]
70    Http(#[from] reqwest::Error),
71
72    #[error(transparent)]
73    ZipExtract(#[from] ZipError),
74
75    #[error(transparent)]
76    JoinError(#[from] tokio::task::JoinError),
77
78    #[error(transparent)]
79    Json(#[from] serde_json::Error),
80
81    #[error(transparent)]
82    Module(#[from] ast::Error),
83
84    #[error("I could not load the {ctx} config file.")]
85    TomlLoading {
86        ctx: TomlLoadingContext,
87        path: PathBuf,
88        src: String,
89        named: Box<NamedSource<String>>,
90        location: Option<Span>,
91        help: String,
92    },
93
94    #[error("I couldn't find any 'aiken.toml' manifest in {path}.")]
95    MissingManifest { path: PathBuf },
96
97    #[error("I just found a cycle in module hierarchy!")]
98    ImportCycle { modules: Vec<String> },
99
100    #[error("While parsing files...")]
101    Parse {
102        path: PathBuf,
103        src: String,
104        named: Box<NamedSource<String>>,
105        #[source]
106        error: Box<ParseError>,
107    },
108
109    #[error("While trying to make sense of your code...")]
110    Type {
111        path: PathBuf,
112        src: String,
113        named: NamedSource<String>,
114        #[source]
115        error: Box<tipo::error::Error>,
116    },
117
118    #[error("{name} failed{}", if *verbose { format!("\n{src}") } else { String::new() } )]
119    TestFailure {
120        name: String,
121        path: PathBuf,
122        verbose: bool,
123        src: String,
124    },
125
126    #[error(
127        "I was unable to resolve '{}' for {}/{}",
128        package.version,
129        package.name.owner,
130        package.name.repo
131    )]
132    UnknownPackageVersion { package: Package },
133
134    #[error(
135        "I need to resolve a package {}/{}, but couldn't find it.",
136        package.name.owner,
137        package.name.repo,
138    )]
139    UnableToResolvePackage { package: Package },
140
141    #[error("I couldn't parse the provided stake address.")]
142    MalformedStakeAddress {
143        error: Option<pallas_addresses::Error>,
144    },
145
146    #[error("I couldn't find any exportable function named '{name}' in module '{module}'.")]
147    ExportNotFound { module: String, name: String },
148
149    #[error("No such module '{module}' found in the project.")]
150    ModuleNotFound {
151        module: String,
152        known_modules: Vec<String>,
153    },
154
155    #[error("I located conditional modules under 'env', but no default one!")]
156    NoDefaultEnvironment,
157
158    #[error(
159        "I couldn't find any script matching {} in your blueprint (plutus.json).",
160        script_hash.to_string().if_supports_color(Stdout, |s| s.yellow()),
161    )]
162    ScriptOverrideNotFound {
163        script_hash: ScriptHash,
164        known_scripts: BTreeSet<ScriptHash>,
165    },
166
167    #[error(
168        "I couldn't parse the {} script override argument.",
169        Ordinal(*index as u32 + 1).to_string().if_supports_color(Stdout, |s| s.bold()),
170    )]
171    ScriptOverrideArgumentParseError {
172        index: usize,
173        #[source]
174        error: ScriptOverrideArgumentError,
175    },
176}
177
178#[derive(thiserror::Error, Debug, Diagnostic)]
179pub enum ScriptOverrideArgumentError {
180    #[error(
181        "I couldn't find the left side of the mapping. Are you sure you provided two colon-separated validator qualifiers?\n"
182    )]
183    #[diagnostic(help(
184        "{}",
185        formatdoc!{
186            r#"I am expecting a mapping {from} a validator of the transaction, {to} a replacement in the blueprint as such:
187
188                --script-override "122171C7C82348C420A47AFD8F{colon}7730C913CEEC22524B98E1604A"
189                                   {underline}
190                                    {missing}
191            "#,
192            from = "from"
193                .if_supports_color(Stdout, |s| s.bold()),
194            to = "to"
195                .if_supports_color(Stdout, |s| s.bold()),
196            colon = ":"
197                .if_supports_color(Stderr, |s| s.cyan())
198                .if_supports_color(Stderr, |s| s.bold()),
199            underline = "^^^^^^^^^^^^^^^^^^^^^^^^^^"
200                .if_supports_color(Stdout, |s| s.yellow())
201                .if_supports_color(Stdout, |s| s.bold()),
202            missing = "missing left side (from)"
203                .if_supports_color(Stdout, |s| s.yellow())
204        },
205    ))]
206    MissingFrom,
207
208    #[error(
209        "I couldn't find the right side of the mapping. Are you sure you provided two colon-separated validator qualifiers?\n"
210    )]
211    #[diagnostic(help(
212        "{}",
213        formatdoc!{
214            r#"I am expecting a mapping {from} a validator of the transaction, {to} a replacement in the blueprint as such:
215
216                --script-override "122171C7C82348C420A47AFD8F{colon}7730C913CEEC22524B98E1604A"
217                                                              {underline}
218                                                               {missing}
219            "#,
220            from = "from"
221                .if_supports_color(Stdout, |s| s.bold()),
222            to = "to"
223                .if_supports_color(Stdout, |s| s.bold()),
224            colon = ":"
225                .if_supports_color(Stderr, |s| s.cyan())
226                .if_supports_color(Stderr, |s| s.bold()),
227            underline = "^^^^^^^^^^^^^^^^^^^^^^^^^^"
228                .if_supports_color(Stdout, |s| s.yellow())
229                .if_supports_color(Stdout, |s| s.bold()),
230            missing = "missing right side (to)"
231                .if_supports_color(Stdout, |s| s.yellow())
232        },
233    ))]
234    MissingTo,
235
236    #[error(
237        "The origin qualifier ({}) isn't a valid hex-encoded hash digest.\n",
238        "from".if_supports_color(Stdout, |s| s.bold()),
239    )]
240    #[diagnostic(help("{0}"))]
241    InvalidFromHash(FromHexError),
242
243    #[error(
244        "The destination qualifier ({}) isn't a valid hex-encoded hash digest.\n",
245        "to".if_supports_color(Stdout, |s| s.bold()),
246    )]
247    #[diagnostic(help("{0}"))]
248    InvalidToHash(FromHexError),
249
250    #[error(
251        "The origin qualifier ({}) isn't a valid script hash digest.\n",
252        "from".if_supports_color(Stdout, |s| s.bold()),
253    )]
254    #[diagnostic(help(
255        "The size is incorrect. I expected to find {} bytes but found {}.",
256        "28".if_supports_color(Stdout, |s| s.green()),
257        .0.to_string().if_supports_color(Stdout, |s| s.red()),
258    ))]
259    InvalidFromSize(usize),
260
261    #[error(
262        "The destination qualifier ({}) isn't a valid script hash digest.\n",
263        "to".if_supports_color(Stdout, |s| s.bold()),
264    )]
265    #[diagnostic(help(
266        "The size is incorrect. I expected to find {} bytes but found {}.",
267        "28".if_supports_color(Stdout, |s| s.green()),
268        .0.to_string().if_supports_color(Stdout, |s| s.red()),
269    ))]
270    InvalidToSize(usize),
271}
272
273impl Error {
274    pub fn report(&self) {
275        if let Error::TestFailure { verbose, .. } = self {
276            if !verbose {
277                return;
278            }
279        }
280
281        println!("{self:?}")
282    }
283
284    pub fn from_parse_errors(errs: Vec<ParseError>, path: &Path, src: &str) -> Vec<Self> {
285        let mut errors = Vec::with_capacity(errs.len());
286
287        for error in errs {
288            errors.push(Error::Parse {
289                path: path.into(),
290                src: src.to_string(),
291                named: NamedSource::new(path.display().to_string(), src.to_string()).into(),
292                error: error.into(),
293            });
294        }
295
296        errors
297    }
298
299    pub fn from_test_result<U, T>(result: &TestResult<U, T>, verbose: bool) -> Self {
300        let (name, path, src) = match result {
301            TestResult::UnitTestResult(UnitTestResult { test, .. }) => (
302                test.name.to_string(),
303                test.input_path.to_path_buf(),
304                test.program.to_pretty(),
305            ),
306            TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => (
307                test.name.to_string(),
308                test.input_path.to_path_buf(),
309                test.program.to_pretty(),
310            ),
311            TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => (
312                bench.name.to_string(),
313                bench.input_path.to_path_buf(),
314                bench.program.to_pretty(),
315            ),
316        };
317
318        Error::TestFailure {
319            name,
320            path,
321            src,
322            verbose,
323        }
324    }
325}
326
327impl Debug for Error {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        default_miette_handler(2)
330            .debug(self, f)
331            // Ignore error to prevent format! panics. This can happen if span points at some
332            // inaccessible location, for example by calling `report_error()` with wrong working set.
333            .or(Ok(()))
334    }
335}
336
337impl From<Error> for Vec<Error> {
338    fn from(value: Error) -> Self {
339        vec![value]
340    }
341}
342
343impl ExtraData for Error {
344    fn extra_data(&self) -> Option<String> {
345        match self {
346            Error::DuplicateModule { .. }
347            | Error::FileIo { .. }
348            | Error::Format { .. }
349            | Error::StandardIo { .. }
350            | Error::Blueprint { .. }
351            | Error::MissingManifest { .. }
352            | Error::TomlLoading { .. }
353            | Error::ImportCycle { .. }
354            | Error::Parse { .. }
355            | Error::TestFailure { .. }
356            | Error::Http { .. }
357            | Error::ZipExtract { .. }
358            | Error::JoinError { .. }
359            | Error::UnknownPackageVersion { .. }
360            | Error::UnableToResolvePackage { .. }
361            | Error::Json { .. }
362            | Error::MalformedStakeAddress { .. }
363            | Error::Module { .. }
364            | Error::NoDefaultEnvironment
365            | Error::ModuleNotFound { .. }
366            | Error::ExportNotFound { .. }
367            | Error::ScriptOverrideNotFound { .. }
368            | Error::ScriptOverrideArgumentParseError { .. } => None,
369            Error::Type { error, .. } => error.extra_data(),
370        }
371    }
372}
373
374pub trait GetSource {
375    fn path(&self) -> Option<PathBuf>;
376    fn src(&self) -> Option<String>;
377}
378
379impl GetSource for Error {
380    fn path(&self) -> Option<PathBuf> {
381        match self {
382            Error::FileIo { .. }
383            | Error::Format { .. }
384            | Error::StandardIo(_)
385            | Error::Blueprint(_)
386            | Error::ImportCycle { .. }
387            | Error::Http(_)
388            | Error::ZipExtract(_)
389            | Error::JoinError(_)
390            | Error::UnknownPackageVersion { .. }
391            | Error::UnableToResolvePackage { .. }
392            | Error::Json { .. }
393            | Error::MalformedStakeAddress { .. }
394            | Error::ModuleNotFound { .. }
395            | Error::ExportNotFound { .. }
396            | Error::NoDefaultEnvironment
397            | Error::Module { .. }
398            | Error::ScriptOverrideNotFound { .. }
399            | Error::ScriptOverrideArgumentParseError { .. } => None,
400            Error::DuplicateModule { second: path, .. }
401            | Error::MissingManifest { path }
402            | Error::TomlLoading { path, .. }
403            | Error::Parse { path, .. }
404            | Error::Type { path, .. }
405            | Error::TestFailure { path, .. } => Some(path.to_path_buf()),
406        }
407    }
408
409    fn src(&self) -> Option<String> {
410        match self {
411            Error::DuplicateModule { .. }
412            | Error::FileIo { .. }
413            | Error::Format { .. }
414            | Error::StandardIo(_)
415            | Error::Blueprint(_)
416            | Error::MissingManifest { .. }
417            | Error::ImportCycle { .. }
418            | Error::TestFailure { .. }
419            | Error::Http(_)
420            | Error::ZipExtract(_)
421            | Error::JoinError(_)
422            | Error::UnknownPackageVersion { .. }
423            | Error::UnableToResolvePackage { .. }
424            | Error::Json { .. }
425            | Error::MalformedStakeAddress { .. }
426            | Error::NoDefaultEnvironment
427            | Error::ModuleNotFound { .. }
428            | Error::ExportNotFound { .. }
429            | Error::Module { .. }
430            | Error::ScriptOverrideNotFound { .. }
431            | Error::ScriptOverrideArgumentParseError { .. } => None,
432            Error::TomlLoading { src, .. } | Error::Parse { src, .. } | Error::Type { src, .. } => {
433                Some(src.to_string())
434            }
435        }
436    }
437}
438
439impl Diagnostic for Error {
440    fn severity(&self) -> Option<miette::Severity> {
441        Some(miette::Severity::Error)
442    }
443
444    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
445        fn boxed<'a>(s: Box<dyn Display + 'a>) -> Box<dyn Display + 'a> {
446            Box::new(format!(
447                "        {} {}",
448                "Error"
449                    .if_supports_color(Stdout, |s| s.red())
450                    .if_supports_color(Stdout, |s| s.bold()),
451                format!("{s}").if_supports_color(Stdout, |s| s.red())
452            ))
453        }
454
455        match self {
456            Error::DuplicateModule { .. } => Some(boxed(Box::new("aiken::module::duplicate"))),
457            Error::Blueprint(e) => e.code().map(boxed),
458            Error::ImportCycle { .. } => Some(boxed(Box::new("aiken::module::cyclical"))),
459            Error::Parse { .. } => Some(boxed(Box::new("aiken::parser"))),
460            Error::Type { error, .. } => Some(boxed(Box::new(format!(
461                "aiken::check{}",
462                error.code().map(|s| format!("::{s}")).unwrap_or_default()
463            )))),
464            Error::TomlLoading { .. } => Some(boxed(Box::new("aiken::loading::toml"))),
465            Error::TestFailure { path, .. } => Some(boxed(Box::new(path.to_str().unwrap_or("")))),
466            Error::Http(_) => Some(Box::new("aiken::packages::download")),
467            Error::UnknownPackageVersion { .. } => {
468                Some(boxed(Box::new("aiken::packages::resolve")))
469            }
470            Error::UnableToResolvePackage { .. } => {
471                Some(boxed(Box::new("aiken::package::download")))
472            }
473            Error::StandardIo(_)
474            | Error::MissingManifest { .. }
475            | Error::ZipExtract(_)
476            | Error::JoinError(_)
477            | Error::FileIo { .. }
478            | Error::Format { .. }
479            | Error::Json { .. }
480            | Error::MalformedStakeAddress { .. }
481            | Error::ExportNotFound { .. }
482            | Error::ModuleNotFound { .. }
483            | Error::NoDefaultEnvironment
484            | Error::ScriptOverrideNotFound { .. }
485            | Error::ScriptOverrideArgumentParseError { .. } => None,
486            Error::Module(e) => e.code().map(boxed),
487        }
488    }
489
490    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
491        match self {
492            Error::DuplicateModule { first, second, .. } => Some(Box::new(format!(
493                "Rename either of them:\n- {}\n- {}",
494                first.display().if_supports_color(Stderr, |s| s.yellow()),
495                second.display().if_supports_color(Stderr, |s| s.yellow()),
496            ))),
497            Error::FileIo { error, .. } => Some(Box::new(format!("{error}"))),
498            Error::Blueprint(e) => e.help(),
499            Error::ImportCycle { modules } => Some(Box::new(format!(
500                "Try moving the shared code to a separate module that the others can depend on\n- {}",
501                modules.join("\n- ")
502            ))),
503            Error::Parse { error, .. } => error.help(),
504            Error::Type { error, .. } => error.help(),
505            Error::MissingManifest { .. } => Some(Box::new(
506                "Try running `aiken new <REPOSITORY/PROJECT>` to initialise a project with an example manifest.",
507            )),
508            Error::NoDefaultEnvironment => Some(Box::new(
509                "Environment module names are free, but there must be at least one named 'default.ak'.",
510            )),
511            Error::TomlLoading { help, .. } => Some(Box::new(help)),
512
513            Error::ModuleNotFound { known_modules, .. } => Some(Box::new(format!(
514                "I know about the following modules:\n{}",
515                known_modules
516                    .iter()
517                    .map(|s| format!("─▶ {}", s.if_supports_color(Stdout, |s| s.purple())))
518                    .collect::<Vec<_>>()
519                    .join("\n")
520            ))),
521            Error::UnknownPackageVersion { .. } => Some(Box::new(
522                "Perhaps, double-check the package repository and version?",
523            )),
524            Error::UnableToResolvePackage { .. } => Some(Box::new(
525                "The network is unavailable and the package isn't in the local cache either. Try connecting to the Internet so I can look it up?",
526            )),
527            Error::Json(error) => Some(Box::new(format!("{error}"))),
528            Error::MalformedStakeAddress { error } => Some(Box::new(format!(
529                "A stake address must be provided either as a base16-encoded string, or as a bech32-encoded string with the 'stake' or 'stake_test' prefix.{hint}",
530                hint = match error {
531                    Some(error) => format!("\n\nHere's the error I encountered: {error}"),
532                    None => String::new(),
533                }
534            ))),
535            Error::ScriptOverrideNotFound { known_scripts, .. } => Some(Box::new(format!(
536                "These are all the scripts found in your blueprint:\n\n{}",
537                known_scripts
538                    .iter()
539                    .map(|s| format!("✓ {s}"))
540                    .collect::<Vec<_>>()
541                    .join("\n"),
542            ))),
543            Error::ScriptOverrideArgumentParseError { error, .. } => error.help(),
544            Error::Module(e) => e.help(),
545            Error::StandardIo(_)
546            | Error::Format { .. }
547            | Error::TestFailure { .. }
548            | Error::Http(_)
549            | Error::ZipExtract(_)
550            | Error::JoinError(_)
551            | Error::ExportNotFound { .. } => None,
552        }
553    }
554
555    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
556        match self {
557            Error::Blueprint(e) => e.labels(),
558            Error::Parse { error, .. } => error.labels(),
559            Error::Type { error, .. } => error.labels(),
560            Error::TomlLoading { location, .. } => {
561                if let Some(location) = location {
562                    Some(Box::new(
563                        vec![LabeledSpan::new_with_span(None, *location)].into_iter(),
564                    ))
565                } else {
566                    None
567                }
568            }
569            Error::DuplicateModule { .. }
570            | Error::FileIo { .. }
571            | Error::ImportCycle { .. }
572            | Error::ExportNotFound { .. }
573            | Error::StandardIo(_)
574            | Error::MissingManifest { .. }
575            | Error::Format { .. }
576            | Error::TestFailure { .. }
577            | Error::Http(_)
578            | Error::ZipExtract(_)
579            | Error::JoinError(_)
580            | Error::UnknownPackageVersion { .. }
581            | Error::UnableToResolvePackage { .. }
582            | Error::Json { .. }
583            | Error::MalformedStakeAddress { .. }
584            | Error::NoDefaultEnvironment
585            | Error::ModuleNotFound { .. }
586            | Error::ScriptOverrideNotFound { .. }
587            | Error::ScriptOverrideArgumentParseError { .. } => None,
588
589            Error::Module(e) => e.labels(),
590        }
591    }
592
593    fn source_code(&self) -> Option<&dyn SourceCode> {
594        match self {
595            Error::Blueprint(e) => e.source_code(),
596            Error::Parse { named, .. } => Some(named.as_ref()),
597            Error::Type { named, .. } => Some(named),
598            Error::TomlLoading { named, .. } => Some(named.as_ref()),
599            Error::DuplicateModule { .. }
600            | Error::FileIo { .. }
601            | Error::ImportCycle { .. }
602            | Error::ModuleNotFound { .. }
603            | Error::ExportNotFound { .. }
604            | Error::NoDefaultEnvironment
605            | Error::StandardIo(_)
606            | Error::MissingManifest { .. }
607            | Error::Format { .. }
608            | Error::TestFailure { .. }
609            | Error::Http(_)
610            | Error::ZipExtract(_)
611            | Error::JoinError(_)
612            | Error::UnknownPackageVersion { .. }
613            | Error::UnableToResolvePackage { .. }
614            | Error::Json { .. }
615            | Error::MalformedStakeAddress { .. }
616            | Error::ScriptOverrideNotFound { .. }
617            | Error::ScriptOverrideArgumentParseError { .. } => None,
618            Error::Module(e) => e.source_code(),
619        }
620    }
621
622    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
623        match self {
624            Error::Blueprint(e) => e.url(),
625            Error::Type { error, .. } => error.url(),
626            Error::DuplicateModule { .. }
627            | Error::FileIo { .. }
628            | Error::ImportCycle { .. }
629            | Error::ModuleNotFound { .. }
630            | Error::ExportNotFound { .. }
631            | Error::Parse { .. }
632            | Error::StandardIo(_)
633            | Error::MissingManifest { .. }
634            | Error::TomlLoading { .. }
635            | Error::Format { .. }
636            | Error::TestFailure { .. }
637            | Error::Http { .. }
638            | Error::ZipExtract { .. }
639            | Error::JoinError { .. }
640            | Error::UnknownPackageVersion { .. }
641            | Error::UnableToResolvePackage { .. }
642            | Error::Json { .. }
643            | Error::MalformedStakeAddress { .. }
644            | Error::NoDefaultEnvironment
645            | Error::ScriptOverrideNotFound { .. }
646            | Error::ScriptOverrideArgumentParseError { .. } => None,
647
648            Error::Module(e) => e.url(),
649        }
650    }
651
652    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
653        match self {
654            Error::Blueprint(e) => e.related(),
655            Error::Type { error, .. } => error.related(),
656            Error::DuplicateModule { .. }
657            | Error::FileIo { .. }
658            | Error::ModuleNotFound { .. }
659            | Error::ExportNotFound { .. }
660            | Error::ImportCycle { .. }
661            | Error::Parse { .. }
662            | Error::StandardIo(_)
663            | Error::NoDefaultEnvironment
664            | Error::MissingManifest { .. }
665            | Error::TomlLoading { .. }
666            | Error::Format { .. }
667            | Error::TestFailure { .. }
668            | Error::Http { .. }
669            | Error::ZipExtract { .. }
670            | Error::JoinError { .. }
671            | Error::UnknownPackageVersion { .. }
672            | Error::UnableToResolvePackage { .. }
673            | Error::Json { .. }
674            | Error::MalformedStakeAddress { .. }
675            | Error::ScriptOverrideNotFound { .. }
676            | Error::ScriptOverrideArgumentParseError { .. } => None,
677            Error::Module(e) => e.related(),
678        }
679    }
680}
681
682#[derive(thiserror::Error)]
683#[allow(clippy::large_enum_variant)]
684pub enum Warning {
685    #[error("You do not have any validators to build!")]
686    NoValidators,
687    #[error("{}", warning)]
688    Type {
689        path: PathBuf,
690        src: String,
691        named: NamedSource<String>,
692        #[source]
693        warning: tipo::error::Warning,
694    },
695    #[error("{name} is already a dependency.")]
696    DependencyAlreadyExists { name: PackageName },
697    #[error("Ignoring file with invalid module name at: {path:?}")]
698    InvalidModuleName { path: PathBuf },
699    #[error("aiken.toml demands compiler version {demanded}, but you are using {current}.")]
700    CompilerVersionMismatch { demanded: String, current: String },
701    #[error("No configuration found for environment {env}.")]
702    NoConfigurationForEnv { env: String },
703    #[error("Suspicious test filter (-m) yielding no test scenarios.")]
704    SuspiciousTestMatch { test: String },
705}
706
707impl ExtraData for Warning {
708    fn extra_data(&self) -> Option<String> {
709        match self {
710            Warning::NoValidators
711            | Warning::DependencyAlreadyExists { .. }
712            | Warning::InvalidModuleName { .. }
713            | Warning::CompilerVersionMismatch { .. }
714            | Warning::NoConfigurationForEnv { .. }
715            | Warning::SuspiciousTestMatch { .. } => None,
716            Warning::Type { warning, .. } => warning.extra_data(),
717        }
718    }
719}
720
721impl GetSource for Warning {
722    fn path(&self) -> Option<PathBuf> {
723        match self {
724            Warning::InvalidModuleName { path } | Warning::Type { path, .. } => Some(path.clone()),
725            Warning::NoValidators
726            | Warning::DependencyAlreadyExists { .. }
727            | Warning::NoConfigurationForEnv { .. }
728            | Warning::CompilerVersionMismatch { .. }
729            | Warning::SuspiciousTestMatch { .. } => None,
730        }
731    }
732
733    fn src(&self) -> Option<String> {
734        match self {
735            Warning::Type { src, .. } => Some(src.clone()),
736            Warning::NoValidators
737            | Warning::InvalidModuleName { .. }
738            | Warning::DependencyAlreadyExists { .. }
739            | Warning::NoConfigurationForEnv { .. }
740            | Warning::CompilerVersionMismatch { .. }
741            | Warning::SuspiciousTestMatch { .. } => None,
742        }
743    }
744}
745
746impl Diagnostic for Warning {
747    fn severity(&self) -> Option<miette::Severity> {
748        Some(miette::Severity::Warning)
749    }
750
751    fn source_code(&self) -> Option<&dyn SourceCode> {
752        match self {
753            Warning::Type { named, .. } => Some(named),
754            Warning::NoValidators
755            | Warning::InvalidModuleName { .. }
756            | Warning::NoConfigurationForEnv { .. }
757            | Warning::DependencyAlreadyExists { .. }
758            | Warning::CompilerVersionMismatch { .. }
759            | Warning::SuspiciousTestMatch { .. } => None,
760        }
761    }
762
763    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
764        match self {
765            Warning::Type { warning, .. } => warning.labels(),
766            Warning::InvalidModuleName { .. }
767            | Warning::NoValidators
768            | Warning::DependencyAlreadyExists { .. }
769            | Warning::NoConfigurationForEnv { .. }
770            | Warning::CompilerVersionMismatch { .. }
771            | Warning::SuspiciousTestMatch { .. } => None,
772        }
773    }
774
775    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
776        match self {
777            Warning::Type { warning, .. } => Some(Box::new(format!(
778                "aiken::check{}",
779                warning.code().map(|s| format!("::{s}")).unwrap_or_default()
780            ))),
781            Warning::NoValidators => Some(Box::new("aiken::check")),
782            Warning::InvalidModuleName { .. } => Some(Box::new("aiken::project::module_name")),
783            Warning::CompilerVersionMismatch { .. } => {
784                Some(Box::new("aiken::project::compiler_version_mismatch"))
785            }
786            Warning::DependencyAlreadyExists { .. } => {
787                Some(Box::new("aiken::packages::already_exists"))
788            }
789            Warning::NoConfigurationForEnv { .. } => {
790                Some(Box::new("aiken::project::config::missing::env"))
791            }
792            Warning::SuspiciousTestMatch { .. } => Some(Box::new("aiken::check::suspicious_match")),
793        }
794    }
795
796    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
797        match self {
798            Warning::Type { warning, .. } => warning.help(),
799            Warning::NoValidators => None,
800            Warning::CompilerVersionMismatch { demanded, .. } => Some(Box::new(format!(
801                "You may want to switch to {}",
802                demanded.if_supports_color(Stdout, |s| s.purple())
803            ))),
804            Warning::InvalidModuleName { .. } => Some(Box::new(
805                "Module names are lowercase, (ascii) alpha-numeric and may contain dashes or underscores.",
806            )),
807            Warning::DependencyAlreadyExists { .. } => Some(Box::new(
808                "If you need to change the version, try 'aiken packages upgrade' instead.",
809            )),
810            Warning::NoConfigurationForEnv { .. } => Some(Box::new(
811                "When configuration keys are missing for a target environment, no 'config' module will be created. This may lead to issues down the line.",
812            )),
813            Warning::SuspiciousTestMatch { test } => Some(Box::new(format!(
814                "Did you mean to match all tests within a specific module? Like so:\n\n╰─▶ {}",
815                format!("-m \"{test}.{{..}}\"").if_supports_color(Stderr, |s| s.bold()),
816            ))),
817        }
818    }
819}
820
821impl Warning {
822    pub fn from_type_warning(warning: tipo::error::Warning, path: PathBuf, src: String) -> Warning {
823        Warning::Type {
824            path: path.clone(),
825            warning,
826            src: src.clone(),
827            named: NamedSource::new(path.display().to_string(), src),
828        }
829    }
830
831    pub fn report(&self) {
832        eprintln!("{self:?}")
833    }
834}
835
836impl Debug for Warning {
837    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838        default_miette_handler(1)
839            .debug(
840                &DisplayWarning {
841                    title: &self.to_string(),
842                    source_code: self.source_code(),
843                    labels: self.labels().map(|ls| ls.collect()),
844                    help: self.help().map(|s| s.to_string()),
845                },
846                f,
847            )
848            // Ignore error to prevent format! panics. This can happen if span points at some
849            // inaccessible location, for example by calling `report_error()` with wrong working set.
850            .or(Ok(()))
851    }
852}
853
854#[derive(thiserror::Error)]
855#[error("{}", title.if_supports_color(Stderr, |s| s.yellow()))]
856struct DisplayWarning<'a> {
857    title: &'a str,
858    source_code: Option<&'a dyn miette::SourceCode>,
859    labels: Option<Vec<LabeledSpan>>,
860    help: Option<String>,
861}
862
863impl Diagnostic for DisplayWarning<'_> {
864    fn severity(&self) -> Option<miette::Severity> {
865        Some(miette::Severity::Warning)
866    }
867
868    fn source_code(&self) -> Option<&dyn SourceCode> {
869        self.source_code
870    }
871
872    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
873        self.labels
874            .as_ref()
875            .map(|ls| ls.iter().cloned())
876            .map(Box::new)
877            .map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
878    }
879
880    fn code<'b>(&'b self) -> Option<Box<dyn Display + 'b>> {
881        None
882    }
883
884    fn help<'b>(&'b self) -> Option<Box<dyn Display + 'b>> {
885        self.help
886            .as_ref()
887            .map(Box::new)
888            .map(|b| b as Box<dyn Display + 'b>)
889    }
890}
891
892impl Debug for DisplayWarning<'_> {
893    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894        unreachable!("Display warning are never shown directly.");
895    }
896}
897
898#[derive(Debug, PartialEq, Eq)]
899pub struct Unformatted {
900    pub source: PathBuf,
901    pub destination: PathBuf,
902    pub input: String,
903    pub output: String,
904}
905
906fn default_miette_handler(context_lines: usize) -> MietteHandler {
907    MietteHandlerOpts::new()
908        // For better support of terminal themes use the ANSI coloring
909        .rgb_colors(RgbColors::Never)
910        // If ansi support is disabled in the config disable the eye-candy
911        .unicode(true)
912        .terminal_links(true)
913        .context_lines(context_lines)
914        .build()
915}