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