mod encoding;
mod eof;
mod indentation;
mod line_endings;
use std::path::Path;
use anyhow::Result;
use crate::config::{Config, FileSettings, LineEnding};
use crate::filetypes::FileTypeRegistry;
pub use encoding::EncodingAnalyzer;
pub use eof::EofAnalyzer;
pub use indentation::IndentationAnalyzer;
pub use line_endings::LineEndingAnalyzer;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Issue {
MixedLineEndings { lf_count: usize, crlf_count: usize },
WrongLineEnding {
expected: LineEndingStyle,
found: LineEndingStyle,
},
MixedIndentation { tabs: usize, spaces: usize },
WrongIndentation {
expected: IndentationStyle,
found: IndentationStyle,
},
TabsRequired,
TabsForbidden,
NonUtf8Encoding { detected: String },
Utf8Bom,
InvalidUtf8 { positions: Vec<usize> },
MissingFinalNewline,
ExcessiveTrailingBlankLines { count: usize },
TrailingWhitespace { line_count: usize },
SuccessiveBlankLines { occurrences: usize },
}
impl Issue {
pub fn is_fixable(&self) -> bool {
match self {
Issue::TabsForbidden => false,
Issue::NonUtf8Encoding { .. } => false,
Issue::InvalidUtf8 { .. } => false,
Issue::MissingFinalNewline => false,
Issue::TrailingWhitespace { .. } => false,
Issue::SuccessiveBlankLines { .. } => false,
_ => true,
}
}
pub fn description(&self) -> String {
match self {
Issue::MixedLineEndings { lf_count, crlf_count } => {
format!("Mixed line endings ({lf_count} LF, {crlf_count} CRLF)")
}
Issue::WrongLineEnding { expected, found } => {
format!("Wrong line ending (expected {expected}, found {found})")
}
Issue::MixedIndentation { tabs, spaces } => {
format!("Mixed indentation ({tabs} tabs, {spaces} spaces)")
}
Issue::WrongIndentation { expected, found } => {
format!("Wrong indentation (expected {expected:?}, found {found:?})")
}
Issue::TabsRequired => "Tabs required but spaces found".to_string(),
Issue::TabsForbidden => "Tabs forbidden but tabs found in indentation".to_string(),
Issue::NonUtf8Encoding { detected } => {
format!("Non-UTF8 encoding detected: {detected}")
}
Issue::Utf8Bom => "UTF-8 BOM present".to_string(),
Issue::InvalidUtf8 { positions } => {
format!("Invalid UTF-8 sequences at {} positions", positions.len())
}
Issue::MissingFinalNewline => "Missing final newline".to_string(),
Issue::ExcessiveTrailingBlankLines { count } => {
format!("Excessive trailing blank lines ({count})")
}
Issue::TrailingWhitespace { line_count } => {
format!("Trailing whitespace on {line_count} lines")
}
Issue::SuccessiveBlankLines { occurrences } => {
format!("Multiple successive blank lines (found {occurrences} occurrences)")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineEndingStyle {
Lf,
Crlf,
Cr,
Mixed,
None,
}
impl std::fmt::Display for LineEndingStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LineEndingStyle::Lf => write!(f, "LF"),
LineEndingStyle::Crlf => write!(f, "CRLF"),
LineEndingStyle::Cr => write!(f, "CR"),
LineEndingStyle::Mixed => write!(f, "Mixed"),
LineEndingStyle::None => write!(f, "None"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndentationStyle {
Tabs,
Spaces,
Mixed,
None,
}
pub struct Analyzer<'a> {
config: &'a Config,
registry: FileTypeRegistry,
}
impl<'a> Analyzer<'a> {
pub fn new(config: &'a Config) -> Self {
Self {
config,
registry: FileTypeRegistry::new(),
}
}
pub fn analyze(&self, path: &Path) -> Result<Vec<Issue>> {
let content = std::fs::read(path)?;
let settings = self.get_settings(path);
let file_type = self.registry.detect(path);
let mut issues = Vec::new();
let le_analyzer = LineEndingAnalyzer::new(&settings);
issues.extend(le_analyzer.analyze(&content));
let indent_analyzer = IndentationAnalyzer::new(&settings, file_type);
issues.extend(indent_analyzer.analyze(&content));
let encoding_analyzer = EncodingAnalyzer::new(&settings);
issues.extend(encoding_analyzer.analyze(&content));
let eof_analyzer = EofAnalyzer::new(&settings, file_type);
issues.extend(eof_analyzer.analyze(&content));
Ok(issues)
}
fn get_settings(&self, path: &Path) -> FileSettings {
let mut settings = self.config.settings_for_file(path);
if let Some(file_type) = self.registry.detect(path) {
if settings.line_ending == LineEnding::Auto {
settings.line_ending = file_type.default_line_ending;
}
if !self.has_indent_override_for_path(path) {
settings.indent = file_type.default_indent.clone();
}
}
settings
}
fn has_indent_override_for_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for rule in &self.config.rules {
if rule.indent.is_some() && rule.matches(&path_str) {
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issue_description() {
let issue = Issue::MixedLineEndings {
lf_count: 10,
crlf_count: 2,
};
assert!(issue.description().contains("10 LF"));
assert!(issue.description().contains("2 CRLF"));
}
#[test]
fn test_is_fixable_returns_true_for_fixable_issues() {
let fixable_issues = vec![
Issue::MixedLineEndings {
lf_count: 10,
crlf_count: 2,
},
Issue::WrongLineEnding {
expected: LineEndingStyle::Lf,
found: LineEndingStyle::Crlf,
},
Issue::MixedIndentation { tabs: 5, spaces: 3 },
Issue::WrongIndentation {
expected: IndentationStyle::Spaces,
found: IndentationStyle::Tabs,
},
Issue::TabsRequired,
Issue::Utf8Bom,
Issue::ExcessiveTrailingBlankLines { count: 3 },
];
for issue in fixable_issues {
assert!(
issue.is_fixable(),
"{:?} should be fixable",
issue
);
}
}
#[test]
fn test_is_fixable_returns_false_for_unfixable_issues() {
let unfixable_issues = vec![
Issue::TabsForbidden,
Issue::NonUtf8Encoding {
detected: "windows-1252".to_string(),
},
Issue::InvalidUtf8 {
positions: vec![10, 20, 30],
},
Issue::MissingFinalNewline,
Issue::TrailingWhitespace { line_count: 5 },
Issue::SuccessiveBlankLines { occurrences: 3 },
];
for issue in unfixable_issues {
assert!(
!issue.is_fixable(),
"{:?} should NOT be fixable",
issue
);
}
}
}