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}