forge_lint/linter/
mod.rs

1mod early;
2mod late;
3
4pub use early::{EarlyLintPass, EarlyLintVisitor};
5pub use late::{LateLintPass, LateLintVisitor};
6
7use foundry_common::comments::inline_config::InlineConfig;
8use foundry_compilers::Language;
9use foundry_config::{DenyLevel, lint::Severity};
10use solar::{
11    interface::{
12        Session, Span,
13        diagnostics::{
14            Applicability, DiagBuilder, DiagId, DiagMsg, MultiSpan, Style, SuggestionStyle,
15        },
16    },
17    sema::Compiler,
18};
19use std::path::PathBuf;
20
21/// Trait representing a generic linter for analyzing and reporting issues in smart contract source
22/// code files.
23///
24/// A linter can be implemented for any smart contract language supported by Foundry.
25pub trait Linter: Send + Sync {
26    /// The target [`Language`].
27    type Language: Language;
28    /// The [`Lint`] type.
29    type Lint: Lint;
30
31    /// Run all lints.
32    ///
33    /// The `compiler` should have already been configured with all the sources necessary,
34    /// as well as having performed parsing and lowering.
35    ///
36    /// Should return an error based on the configured [`DenyLevel`] and the emitted diagnostics.
37    fn lint(&self, input: &[PathBuf], deny: DenyLevel, compiler: &mut Compiler)
38    -> eyre::Result<()>;
39}
40
41pub trait Lint {
42    fn id(&self) -> &'static str;
43    fn severity(&self) -> Severity;
44    fn description(&self) -> &'static str;
45    fn help(&self) -> &'static str;
46}
47
48pub struct LintContext<'s, 'c> {
49    sess: &'s Session,
50    with_description: bool,
51    with_json_emitter: bool,
52    pub config: LinterConfig<'c>,
53    active_lints: Vec<&'static str>,
54}
55
56pub struct LinterConfig<'s> {
57    pub inline: &'s InlineConfig<Vec<String>>,
58    pub mixed_case_exceptions: &'s [String],
59}
60
61impl<'s, 'c> LintContext<'s, 'c> {
62    pub fn new(
63        sess: &'s Session,
64        with_description: bool,
65        with_json_emitter: bool,
66        config: LinterConfig<'c>,
67        active_lints: Vec<&'static str>,
68    ) -> Self {
69        Self { sess, with_description, with_json_emitter, config, active_lints }
70    }
71
72    pub fn session(&self) -> &'s Session {
73        self.sess
74    }
75
76    // Helper method to check if a lint id is enabled.
77    //
78    // For performance reasons, some passes check several lints at once. Thus, this method is
79    // required to avoid unintended warnings.
80    pub fn is_lint_enabled(&self, id: &'static str) -> bool {
81        self.active_lints.contains(&id)
82    }
83
84    /// Helper method to emit diagnostics easily from passes
85    pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
86        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
87            return;
88        }
89
90        let desc = if self.with_description { lint.description() } else { "" };
91        let mut diag: DiagBuilder<'_, ()> = self
92            .sess
93            .dcx
94            .diag(lint.severity().into(), desc)
95            .code(DiagId::new_str(lint.id()))
96            .span(MultiSpan::from_span(span));
97
98        // Avoid ANSI characters when using a JSON emitter
99        if self.with_json_emitter {
100            diag = diag.help(lint.help());
101        } else {
102            diag = diag.help(hyperlink(lint.help()));
103        }
104
105        diag.emit();
106    }
107
108    /// Emit a diagnostic with a code suggestion.
109    ///
110    /// If no span is provided for [`SuggestionKind::Fix`], it will use the lint's span.
111    pub fn emit_with_suggestion<L: Lint>(
112        &self,
113        lint: &'static L,
114        span: Span,
115        suggestion: Suggestion,
116    ) {
117        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
118            return;
119        }
120
121        let desc = if self.with_description { lint.description() } else { "" };
122        let mut diag: DiagBuilder<'_, ()> = self
123            .sess
124            .dcx
125            .diag(lint.severity().into(), desc)
126            .code(DiagId::new_str(lint.id()))
127            .span(MultiSpan::from_span(span));
128
129        diag = match suggestion.kind {
130            SuggestionKind::Fix { span: fix_span, applicability, style } => diag
131                .span_suggestion_with_style(
132                    fix_span.unwrap_or(span),
133                    suggestion.desc.unwrap_or_default(),
134                    suggestion.content,
135                    applicability,
136                    style,
137                ),
138            SuggestionKind::Example => {
139                if let Some(note) = suggestion.to_note() {
140                    diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
141                } else {
142                    diag
143                }
144            }
145        };
146
147        // Avoid ANSI characters when using a JSON emitter
148        if self.with_json_emitter {
149            diag = diag.help(lint.help());
150        } else {
151            diag = diag.help(hyperlink(lint.help()));
152        }
153
154        diag.emit();
155    }
156
157    /// Gets the "raw" source code (snippet) of the given span.
158    pub fn span_to_snippet(&self, span: Span) -> Option<String> {
159        self.sess.source_map().span_to_snippet(span).ok()
160    }
161
162    /// Gets the number of leading whitespaces (indentation) of the line where the span begins.
163    pub fn get_span_indentation(&self, span: Span) -> usize {
164        if !span.is_dummy() {
165            // Get the line text and compute the indentation prior to the span's position.
166            let loc = self.sess.source_map().lookup_char_pos(span.lo());
167            if let Some(line_text) = loc.file.get_line(loc.line) {
168                let col_offset = loc.col.to_usize();
169                if col_offset <= line_text.len() {
170                    let prev_text = &line_text[..col_offset];
171                    return prev_text.len() - prev_text.trim().len();
172                }
173            }
174        }
175
176        0
177    }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq)]
181pub enum SuggestionKind {
182    /// A standalone block of code. Used for showing examples without suggesting a fix.
183    ///
184    /// Multi-line strings should include newlines.
185    Example,
186
187    /// A proposed code change, displayed as a diff. Used to suggest replacements, showing the code
188    /// to be removed (from `span`) and the code to be added (from `add`).
189    Fix {
190        /// The `Span` of the source code to be removed. Note that, if uninformed,
191        /// `fn emit_with_fix()` falls back to the lint span.
192        span: Option<Span>,
193        /// The applicability of the suggested fix.
194        applicability: Applicability,
195        /// The style of the suggested fix.
196        style: SuggestionStyle,
197    },
198}
199
200// An emittable diagnostic suggestion.
201//
202// Depending on its [`SuggestionKind`] will be emitted as a simple note (examples), or a fix
203// suggestion.
204#[derive(Debug, Clone, Eq, PartialEq)]
205pub struct Suggestion {
206    /// An optional description displayed above the code block.
207    desc: Option<&'static str>,
208    /// The actual suggestion.
209    content: String,
210    /// The suggestion type and its specific data.
211    kind: SuggestionKind,
212}
213
214impl Suggestion {
215    /// Creates a new [`SuggestionKind::Example`] suggestion.
216    pub fn example(content: String) -> Self {
217        Self { desc: None, content, kind: SuggestionKind::Example }
218    }
219
220    /// Creates a new [`SuggestionKind::Fix`] suggestion.
221    ///
222    /// When possible, will attempt to inline the suggestion.
223    pub fn fix(content: String, applicability: Applicability) -> Self {
224        Self {
225            desc: None,
226            content,
227            kind: SuggestionKind::Fix {
228                span: None,
229                applicability,
230                style: SuggestionStyle::ShowCode,
231            },
232        }
233    }
234
235    /// Sets the description for the suggestion.
236    pub fn with_desc(mut self, desc: &'static str) -> Self {
237        self.desc = Some(desc);
238        self
239    }
240
241    /// Sets the span for a [`SuggestionKind::Fix`] suggestion.
242    pub fn with_span(mut self, span: Span) -> Self {
243        if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
244            *s = Some(span);
245        }
246        self
247    }
248
249    /// Sets the style for a [`SuggestionKind::Fix`] suggestion.
250    pub fn with_style(mut self, style: SuggestionStyle) -> Self {
251        if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
252            *s = style;
253        }
254        self
255    }
256
257    fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
258        if let SuggestionKind::Fix { .. } = &self.kind {
259            return None;
260        };
261
262        let mut output = if let Some(desc) = self.desc {
263            vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
264        } else {
265            vec![(DiagMsg::from(" \n"), Style::NoStyle)]
266        };
267
268        output.extend(
269            self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
270        );
271        output.push((DiagMsg::from("\n"), Style::NoStyle));
272        Some(output)
273    }
274}
275
276/// Creates a hyperlink of the input url.
277fn hyperlink(url: &'static str) -> String {
278    format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
279}