use std::fmt;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub rule_id: String,
pub severity: Severity,
pub location: Location,
pub section: Option<String>,
pub message: String,
pub weight: u32,
}
impl Diagnostic {
#[must_use]
pub fn new(
rule_id: impl Into<String>,
severity: Severity,
location: Location,
message: impl Into<String>,
) -> Self {
let rule_id = rule_id.into();
let weight = crate::scoring::default_weight_for(&rule_id);
Self {
rule_id,
severity,
location,
section: None,
message: message.into(),
weight,
}
}
#[must_use]
pub fn with_section(mut self, section: impl Into<String>) -> Self {
self.section = Some(section.into());
self
}
#[must_use]
pub const fn with_weight(mut self, weight: u32) -> Self {
self.weight = weight;
self
}
#[must_use]
pub fn category(&self) -> Category {
Category::for_rule(&self.rule_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => f.write_str("info"),
Self::Warning => f.write_str("warning"),
Self::Error => f.write_str("error"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Structure,
Rhythm,
Lexicon,
Syntax,
Readability,
}
impl Category {
pub const ALL: [Self; 5] = [
Self::Structure,
Self::Rhythm,
Self::Lexicon,
Self::Syntax,
Self::Readability,
];
#[must_use]
pub fn for_rule(rule_id: &str) -> Self {
match rule_id.split_once('.').map(|(cat, _)| cat) {
Some("structure") => Self::Structure,
Some("rhythm") => Self::Rhythm,
Some("lexicon") => Self::Lexicon,
Some("syntax") => Self::Syntax,
Some("readability") => Self::Readability,
_ => Self::Syntax,
}
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Structure => f.write_str("structure"),
Self::Rhythm => f.write_str("rhythm"),
Self::Lexicon => f.write_str("lexicon"),
Self::Syntax => f.write_str("syntax"),
Self::Readability => f.write_str("readability"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
pub file: SourceFile,
pub line: u32,
pub column: u32,
pub length: u32,
}
impl Location {
#[must_use]
pub const fn new(file: SourceFile, line: u32, column: u32, length: u32) -> Self {
Self {
file,
line,
column,
length,
}
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "path", rename_all = "lowercase")]
pub enum SourceFile {
Path(PathBuf),
Stdin,
Anonymous,
}
impl fmt::Display for SourceFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path(p) => write!(f, "{}", p.display()),
Self::Stdin => f.write_str("<stdin>"),
Self::Anonymous => f.write_str("<input>"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
En,
Fr,
Unknown,
}
impl fmt::Display for Language {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::En => f.write_str("en"),
Self::Fr => f.write_str("fr"),
Self::Unknown => f.write_str("unknown"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn category_for_rule_maps_known_ids() {
assert_eq!(
Category::for_rule("structure.sentence-too-long"),
Category::Structure
);
assert_eq!(
Category::for_rule("structure.excessive-commas"),
Category::Structure
);
assert_eq!(
Category::for_rule("rhythm.consecutive-long-sentences"),
Category::Rhythm
);
assert_eq!(
Category::for_rule("rhythm.repetitive-connectors"),
Category::Rhythm
);
assert_eq!(
Category::for_rule("lexicon.weasel-words"),
Category::Lexicon
);
assert_eq!(Category::for_rule("syntax.passive-voice"), Category::Syntax);
assert_eq!(
Category::for_rule("readability.score"),
Category::Readability
);
}
#[test]
fn category_for_unknown_rule_defaults_to_syntax() {
assert_eq!(Category::for_rule("no-such-rule"), Category::Syntax);
}
#[test]
fn category_all_has_five_variants_in_fixed_order() {
assert_eq!(Category::ALL.len(), 5);
assert_eq!(Category::ALL[0], Category::Structure);
assert_eq!(Category::ALL[4], Category::Readability);
}
#[test]
fn severity_display_is_lowercase() {
assert_eq!(Severity::Info.to_string(), "info");
assert_eq!(Severity::Warning.to_string(), "warning");
assert_eq!(Severity::Error.to_string(), "error");
}
#[test]
fn diagnostic_category_is_derived_from_rule_id() {
let location = Location::new(SourceFile::Anonymous, 1, 1, 5);
let diag = Diagnostic::new(
"structure.sentence-too-long",
Severity::Warning,
location,
"Too long",
);
assert_eq!(diag.category(), Category::Structure);
}
#[test]
fn diagnostic_with_section_sets_section() {
let location = Location::new(SourceFile::Anonymous, 1, 1, 5);
let diag = Diagnostic::new(
"structure.sentence-too-long",
Severity::Warning,
location,
"Too long",
)
.with_section("Introduction");
assert_eq!(diag.section.as_deref(), Some("Introduction"));
}
}