cargo_ravenrs_lint/
cargo-ravenrs-lint.rs1use 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 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}