use dictator_decree_abi::{Diagnostic, Diagnostics, Span};
use crate::RustConfig;
#[must_use]
pub fn lint_cargo_toml(source: &str, config: &RustConfig) -> Diagnostics {
let mut diags = Diagnostics::new();
if let Some(ref min_edition) = config.min_edition {
check_cargo_edition(source, min_edition, &mut diags);
}
if let Some(ref min_rust_version) = config.min_rust_version {
check_rust_version(source, min_rust_version, &mut diags);
}
diags
}
fn check_cargo_edition(source: &str, min_edition: &str, diags: &mut Diagnostics) {
let mut found_edition: Option<(String, usize, usize)> = None;
for (line_idx, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("edition.workspace") {
return; }
if trimmed.starts_with("edition") && !trimmed.contains(".workspace") {
if let Some(eq_pos) = trimmed.find('=') {
let value_part = trimmed[eq_pos + 1..].trim();
let edition = value_part.trim_matches('"').trim_matches('\'').trim();
let line_start: usize = source.lines().take(line_idx).map(|l| l.len() + 1).sum();
found_edition = Some((edition.to_string(), line_start, line_start + line.len()));
break;
}
}
}
match found_edition {
Some((edition, start, end)) => {
if edition_ord(&edition) < edition_ord(min_edition) {
diags.push(Diagnostic {
rule: "rust/fossil-edition".to_string(),
message: format!(
"edition {edition} is fossilized, the Dictator demands {min_edition}"
),
enforced: true,
span: Span::new(start, end),
});
}
}
None => {
diags.push(Diagnostic {
rule: "rust/missing-edition".to_string(),
message: format!("no edition declared, the Dictator demands {min_edition}"),
enforced: false,
span: Span::new(0, source.len().min(50)),
});
}
}
}
fn edition_ord(edition: &str) -> u32 {
match edition {
"2015" => 1,
"2018" => 2,
"2021" => 3,
"2024" => 4,
_ => 0,
}
}
fn check_rust_version(source: &str, min_version: &str, diags: &mut Diagnostics) {
let mut found_version: Option<(String, usize, usize)> = None;
for (line_idx, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("rust-version.workspace") {
return; }
if trimmed.starts_with("rust-version")
&& !trimmed.contains(".workspace")
&& let Some(eq_pos) = trimmed.find('=')
{
let value_part = trimmed[eq_pos + 1..].trim();
let version = value_part.trim_matches('"').trim_matches('\'').trim();
let line_start: usize = source.lines().take(line_idx).map(|l| l.len() + 1).sum();
found_version = Some((version.to_string(), line_start, line_start + line.len()));
break;
}
}
match found_version {
Some((version, start, end)) => {
if version_cmp(&version, min_version) == std::cmp::Ordering::Less {
diags.push(Diagnostic {
rule: "rust/fossil-rust-version".to_string(),
message: format!(
"rust-version {version} is prehistoric, the Dictator demands {min_version}+"
),
enforced: true,
span: Span::new(start, end),
});
}
}
None => {
diags.push(Diagnostic {
rule: "rust/missing-rust-version".to_string(),
message: format!("no rust-version declared, the Dictator demands {min_version}+"),
enforced: false,
span: Span::new(0, source.len().min(50)),
});
}
}
}
fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
let parse = |v: &str| -> Vec<u32> { v.split('.').filter_map(|p| p.parse().ok()).collect() };
let a_parts = parse(a);
let b_parts = parse(b);
for i in 0..3 {
let a_val = a_parts.get(i).copied().unwrap_or(0);
let b_val = b_parts.get(i).copied().unwrap_or(0);
match a_val.cmp(&b_val) {
std::cmp::Ordering::Equal => {}
other => return other,
}
}
std::cmp::Ordering::Equal
}
#[cfg(test)]
mod tests;