Skip to main content

rigsql_rules/
violation.rs

1use rigsql_core::Span;
2
3/// Severity of a lint violation.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Severity {
6    Error,
7    Warning,
8}
9
10/// A source-level edit that can be applied to fix a violation.
11#[derive(Debug, Clone)]
12pub struct SourceEdit {
13    /// Span to replace (use empty span for pure insert, non-empty for replace/delete).
14    pub span: Span,
15    /// Replacement text (empty string for deletion).
16    pub new_text: String,
17}
18
19impl SourceEdit {
20    /// Replace the text at `span` with `new_text`.
21    pub fn replace(span: Span, new_text: impl Into<String>) -> Self {
22        Self {
23            span,
24            new_text: new_text.into(),
25        }
26    }
27
28    /// Insert `text` before byte offset `offset`.
29    pub fn insert(offset: u32, text: impl Into<String>) -> Self {
30        Self {
31            span: Span::new(offset, offset),
32            new_text: text.into(),
33        }
34    }
35
36    /// Delete the text covered by `span`.
37    pub fn delete(span: Span) -> Self {
38        Self {
39            span,
40            new_text: String::new(),
41        }
42    }
43}
44
45/// A single lint violation found by a rule.
46#[derive(Debug, Clone)]
47pub struct LintViolation {
48    /// Rule code, e.g. "CP01".
49    pub rule_code: &'static str,
50    /// Human-readable message describing the violation (English).
51    pub message: String,
52    /// Translation key for the message (e.g. "rules.LT01.msg").
53    pub message_key: String,
54    /// Parameters for message interpolation (e.g. [("count", "3")]).
55    pub message_params: Vec<(String, String)>,
56    /// Location in source.
57    pub span: Span,
58    /// Severity level.
59    pub severity: Severity,
60    /// Suggested fixes (empty if not auto-fixable).
61    pub fixes: Vec<SourceEdit>,
62}
63
64impl LintViolation {
65    pub fn new(rule_code: &'static str, message: impl Into<String>, span: Span) -> Self {
66        let message = message.into();
67        Self {
68            rule_code,
69            message_key: String::new(),
70            message_params: Vec::new(),
71            message,
72            span,
73            severity: Severity::Warning,
74            fixes: Vec::new(),
75        }
76    }
77
78    /// Create a violation with a translation key and parameters.
79    pub fn with_msg_key(
80        rule_code: &'static str,
81        message: impl Into<String>,
82        span: Span,
83        message_key: impl Into<String>,
84        message_params: Vec<(String, String)>,
85    ) -> Self {
86        Self {
87            rule_code,
88            message: message.into(),
89            message_key: message_key.into(),
90            message_params,
91            span,
92            severity: Severity::Warning,
93            fixes: Vec::new(),
94        }
95    }
96
97    /// Create a violation with a suggested fix.
98    pub fn with_fix(
99        rule_code: &'static str,
100        message: impl Into<String>,
101        span: Span,
102        fixes: Vec<SourceEdit>,
103    ) -> Self {
104        let message = message.into();
105        Self {
106            rule_code,
107            message_key: String::new(),
108            message_params: Vec::new(),
109            message,
110            span,
111            severity: Severity::Warning,
112            fixes,
113        }
114    }
115
116    /// Create a violation with a suggested fix, translation key, and parameters.
117    pub fn with_fix_and_msg_key(
118        rule_code: &'static str,
119        message: impl Into<String>,
120        span: Span,
121        fixes: Vec<SourceEdit>,
122        message_key: impl Into<String>,
123        message_params: Vec<(String, String)>,
124    ) -> Self {
125        Self {
126            rule_code,
127            message: message.into(),
128            message_key: message_key.into(),
129            message_params,
130            span,
131            severity: Severity::Warning,
132            fixes,
133        }
134    }
135
136    /// Compute 1-based line and column from source text.
137    pub fn line_col(&self, source: &str) -> (usize, usize) {
138        let offset = (self.span.start as usize).min(source.len());
139        // Ensure we're at a char boundary
140        let offset = if source.is_char_boundary(offset) {
141            offset
142        } else {
143            // Walk backwards to find a valid char boundary
144            (0..offset)
145                .rev()
146                .find(|&i| source.is_char_boundary(i))
147                .unwrap_or(0)
148        };
149        let before = &source[..offset];
150        let line = before.chars().filter(|&c| c == '\n').count() + 1;
151        let col = before.rfind('\n').map_or(offset, |pos| offset - pos - 1) + 1;
152        (line, col)
153    }
154}