Skip to main content

cargo_ravenrs_lint/
cargo-ravenrs-lint.rs

1//! `cargo ravenrs-lint`: checks the packages of a workspace that depend on RavenRS.
2
3use std::path::PathBuf;
4use std::process::{Command, ExitCode};
5
6use ravenrs_lint::{Language, Level, Lint};
7
8const USAGE: &str = "\
9Checks applications built with RavenRS.
10
11Usage: cargo ravenrs-lint [options]
12
13Options:
14  --manifest-path <path>  The Cargo.toml to start from (default: ./Cargo.toml)
15  -p, --package <name>    Check only this package
16  --message-format json   One JSON object per diagnostic
17  --gate                  Run cargo fmt --check, cargo clippy -D warnings, these rules and cargo test,
18                          stopping at the first that fails. Warnings fail too.
19  --init                  Write the strict lints, clippy.toml, the test and the agents' instructions
20  --language <en|pt-BR>   The interface's language, over the configuration (default: en)
21  --list                  List the rules
22  --print-lints           Print the [lints] tables --init writes
23  --print-clippy          Print the clippy.toml --init writes
24  -h, --help              Show this
25";
26
27struct Options {
28    manifest: PathBuf,
29    package: Option<String>,
30    json: bool,
31    gate: bool,
32    init: bool,
33    language: Option<Language>,
34}
35
36fn main() -> ExitCode {
37    let mut args: Vec<String> = std::env::args().skip(1).collect();
38    // Cargo runs `cargo-ravenrs-lint ravenrs-lint ...`.
39    if args.first().is_some_and(|a| a == "ravenrs-lint") {
40        args.remove(0);
41    }
42    match run(&args) {
43        Ok(code) => code,
44        Err(message) => {
45            eprintln!("error: {message}");
46            ExitCode::from(2)
47        }
48    }
49}
50
51fn run(args: &[String]) -> Result<ExitCode, String> {
52    let mut options = Options {
53        manifest: PathBuf::from("Cargo.toml"),
54        package: None,
55        json: false,
56        gate: false,
57        init: false,
58        language: None,
59    };
60    let mut iter = args.iter();
61    while let Some(arg) = iter.next() {
62        let mut value = |name: &str| iter.next().cloned().ok_or_else(|| format!("{name} needs a value"));
63        match arg.as_str() {
64            "-h" | "--help" => {
65                print!("{USAGE}");
66                return Ok(ExitCode::SUCCESS);
67            }
68            "--list" => {
69                for lint in Lint::ALL {
70                    println!("ravenrs::{:<28} {}", lint.name(), lint.summary());
71                }
72                return Ok(ExitCode::SUCCESS);
73            }
74            "--print-lints" => {
75                print!("{}", ravenrs_lint::recommended_lints());
76                return Ok(ExitCode::SUCCESS);
77            }
78            "--print-clippy" => {
79                print!("{}", ravenrs_lint::recommended_clippy());
80                return Ok(ExitCode::SUCCESS);
81            }
82            "--manifest-path" => options.manifest = PathBuf::from(value("--manifest-path")?),
83            "-p" | "--package" => options.package = Some(value("--package")?),
84            "--message-format" => options.json = value("--message-format")? == "json",
85            "--gate" => options.gate = true,
86            "--init" => options.init = true,
87            "--language" => {
88                options.language = Some(match value("--language")?.as_str() {
89                    "en" => Language::English,
90                    "pt-BR" | "pt" => Language::PortugueseBrazil,
91                    other => return Err(format!("unknown language `{other}`; use en or pt-BR")),
92                });
93            }
94            other => return Err(format!("unknown option `{other}`\n\n{USAGE}")),
95        }
96    }
97
98    if options.init {
99        let dir = options.manifest.parent().map(PathBuf::from).unwrap_or_default();
100        let dir = if dir.as_os_str().is_empty() { PathBuf::from(".") } else { dir };
101        let report =
102            ravenrs_lint::init(&dir, options.language.unwrap_or_default()).map_err(|e| e.to_string())?;
103        for path in &report.changed {
104            println!("wrote {}", path.display());
105        }
106        for note in &report.notes {
107            println!("note: {note}");
108        }
109        println!("next: cargo ravenrs-lint --gate");
110        return Ok(ExitCode::SUCCESS);
111    }
112
113    if options.gate {
114        let steps: [&[&str]; 2] =
115            [&["fmt", "--all", "--check"], &["clippy", "--all-targets", "--", "-D", "warnings"]];
116        for step in steps {
117            cargo(&options, step)?;
118        }
119    }
120
121    let packages = ravenrs_lint::packages(&options.manifest).map_err(|e| e.to_string())?;
122    let mut chosen: Vec<_> = packages
123        .into_iter()
124        .filter(|p| match &options.package {
125            Some(name) => &p.name == name,
126            None => p.uses_ravenrs,
127        })
128        .collect();
129    if chosen.is_empty() {
130        return Err(match &options.package {
131            Some(name) => format!("no package `{name}` in this workspace"),
132            None => "no package in this workspace depends on RavenRS".into(),
133        });
134    }
135    let mut diagnostics = Vec::new();
136    let many = chosen.len() > 1;
137    for package in &mut chosen {
138        if let Some(language) = options.language {
139            package.config.language = language;
140        }
141        let found = ravenrs_lint::check_package(package).map_err(|e| e.to_string())?;
142        diagnostics.extend(found.into_iter().map(|mut d| {
143            if many {
144                d.file = PathBuf::from(&package.name).join(&d.file);
145            }
146            d
147        }));
148    }
149    if options.json {
150        print!("{}", ravenrs_lint::render_json(&diagnostics));
151    } else {
152        eprint!("{}", ravenrs_lint::render(&diagnostics, true));
153    }
154    let failed = diagnostics.iter().any(|d| d.level == Level::Deny || options.gate && d.level == Level::Warn);
155    if failed {
156        return Ok(ExitCode::FAILURE);
157    }
158    if options.gate {
159        cargo(&options, &["test", "--all-targets"])?;
160        eprintln!("ravenrs-lint gate: everything passed");
161    }
162    Ok(ExitCode::SUCCESS)
163}
164
165fn cargo(options: &Options, args: &[&str]) -> Result<(), String> {
166    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
167    let mut command = Command::new(cargo);
168    command.arg(args[0]).arg("--manifest-path").arg(&options.manifest);
169    if let Some(name) = &options.package {
170        command.args(["-p", name]);
171    }
172    command.args(&args[1..]);
173    let status = command.status().map_err(|e| e.to_string())?;
174    if status.success() {
175        Ok(())
176    } else {
177        Err(format!("`cargo {}` failed; fix it and run the gate again", args.join(" ")))
178    }
179}