#![warn(rust_2024_compatibility, clippy::all)]
mod cargo_toml;
mod counting;
mod structure;
mod visibility;
use dictator_decree_abi::{BoxDecree, Decree, Diagnostics};
use dictator_supreme::SupremeConfig;
pub use cargo_toml::lint_cargo_toml;
#[derive(Debug, Clone)]
pub struct RustConfig {
pub max_lines: usize,
pub min_edition: Option<String>,
pub min_rust_version: Option<String>,
pub ignore_comments: bool,
}
impl Default for RustConfig {
fn default() -> Self {
Self {
max_lines: 400,
min_edition: None,
min_rust_version: None,
ignore_comments: false,
}
}
}
#[must_use]
pub fn lint_source(source: &str) -> Diagnostics {
lint_source_with_configs(source, &RustConfig::default(), &SupremeConfig::default())
}
#[must_use]
pub fn lint_source_with_config(source: &str, config: &RustConfig) -> Diagnostics {
let mut diags = Diagnostics::new();
counting::check_file_line_count(source, config.max_lines, &mut diags);
visibility::check_visibility_ordering(source, &mut diags);
diags
}
#[must_use]
pub fn lint_source_with_configs(
source: &str,
rust_config: &RustConfig,
supreme_config: &SupremeConfig,
) -> Diagnostics {
let mut diags = Diagnostics::new();
let supreme_diags = dictator_supreme::lint_source_with_owner(source, supreme_config, "rust");
if rust_config.ignore_comments {
let lines: Vec<&str> = source.lines().collect();
diags.extend(supreme_diags.into_iter().filter(|d| {
if d.rule == "rust/line-too-long" {
let line_idx = source[..d.span.start].matches('\n').count();
!lines
.get(line_idx)
.is_some_and(|line| line.trim_start().starts_with("//"))
} else {
true
}
}));
} else {
diags.extend(supreme_diags);
}
diags.extend(lint_source_with_config(source, rust_config));
diags
}
#[derive(Default)]
pub struct RustDecree {
config: RustConfig,
supreme: SupremeConfig,
}
impl RustDecree {
#[must_use]
pub const fn new(config: RustConfig, supreme: SupremeConfig) -> Self {
Self { config, supreme }
}
}
impl Decree for RustDecree {
fn name(&self) -> &'static str {
"rust"
}
fn lint(&self, path: &str, source: &str) -> Diagnostics {
let filename = std::path::Path::new(path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or("");
if filename == "Cargo.toml" {
return cargo_toml::lint_cargo_toml(source, &self.config);
}
let mut diags = lint_source_with_configs(source, &self.config, &self.supreme);
structure::check_mod_rs_structure(path, &mut diags);
diags
}
fn metadata(&self) -> dictator_decree_abi::DecreeMetadata {
dictator_decree_abi::DecreeMetadata {
abi_version: dictator_decree_abi::ABI_VERSION.to_string(),
decree_version: env!("CARGO_PKG_VERSION").to_string(),
description: "Rust structural rules".to_string(),
dectauthors: Some(env!("CARGO_PKG_AUTHORS").to_string()),
supported_extensions: vec!["rs".to_string()],
supported_filenames: vec![
"Cargo.toml".to_string(),
"build.rs".to_string(),
"rust-toolchain".to_string(),
"rust-toolchain.toml".to_string(),
".rustfmt.toml".to_string(),
"rustfmt.toml".to_string(),
"clippy.toml".to_string(),
".clippy.toml".to_string(),
],
skip_filenames: vec!["Cargo.lock".to_string()],
capabilities: vec![dictator_decree_abi::Capability::Lint],
}
}
}
#[must_use]
pub fn init_decree() -> BoxDecree {
Box::new(RustDecree::default())
}
#[must_use]
pub fn init_decree_with_config(config: RustConfig) -> BoxDecree {
Box::new(RustDecree::new(config, SupremeConfig::default()))
}
#[must_use]
pub fn init_decree_with_configs(config: RustConfig, supreme: SupremeConfig) -> BoxDecree {
Box::new(RustDecree::new(config, supreme))
}
#[must_use]
pub fn config_from_decree_settings(settings: &dictator_core::DecreeSettings) -> RustConfig {
RustConfig {
max_lines: settings.max_lines.unwrap_or(400),
min_edition: settings.min_edition.clone(),
min_rust_version: settings.min_rust_version.clone(),
ignore_comments: settings.ignore_comments.unwrap_or(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ignores_long_comment_lines_when_configured() {
let long_comment = format!("// {}\n", "x".repeat(150));
let src = format!("fn main() {{\n{long_comment}}}\n");
let config = RustConfig {
ignore_comments: true,
..Default::default()
};
let supreme = SupremeConfig {
max_line_length: Some(120),
..Default::default()
};
let diags = lint_source_with_configs(&src, &config, &supreme);
assert!(!diags.iter().any(|d| d.rule == "rust/line-too-long"));
}
#[test]
fn detects_long_comment_lines_when_not_configured() {
let long_comment = format!("// {}\n", "x".repeat(150));
let src = format!("fn main() {{\n{long_comment}}}\n");
let config = RustConfig::default(); let supreme = SupremeConfig {
max_line_length: Some(120),
..Default::default()
};
let diags = lint_source_with_configs(&src, &config, &supreme);
assert!(diags.iter().any(|d| d.rule == "rust/line-too-long"));
}
#[test]
fn still_detects_long_code_lines_with_ignore_comments() {
let long_code = format!(" let x = \"{}\";\n", "a".repeat(150));
let src = format!("fn main() {{\n{long_code}}}\n");
let config = RustConfig {
ignore_comments: true,
..Default::default()
};
let supreme = SupremeConfig {
max_line_length: Some(120),
..Default::default()
};
let diags = lint_source_with_configs(&src, &config, &supreme);
assert!(diags.iter().any(|d| d.rule == "rust/line-too-long"));
}
}