nu_lint/violation.rs
1use std::borrow::Cow;
2
3use miette::SourceSpan;
4use nu_protocol::Span;
5
6use crate::config::LintLevel;
7
8/// A lint violation with its diagnostic information
9///
10/// # Display Format
11///
12/// When displayed to the user, violations appear as:
13///
14/// ```text
15/// file.nu:10:5
16/// warn(rule_name)
17///
18/// ⚠ [message] <- Short diagnostic message
19/// ╭─[10:5]
20/// 10 │ code here
21/// · ─┬─
22/// · ╰── [label text] <- Message (or help if short)
23/// ╰────
24///
25/// help: [help text] <- Detailed explanation/rationale
26///
27/// ℹ Available fix: [explanation] <- Fix explanation
28/// - old code
29/// + new code <- From replacements
30/// ```
31///
32/// # Example
33///
34/// ```rust,ignore
35/// Violation::new("prefer_pipeline_input", "Use pipeline input", span)
36/// .with_help("Pipeline input enables better composability and streaming performance")
37/// .with_fix(Fix::with_explanation(
38/// format!("Use $in instead of ${}:\n {}", param, transformed_code),
39/// vec![Replacement::new(def_span, transformed_code)]
40/// ))
41/// ```
42#[derive(Debug, Clone)]
43pub struct Violation {
44 pub rule_id: Cow<'static, str>,
45 pub lint_level: LintLevel,
46
47 /// Short message shown in the warning header
48 /// Should be concise, typically < 80 chars
49 /// Example: "Use pipeline input instead of parameter"
50 pub message: Cow<'static, str>,
51
52 /// Span in source code where the violation occurs
53 pub span: Span,
54
55 /// Optional detailed explanation shown in the "help:" section
56 /// Use this to explain WHY the code should change or provide rationale
57 /// Example: "Pipeline input enables better composability and streaming
58 /// performance"
59 pub help: Option<Cow<'static, str>>,
60
61 /// Optional automated fix that can be applied
62 pub fix: Option<Fix>,
63
64 pub(crate) file: Option<Cow<'static, str>>,
65}
66
67impl Violation {
68 /// Create a new violation
69 ///
70 /// # Arguments
71 ///
72 /// * `rule_id` - The lint rule identifier (e.g., "`prefer_pipeline_input`")
73 /// * `message` - Short diagnostic message shown in the warning header
74 /// * `span` - Location in source code where the violation occurs
75 #[must_use]
76 pub fn new(rule_id: &'static str, message: impl Into<Cow<'static, str>>, span: Span) -> Self {
77 Self {
78 rule_id: Cow::Borrowed(rule_id),
79 lint_level: LintLevel::Allow, // Placeholder, will be set by engine
80 message: message.into(),
81 span,
82 help: None,
83 fix: None,
84 file: None,
85 }
86 }
87
88 /// Add detailed help text explaining why this change should be made
89 ///
90 /// This appears in the "help:" section of the diagnostic output.
91 ///
92 /// # Example
93 ///
94 /// ```rust,ignore
95 /// violation.with_help("Pipeline input enables better composability and streaming performance")
96 /// ```
97 #[must_use]
98 pub fn with_help(mut self, help: impl Into<Cow<'static, str>>) -> Self {
99 self.help = Some(help.into());
100 self
101 }
102
103 /// Add an automated fix to this violation
104 ///
105 /// # Example
106 ///
107 /// ```rust,ignore
108 /// violation.with_fix(Fix::with_explanation(
109 /// "Replace with pipeline input version",
110 /// vec![Replacement::new(span, new_code)]
111 /// ))
112 /// ```
113 #[must_use]
114 pub fn with_fix(mut self, fix: Fix) -> Self {
115 self.fix = Some(fix);
116 self
117 }
118
119 /// Set the lint level for this violation (used by the engine)
120 pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
121 self.lint_level = level;
122 }
123
124 #[must_use]
125 pub(crate) fn to_source_span(&self) -> SourceSpan {
126 SourceSpan::from((self.span.start, self.span.end - self.span.start))
127 }
128}
129
130/// An automated fix that can be applied to resolve a violation
131///
132/// # Display Format
133///
134/// Fixes are displayed as:
135///
136/// ```text
137/// ℹ Available fix: [explanation]
138/// - old code
139/// + new code
140/// ```
141///
142/// # Important
143///
144/// - `explanation`: User-facing text shown in "Available fix:" (can be
145/// multi-line)
146/// - `replacements[].replacement_text`: Actual code written to the file
147///
148/// These should be different! The explanation describes the change,
149/// the `replacement_text` is the actual code.
150///
151/// # Example
152///
153/// ```rust,ignore
154/// Fix::with_explanation(
155/// format!("Use pipeline input ($in) instead of parameter (${}):\n {}", param, full_code),
156/// vec![Replacement::new(span, actual_code_to_write)]
157/// )
158/// ```
159#[derive(Debug, Clone)]
160pub struct Fix {
161 /// User-facing explanation of what this fix does
162 /// Shown in the "ℹ Available fix:" line (can be multi-line)
163 pub explanation: Cow<'static, str>,
164
165 /// The actual code replacements to apply to the file
166 pub replacements: Vec<Replacement>,
167}
168
169impl Fix {
170 /// Create a fix with an explanation and code replacements
171 ///
172 /// # Arguments
173 ///
174 /// * `explanation` - User-facing description (shown in "Available fix:")
175 /// * `replacements` - Actual code changes to apply to the file
176 ///
177 /// # Example
178 ///
179 /// ```rust,ignore
180 /// Fix::with_explanation(
181 /// "Replace with is-not-empty",
182 /// vec![Replacement::new(span, "is-not-empty")]
183 /// )
184 /// ```
185 #[must_use]
186 pub fn with_explanation(
187 explanation: impl Into<Cow<'static, str>>,
188 replacements: Vec<Replacement>,
189 ) -> Self {
190 Self {
191 explanation: explanation.into(),
192 replacements,
193 }
194 }
195}
196
197/// A single code replacement to apply when fixing a violation
198///
199/// # Important
200///
201/// The `replacement_text` field contains the ACTUAL CODE that will be written
202/// to the file at the specified span. This is not shown directly to the user
203/// (except in the before/after diff), but is what gets applied when the fix
204/// runs.
205#[derive(Debug, Clone)]
206pub struct Replacement {
207 /// Span in source code to replace
208 pub span: Span,
209
210 /// New text to insert at this location
211 /// This is the ACTUAL CODE written to the file when the fix is applied
212 pub replacement_text: Cow<'static, str>,
213}
214
215impl Replacement {
216 /// Create a new code replacement
217 ///
218 /// # Arguments
219 ///
220 /// * `span` - Location in source code to replace
221 /// * `replacement_text` - Actual code to write (not a description!)
222 ///
223 /// # Example
224 ///
225 /// ```rust,ignore
226 /// Replacement::new(param_span, "[]") // Replace "[x: int]" with "[]"
227 /// ```
228 #[must_use]
229 pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
230 Self {
231 span,
232 replacement_text: replacement_text.into(),
233 }
234 }
235}