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    fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
73        // Avoid ANSI characters when using a JSON emitter
74        if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
75    }
76
77    pub fn session(&self) -> &'s Session {
78        self.sess
79    }
80
81    // Helper method to check if a lint id is enabled.
82    //
83    // For performance reasons, some passes check several lints at once. Thus, this method is
84    // required to avoid unintended warnings.
85    pub fn is_lint_enabled(&self, id: &'static str) -> bool {
86        self.active_lints.contains(&id)
87    }
88
89    /// Helper method to emit diagnostics easily from passes
90    pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
91        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
92            return;
93        }
94
95        let desc = if self.with_description { lint.description() } else { "" };
96        let mut diag: DiagBuilder<'_, ()> = self
97            .sess
98            .dcx
99            .diag(lint.severity().into(), desc)
100            .code(DiagId::new_str(lint.id()))
101            .span(MultiSpan::from_span(span));
102
103        diag = self.add_help(diag, lint.help());
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        diag = self.add_help(diag, lint.help());
148
149        diag.emit();
150    }
151
152    /// Gets the "raw" source code (snippet) of the given span.
153    pub fn span_to_snippet(&self, span: Span) -> Option<String> {
154        self.sess.source_map().span_to_snippet(span).ok()
155    }
156
157    /// Gets the number of leading whitespaces (indentation) of the line where the span begins.
158    pub fn get_span_indentation(&self, span: Span) -> usize {
159        if !span.is_dummy() {
160            // Get the line text and compute the indentation prior to the span's position.
161            let loc = self.sess.source_map().lookup_char_pos(span.lo());
162            if let Some(line_text) = loc.file.get_line(loc.line) {
163                let col_offset = loc.col.to_usize();
164                if col_offset <= line_text.len() {
165                    let prev_text = &line_text[..col_offset];
166                    return prev_text.len() - prev_text.trim().len();
167                }
168            }
169        }
170
171        0
172    }
173}
174
175#[derive(Debug, Clone, Eq, PartialEq)]
176pub enum SuggestionKind {
177    /// A standalone block of code. Used for showing examples without suggesting a fix.
178    ///
179    /// Multi-line strings should include newlines.
180    Example,
181
182    /// A proposed code change, displayed as a diff. Used to suggest replacements, showing the code
183    /// to be removed (from `span`) and the code to be added (from `add`).
184    Fix {
185        /// The `Span` of the source code to be removed. Note that, if uninformed,
186        /// `fn emit_with_fix()` falls back to the lint span.
187        span: Option<Span>,
188        /// The applicability of the suggested fix.
189        applicability: Applicability,
190        /// The style of the suggested fix.
191        style: SuggestionStyle,
192    },
193}
194
195// An emittable diagnostic suggestion.
196//
197// Depending on its [`SuggestionKind`] will be emitted as a simple note (examples), or a fix
198// suggestion.
199#[derive(Debug, Clone, Eq, PartialEq)]
200pub struct Suggestion {
201    /// An optional description displayed above the code block.
202    desc: Option<&'static str>,
203    /// The actual suggestion.
204    content: String,
205    /// The suggestion type and its specific data.
206    kind: SuggestionKind,
207}
208
209impl Suggestion {
210    /// Creates a new [`SuggestionKind::Example`] suggestion.
211    pub fn example(content: String) -> Self {
212        Self { desc: None, content, kind: SuggestionKind::Example }
213    }
214
215    /// Creates a new [`SuggestionKind::Fix`] suggestion.
216    ///
217    /// When possible, will attempt to inline the suggestion.
218    pub fn fix(content: String, applicability: Applicability) -> Self {
219        Self {
220            desc: None,
221            content,
222            kind: SuggestionKind::Fix {
223                span: None,
224                applicability,
225                style: SuggestionStyle::ShowCode,
226            },
227        }
228    }
229
230    /// Sets the description for the suggestion.
231    pub fn with_desc(mut self, desc: &'static str) -> Self {
232        self.desc = Some(desc);
233        self
234    }
235
236    /// Sets the span for a [`SuggestionKind::Fix`] suggestion.
237    pub fn with_span(mut self, span: Span) -> Self {
238        if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
239            *s = Some(span);
240        }
241        self
242    }
243
244    /// Sets the style for a [`SuggestionKind::Fix`] suggestion.
245    pub fn with_style(mut self, style: SuggestionStyle) -> Self {
246        if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
247            *s = style;
248        }
249        self
250    }
251
252    fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
253        if let SuggestionKind::Fix { .. } = &self.kind {
254            return None;
255        };
256
257        let mut output = if let Some(desc) = self.desc {
258            vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
259        } else {
260            vec![(DiagMsg::from(" \n"), Style::NoStyle)]
261        };
262
263        output.extend(
264            self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
265        );
266        output.push((DiagMsg::from("\n"), Style::NoStyle));
267        Some(output)
268    }
269}
270
271/// Creates a hyperlink of the input url.
272fn hyperlink(url: &'static str) -> String {
273    format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
274}