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    /// Optional source code content (used for stdin or when file is not
67    /// accessible)
68    pub(crate) source: Option<Cow<'static, str>>,
69}
70
71impl Violation {
72    /// Create a new violation
73    ///
74    /// # Arguments
75    ///
76    /// * `rule_id` - The lint rule identifier (e.g., "`prefer_pipeline_input`")
77    /// * `message` - Short diagnostic message shown in the warning header
78    /// * `span` - Location in source code where the violation occurs
79    #[must_use]
80    pub fn new(rule_id: &'static str, message: impl Into<Cow<'static, str>>, span: Span) -> Self {
81        Self {
82            rule_id: Cow::Borrowed(rule_id),
83            lint_level: LintLevel::Allow, // Placeholder, will be set by engine
84            message: message.into(),
85            span,
86            help: None,
87            fix: None,
88            file: None,
89            source: None,
90        }
91    }
92
93    /// Add detailed help text explaining why this change should be made
94    ///
95    /// This appears in the "help:" section of the diagnostic output.
96    ///
97    /// # Example
98    ///
99    /// ```rust,ignore
100    /// violation.with_help("Pipeline input enables better composability and streaming performance")
101    /// ```
102    #[must_use]
103    pub fn with_help(mut self, help: impl Into<Cow<'static, str>>) -> Self {
104        self.help = Some(help.into());
105        self
106    }
107
108    /// Add an automated fix to this violation
109    ///
110    /// # Example
111    ///
112    /// ```rust,ignore
113    /// violation.with_fix(Fix::with_explanation(
114    ///     "Replace with pipeline input version",
115    ///     vec![Replacement::new(span, new_code)]
116    /// ))
117    /// ```
118    #[must_use]
119    pub fn with_fix(mut self, fix: Fix) -> Self {
120        self.fix = Some(fix);
121        self
122    }
123
124    /// Set the lint level for this violation (used by the engine)
125    pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
126        self.lint_level = level;
127    }
128
129    #[must_use]
130    pub(crate) fn to_source_span(&self) -> SourceSpan {
131        SourceSpan::from((self.span.start, self.span.end - self.span.start))
132    }
133}
134
135/// An automated fix that can be applied to resolve a violation
136///
137/// # Display Format
138///
139/// Fixes are displayed as:
140///
141/// ```text
142/// ℹ Available fix: [explanation]
143/// - old code
144/// + new code
145/// ```
146///
147/// # Important
148///
149/// - `explanation`: User-facing text shown in "Available fix:" (can be
150///   multi-line)
151/// - `replacements[].replacement_text`: Actual code written to the file
152///
153/// These should be different! The explanation describes the change,
154/// the `replacement_text` is the actual code.
155///
156/// # Example
157///
158/// ```rust,ignore
159/// Fix::with_explanation(
160///     format!("Use pipeline input ($in) instead of parameter (${}):\n  {}", param, full_code),
161///     vec![Replacement::new(span, actual_code_to_write)]
162/// )
163/// ```
164#[derive(Debug, Clone)]
165pub struct Fix {
166    /// User-facing explanation of what this fix does
167    /// Shown in the "ℹ Available fix:" line (can be multi-line)
168    pub explanation: Cow<'static, str>,
169
170    /// The actual code replacements to apply to the file
171    pub replacements: Vec<Replacement>,
172}
173
174impl Fix {
175    /// Create a fix with an explanation and code replacements
176    ///
177    /// # Arguments
178    ///
179    /// * `explanation` - User-facing description (shown in "Available fix:")
180    /// * `replacements` - Actual code changes to apply to the file
181    ///
182    /// # Example
183    ///
184    /// ```rust,ignore
185    /// Fix::with_explanation(
186    ///     "Replace with is-not-empty",
187    ///     vec![Replacement::new(span, "is-not-empty")]
188    /// )
189    /// ```
190    #[must_use]
191    pub fn with_explanation(
192        explanation: impl Into<Cow<'static, str>>,
193        replacements: Vec<Replacement>,
194    ) -> Self {
195        Self {
196            explanation: explanation.into(),
197            replacements,
198        }
199    }
200}
201
202/// A single code replacement to apply when fixing a violation
203///
204/// # Important
205///
206/// The `replacement_text` field contains the ACTUAL CODE that will be written
207/// to the file at the specified span. This is not shown directly to the user
208/// (except in the before/after diff), but is what gets applied when the fix
209/// runs.
210#[derive(Debug, Clone)]
211pub struct Replacement {
212    /// Span in source code to replace
213    pub span: Span,
214
215    /// New text to insert at this location
216    /// This is the ACTUAL CODE written to the file when the fix is applied
217    pub replacement_text: Cow<'static, str>,
218}
219
220impl Replacement {
221    /// Create a new code replacement
222    ///
223    /// # Arguments
224    ///
225    /// * `span` - Location in source code to replace
226    /// * `replacement_text` - Actual code to write (not a description!)
227    ///
228    /// # Example
229    ///
230    /// ```rust,ignore
231    /// Replacement::new(param_span, "[]")  // Replace "[x: int]" with "[]"
232    /// ```
233    #[must_use]
234    pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
235        Self {
236            span,
237            replacement_text: replacement_text.into(),
238        }
239    }
240}