Skip to main content

forge_lint/linter/
mod.rs

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