Skip to main content

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