Skip to main content

dry4rust/cli/
analysis_output.rs

1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6use std::path::Path;
7use std::string::ToString;
8
9use crate::analysis::AnalysisResult;
10use crate::analysis::analyze;
11use crate::analyzer::LanguageAnalyzer;
12use crate::cli::cli_error::CliError;
13use crate::cli::cli_error::CliResult;
14use crate::cli::cli_overrides::CliOverrides;
15use crate::cli::output_format::OutputFormat;
16use crate::config::Config;
17use crate::output::reporter::Reporter;
18use crate::scanner::ScanConfig;
19use crate::scanner::scan_files;
20
21/// Everything one analysis run produces: the configuration it ran under, what
22/// it found, and the reporter that can write it out.
23pub struct AnalysisOutput {
24    pub config: Config,
25    pub result: AnalysisResult,
26    pub reporter: Box<dyn Reporter>,
27}
28
29impl AnalysisOutput {
30    /// Scan files, run the analysis pipeline, and return the output.
31    ///
32    /// Warnings are stored in [`AnalysisOutput::result`] but **not** printed;
33    /// the caller is responsible for writing them to stderr.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`CliError::InvalidConfig`] when the configuration states a
38    /// value the tool cannot run under, [`CliError::NoSourceFiles`] when the
39    /// scan finds nothing to analyse, and [`CliError::Analysis`] when the
40    /// pipeline itself fails.
41    pub fn produce(
42        analyzer: &dyn LanguageAnalyzer,
43        root: &Path,
44        format: OutputFormat,
45        overrides: &CliOverrides,
46    ) -> CliResult<Self> {
47        Self::from_config(analyzer, root, format, Self::configure(root, overrides)?)
48    }
49
50    /// The same run, with the baseline left out of it.
51    ///
52    /// What `baseline` records with: a recording that judged against the
53    /// previous recording would hold only what had been added since, and the
54    /// second run would empty the file.
55    ///
56    /// # Errors
57    ///
58    /// The same as [`AnalysisOutput::produce`], less the baseline.
59    pub fn produce_ignoring_baseline(
60        analyzer: &dyn LanguageAnalyzer,
61        root: &Path,
62        format: OutputFormat,
63        overrides: &CliOverrides,
64    ) -> CliResult<Self> {
65        let mut config = Self::configure(root, overrides)?;
66        let named = config.baseline.take();
67        let output = Self::from_config(analyzer, root, format, config)?;
68        Ok(Self {
69            config: Config {
70                baseline: named,
71                ..output.config
72            },
73            ..output
74        })
75    }
76
77    fn configure(root: &Path, overrides: &CliOverrides) -> CliResult<Config> {
78        Config::load(root)
79            .and_then(|config| overrides.apply_to(config))
80            .map_err(|e| CliError::InvalidConfig(e.to_string()))
81    }
82
83    fn from_config(
84        analyzer: &dyn LanguageAnalyzer,
85        root: &Path,
86        format: OutputFormat,
87        config: Config,
88    ) -> CliResult<Self> {
89        let scan_config = ScanConfig::new(config.root.clone())
90            .with_excludes(config.exclude.clone())
91            .with_extensions(
92                analyzer
93                    .file_extensions()
94                    .iter()
95                    .map(ToString::to_string)
96                    .collect(),
97            );
98        let files = scan_files(&scan_config);
99
100        if files.is_empty() {
101            return Err(CliError::NoSourceFiles(config.root));
102        }
103
104        let result = analyze(analyzer, &files, &config)?;
105        let reporter = format.reporter(Some(root));
106
107        Ok(Self {
108            config,
109            result,
110            reporter,
111        })
112    }
113}