supa_mdx_lint/
errors.rs

1use std::{fmt::Display, ops::Range};
2
3use anyhow::Result;
4use bon::bon;
5use markdown::mdast::Node;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    context::Context,
10    fix::LintCorrection,
11    location::{AdjustedPoint, AdjustedRange, DenormalizedLocation, Offsets},
12};
13
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(rename_all = "UPPERCASE")]
16pub enum LintLevel {
17    Warning,
18    #[default]
19    Error,
20}
21
22impl Display for LintLevel {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            LintLevel::Error => write!(f, "ERROR"),
26            LintLevel::Warning => write!(f, "WARN"),
27        }
28    }
29}
30
31impl TryFrom<&str> for LintLevel {
32    type Error = anyhow::Error;
33
34    fn try_from(value: &str) -> Result<Self> {
35        let value = value.trim().to_lowercase();
36        match value.as_str() {
37            "error" => Ok(Self::Error),
38            "warn" => Ok(Self::Warning),
39            _ => Err(anyhow::anyhow!("Invalid lint level: {value}")),
40        }
41    }
42}
43
44#[derive(Debug, Clone, Deserialize, Serialize)]
45pub struct LintError {
46    pub(crate) rule: String,
47    pub(crate) level: LintLevel,
48    pub(crate) message: String,
49    pub(crate) location: DenormalizedLocation,
50    pub(crate) fix: Option<Vec<LintCorrection>>,
51    pub(crate) suggestions: Option<Vec<LintCorrection>>,
52}
53
54// Required to implement sealed trait Offsets
55impl crate::private::Sealed for LintError {}
56
57impl Offsets for LintError {
58    fn start(&self) -> usize {
59        self.location.offset_range.start.into()
60    }
61
62    fn end(&self) -> usize {
63        self.location.offset_range.end.into()
64    }
65}
66
67#[bon]
68impl LintError {
69    #[builder]
70    #[allow(clippy::needless_lifetimes)]
71    pub(crate) fn new<'ctx>(
72        rule: impl AsRef<str>,
73        message: impl Into<String>,
74        level: LintLevel,
75        location: AdjustedRange,
76        fix: Option<Vec<LintCorrection>>,
77        suggestions: Option<Vec<LintCorrection>>,
78        context: &Context<'ctx>,
79    ) -> Self {
80        let start = AdjustedPoint::from_adjusted_offset(&location.start, context.rope());
81        let end = AdjustedPoint::from_adjusted_offset(&location.end, context.rope());
82        let location = DenormalizedLocation {
83            offset_range: location,
84            start,
85            end,
86        };
87
88        Self {
89            rule: rule.as_ref().into(),
90            level,
91            message: message.into(),
92            location,
93            fix,
94            suggestions,
95        }
96    }
97
98    pub fn level(&self) -> LintLevel {
99        self.level
100    }
101
102    pub fn message(&self) -> &str {
103        &self.message
104    }
105
106    pub fn offset_range(&self) -> Range<usize> {
107        self.location.offset_range.to_usize_range()
108    }
109
110    pub fn combined_suggestions(&self) -> Option<Vec<&LintCorrection>> {
111        match (self.fix.as_ref(), self.suggestions.as_ref()) {
112            (None, None) => None,
113            (fix, suggestions) => {
114                let mut combined = Vec::new();
115                if let Some(f) = fix {
116                    combined.extend(f.iter());
117                }
118                if let Some(s) = suggestions {
119                    combined.extend(s.iter());
120                }
121                Some(combined)
122            }
123        }
124    }
125
126    #[builder]
127    #[allow(clippy::needless_lifetimes)]
128    pub(crate) fn from_node<'ctx>(
129        /// The AST node to generate the error location from.
130        node: &Node,
131        context: &Context<'ctx>,
132        /// The rule name.
133        rule: impl AsRef<str>,
134        message: &str,
135        level: LintLevel,
136        fix: Option<Vec<LintCorrection>>,
137        suggestions: Option<Vec<LintCorrection>>,
138    ) -> Option<Self> {
139        if let Some(position) = node.position() {
140            let location = AdjustedRange::from_unadjusted_position(position, context);
141            Some(
142                Self::builder()
143                    .location(location)
144                    .context(context)
145                    .rule(rule)
146                    .message(message)
147                    .level(level)
148                    .maybe_fix(fix)
149                    .maybe_suggestions(suggestions)
150                    .build(),
151            )
152        } else {
153            None
154        }
155    }
156
157    #[builder]
158    pub(crate) fn from_raw_location(
159        rule: impl AsRef<str>,
160        message: impl Into<String>,
161        level: LintLevel,
162        location: DenormalizedLocation,
163        fix: Option<Vec<LintCorrection>>,
164        suggestions: Option<Vec<LintCorrection>>,
165    ) -> Self {
166        Self {
167            rule: rule.as_ref().into(),
168            level,
169            message: message.into(),
170            location,
171            fix,
172            suggestions,
173        }
174    }
175}