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
54impl 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 node: &Node,
131 context: &Context<'ctx>,
132 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}