1mod config;
62mod init;
63mod report;
64mod rules;
65mod workspace;
66
67use std::fmt;
68use std::path::{Path, PathBuf};
69
70pub use config::{Config, Language, Level};
71pub use init::{InitReport, init, recommended_clippy, recommended_lints};
72pub use report::{render, render_json};
73pub use rules::Lint;
74pub use workspace::{Package, packages};
75
76#[derive(Clone, Debug, PartialEq)]
78pub struct Diagnostic {
79 pub lint: Lint,
81 pub level: Level,
83 pub file: PathBuf,
85 pub line: usize,
87 pub column: usize,
89 pub end_column: usize,
91 pub message: String,
93 pub help: String,
95 pub snippet: String,
97}
98
99#[derive(Debug)]
101#[non_exhaustive]
102pub enum Error {
103 Io(PathBuf, std::io::Error),
105 Parse(PathBuf, syn::Error),
107 Config(String),
109 Cargo(String),
111}
112
113impl fmt::Display for Error {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Error::Io(path, error) => write!(f, "could not read {}: {error}", path.display()),
117 Error::Parse(path, error) => {
118 let at = error.span().start();
119 write!(f, "{}:{}:{}: {error}", path.display(), at.line, at.column + 1)
120 }
121 Error::Config(message) => write!(f, "invalid ravenrs-lint configuration: {message}"),
122 Error::Cargo(message) => write!(f, "cargo metadata: {message}"),
123 }
124 }
125}
126
127impl std::error::Error for Error {}
128
129pub fn check_source(file: &Path, source: &str, config: &Config) -> Result<Vec<Diagnostic>, Error> {
131 let syntax = syn::parse_file(source).map_err(|error| Error::Parse(file.to_owned(), error))?;
132 let mut found = rules::check(&syntax, config, source.lines().count());
133 found.extend(rules::check_allow_comments(source));
134 let allowed = rules::Allowances::read(source);
135 let lines: Vec<&str> = source.lines().collect();
136 let mut diagnostics: Vec<Diagnostic> = found
137 .into_iter()
138 .filter(|finding| !allowed.covers(finding.lint, finding.line))
139 .filter_map(|finding| {
140 let level = config.level(finding.lint);
141 (level != Level::Allow).then(|| Diagnostic {
142 lint: finding.lint,
143 level,
144 file: file.to_owned(),
145 line: finding.line,
146 column: finding.column,
147 end_column: finding.end_column,
148 message: finding.message,
149 help: finding.help,
150 snippet: lines.get(finding.line.saturating_sub(1)).copied().unwrap_or_default().to_owned(),
151 })
152 })
153 .collect();
154 diagnostics.sort_by_key(|d| (d.line, d.column));
155 Ok(diagnostics)
156}
157
158pub fn check_dir(dir: &Path, base: &Path, config: &Config) -> Result<Vec<Diagnostic>, Error> {
161 let mut files = Vec::new();
162 collect_rust_files(dir, &mut files)?;
163 files.sort();
164 let mut diagnostics = Vec::new();
165 for path in files {
166 let source = std::fs::read_to_string(&path).map_err(|error| Error::Io(path.clone(), error))?;
167 let shown = path.strip_prefix(base).unwrap_or(&path);
168 diagnostics.extend(check_source(shown, &source, config)?);
169 }
170 Ok(diagnostics)
171}
172
173fn collect_rust_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), Error> {
174 let entries = std::fs::read_dir(dir).map_err(|error| Error::Io(dir.to_owned(), error))?;
175 for entry in entries {
176 let path = entry.map_err(|error| Error::Io(dir.to_owned(), error))?.path();
177 if path.is_dir() {
178 collect_rust_files(&path, files)?;
179 } else if path.extension().is_some_and(|ext| ext == "rs") {
180 files.push(path);
181 }
182 }
183 Ok(())
184}
185
186pub fn check_package(package: &Package) -> Result<Vec<Diagnostic>, Error> {
188 let mut diagnostics = Vec::new();
189 for dir in &package.source_dirs {
190 diagnostics.extend(check_dir(dir, &package.root, &package.config)?);
191 }
192 Ok(diagnostics)
193}
194
195#[allow(clippy::panic)]
202pub fn assert_clean() {
203 let Some(dir) = std::env::var_os("CARGO_MANIFEST_DIR") else {
204 panic!("ravenrs_lint::assert_clean runs from `cargo test`, which sets CARGO_MANIFEST_DIR");
205 };
206 let manifest = PathBuf::from(dir).join("Cargo.toml");
207 let result = packages(&manifest).and_then(|found| {
208 let same = |a: &Path, b: &Path| a == b || a.canonicalize().ok() == b.canonicalize().ok();
209 let package = found
210 .into_iter()
211 .find(|p| same(&p.manifest, &manifest))
212 .ok_or_else(|| Error::Cargo(format!("{} is not in its workspace", manifest.display())))?;
213 check_package(&package)
214 });
215 match result {
216 Ok(diagnostics) if diagnostics.is_empty() => {}
217 Ok(diagnostics) => panic!("\n{}", render(&diagnostics, true)),
218 Err(error) => panic!("{error}"),
219 }
220}