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.
51    pub message: String,
52    /// Location in source.
53    pub span: Span,
54    /// Severity level.
55    pub severity: Severity,
56    /// Suggested fixes (empty if not auto-fixable).
57    pub fixes: Vec<SourceEdit>,
58}
59
60impl LintViolation {
61    pub fn new(rule_code: &'static str, message: impl Into<String>, span: Span) -> Self {
62        Self {
63            rule_code,
64            message: message.into(),
65            span,
66            severity: Severity::Warning,
67            fixes: Vec::new(),
68        }
69    }
70
71    /// Create a violation with a suggested fix.
72    pub fn with_fix(
73        rule_code: &'static str,
74        message: impl Into<String>,
75        span: Span,
76        fixes: Vec<SourceEdit>,
77    ) -> Self {
78        Self {
79            rule_code,
80            message: message.into(),
81            span,
82            severity: Severity::Warning,
83            fixes,
84        }
85    }
86
87    /// Compute 1-based line and column from source text.
88    pub fn line_col(&self, source: &str) -> (usize, usize) {
89        let offset = (self.span.start as usize).min(source.len());
90        // Ensure we're at a char boundary
91        let offset = if source.is_char_boundary(offset) {
92            offset
93        } else {
94            // Walk backwards to find a valid char boundary
95            (0..offset)
96                .rev()
97                .find(|&i| source.is_char_boundary(i))
98                .unwrap_or(0)
99        };
100        let before = &source[..offset];
101        let line = before.chars().filter(|&c| c == '\n').count() + 1;
102        let col = before.rfind('\n').map_or(offset, |pos| offset - pos - 1) + 1;
103        (line, col)
104    }
105}