1use anyhow::Result;
6
7use crate::args::Args;
8use crate::config::Config;
9use crate::header_source::HeaderSource;
10use crate::json_printer::JsonPrinter;
11use crate::manifest_resolver::ManifestResolver;
12use crate::offence::Offence;
13use crate::offence_threshold::OffenceThreshold;
14use crate::output_format::OutputFormat;
15use crate::report_printer::ReportPrinter;
16use crate::rule_registry::RuleRegistry;
17use crate::rule_selection::RuleSelection;
18use crate::rules::header_rule::HeaderRule;
19use crate::run_outcome::RunOutcome;
20use crate::source_reader::SourceReader;
21use crate::source_walker::SourceWalker;
22
23pub struct Runner;
39
40impl Runner {
41 pub fn run(args: Args) -> Result<RunOutcome> {
42 let config = Self::config_from(args)?;
43 Self::validate_selection(&config)?;
44 let registry = RuleRegistry::from_config(&config);
45 if registry.is_empty() {
46 return Err(anyhow::anyhow!(
47 "no rules are configured, so nothing would be checked -- pass --header-file to \
48 enable the header rule"
49 ));
50 }
51
52 let roots = ManifestResolver::package_roots(&config)?;
53 let mut offences = Vec::new();
54 let mut files_scanned = 0usize;
55
56 for root in &roots {
57 let mut files = Vec::new();
62 for path in SourceWalker::walk(root) {
63 files_scanned += 1;
64 match SourceReader::read(root, &path) {
65 Ok(file) => files.push(file),
66 Err(offence) => offences.push(*offence),
67 }
68 }
69 for file in &files {
70 offences.extend(registry.check(file));
71 }
72 offences.extend(registry.check_workspace(&files));
73 }
74
75 offences.sort_by(|left, right| left.sort_key().cmp(&right.sort_key()));
80 Self::report(&config, ®istry, files_scanned, &offences);
81 Ok(RunOutcome::of(offences.len()))
82 }
83
84 fn validate_selection(config: &Config) -> Result<()> {
89 let known = RuleRegistry::known_names();
90 let unknown = config.selection.unknown_in(&known);
91 if !unknown.is_empty() {
92 return Err(anyhow::anyhow!(
93 "unknown rule name(s): {} -- the rules are: {}",
94 unknown.join(", "),
95 known.join(", ")
96 ));
97 }
98 if config.selection.selects_explicitly(HeaderRule::NAME)
99 && config.expected_header.is_empty()
100 {
101 return Err(anyhow::anyhow!(
102 "--rule {} needs --header-file, otherwise the run would apply no rules at all",
103 HeaderRule::NAME
104 ));
105 }
106 Ok(())
107 }
108
109 fn config_from(args: Args) -> Result<Config> {
110 let expected_header = match &args.header_file {
111 Some(path) => HeaderSource::read(path)?,
112 None => Vec::new(),
113 };
114 Ok(Config {
115 manifest_path: args.manifest_path,
116 packages: args.packages,
117 expected_header,
118 format: args.format,
119 offence_threshold: OffenceThreshold::new(args.offence_threshold),
120 selection: RuleSelection::new(args.rules, args.skipped_rules),
121 })
122 }
123
124 fn report(
125 config: &Config,
126 registry: &RuleRegistry,
127 files_scanned: usize,
128 offences: &[Offence],
129 ) {
130 let threshold = config.offence_threshold;
131 let applied = Self::owned(®istry.names());
132 let skipped = Self::owned(&RuleRegistry::skipped_names(&config.selection));
133 let unconfigured = Self::owned(®istry.unconfigured_names(config));
134 match config.format {
135 OutputFormat::Text => ReportPrinter::new(files_scanned)
136 .with_threshold(threshold)
137 .with_rules(applied.clone(), skipped.clone(), unconfigured.clone())
138 .print(offences),
139 OutputFormat::Json => JsonPrinter::new(files_scanned)
140 .with_threshold(threshold)
141 .with_rules(applied, skipped, unconfigured)
142 .print(offences),
143 }
144 }
145
146 fn owned(names: &[&str]) -> Vec<String> {
147 names.iter().map(|name| (*name).to_string()).collect()
148 }
149}