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::{cell::RefCell, collections::HashSet, 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    emitted: RefCell<HashSet<(&'static str, Span)>>,
63}
64
65pub struct LinterConfig<'s> {
66    pub inline: &'s InlineConfig<Vec<String>>,
67    pub lint_specific: &'s LintSpecificConfig,
68}
69
70impl<'s, 'c> LintContext<'s, 'c> {
71    pub fn new(
72        sess: &'s Session,
73        with_description: bool,
74        with_json_emitter: bool,
75        config: LinterConfig<'c>,
76        active_lints: Vec<&'static str>,
77        source_file: Option<Arc<SourceFile>>,
78    ) -> Self {
79        Self {
80            sess,
81            with_description,
82            with_json_emitter,
83            config,
84            active_lints,
85            source_file,
86            emitted: RefCell::default(),
87        }
88    }
89
90    fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
91        // Avoid ANSI characters when using a JSON emitter
92        if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
93    }
94
95    pub const fn session(&self) -> &'s Session {
96        self.sess
97    }
98
99    /// Returns the source file currently being linted, if any.
100    pub const fn source_file(&self) -> Option<&Arc<SourceFile>> {
101        self.source_file.as_ref()
102    }
103
104    // Helper method to check if a lint id is enabled.
105    //
106    // For performance reasons, some passes check several lints at once. Thus, this method is
107    // required to avoid unintended warnings.
108    pub fn is_lint_enabled(&self, id: &'static str) -> bool {
109        self.active_lints.contains(&id)
110    }
111
112    /// Helper method to emit diagnostics easily from passes
113    pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
114        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
115            return;
116        }
117        if !self.emitted.borrow_mut().insert((lint.id(), span)) {
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 = self.add_help(diag, lint.help());
130
131        diag.emit();
132    }
133
134    /// Emit a diagnostic with a caller-provided message instead of the lint's description.
135    ///
136    /// Useful when the message must vary per occurrence (e.g. embedding the offending
137    /// codepoint detected by the `rtlo` lint).
138    pub fn emit_with_msg<L: Lint>(&self, lint: &'static L, span: Span, msg: impl Into<DiagMsg>) {
139        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
140            return;
141        }
142
143        let diag: DiagBuilder<'_, ()> = self
144            .sess
145            .dcx
146            .diag(lint.severity().into(), msg.into())
147            .code(DiagId::new_str(lint.id()))
148            .span(MultiSpan::from_span(span));
149
150        self.add_help(diag, lint.help()).emit();
151    }
152
153    /// Emit a diagnostic with a code suggestion.
154    ///
155    /// If no span is provided for [`SuggestionKind::Fix`], it will use the lint's span.
156    pub fn emit_with_suggestion<L: Lint>(
157        &self,
158        lint: &'static L,
159        span: Span,
160        suggestion: Suggestion,
161    ) {
162        if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
163            return;
164        }
165
166        let desc = if self.with_description { lint.description() } else { "" };
167        let mut diag: DiagBuilder<'_, ()> = self
168            .sess
169            .dcx
170            .diag(lint.severity().into(), desc)
171            .code(DiagId::new_str(lint.id()))
172            .span(MultiSpan::from_span(span));
173
174        diag = match suggestion.kind {
175            SuggestionKind::Fix { span: fix_span, applicability, style } => diag
176                .span_suggestion_with_style(
177                    fix_span.unwrap_or(span),
178                    suggestion.desc.unwrap_or_default(),
179                    suggestion.content,
180                    applicability,
181                    style,
182                ),
183            SuggestionKind::Example => {
184                if let Some(note) = suggestion.to_note() {
185                    diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
186                } else {
187                    diag
188                }
189            }
190        };
191
192        diag = self.add_help(diag, lint.help());
193
194        diag.emit();
195    }
196
197    /// Gets the "raw" source code (snippet) of the given span.
198    pub fn span_to_snippet(&self, span: Span) -> Option<String> {
199        self.sess.source_map().span_to_snippet(span).ok()
200    }
201
202    /// Gets the number of leading whitespaces (indentation) of the line where the span begins.
203    pub fn get_span_indentation(&self, span: Span) -> usize {
204        if !span.is_dummy() {
205            // Get the line text and compute the indentation prior to the span's position.
206            let loc = self.sess.source_map().lookup_char_pos(span.lo());
207            if let Some(line_text) = loc.file.get_line(loc.line) {
208                let col_offset = loc.col.to_usize();
209                if col_offset <= line_text.len() {
210                    let prev_text = &line_text[..col_offset];
211                    return prev_text.len() - prev_text.trim().len();
212                }
213            }
214        }
215
216        0
217    }
218}
219
220#[derive(Debug, Clone, Eq, PartialEq)]
221pub enum SuggestionKind {
222    /// A standalone block of code. Used for showing examples without suggesting a fix.
223    ///
224    /// Multi-line strings should include newlines.
225    Example,
226
227    /// A proposed code change, displayed as a diff. Used to suggest replacements, showing the code
228    /// to be removed (from `span`) and the code to be added (from `add`).
229    Fix {
230        /// The `Span` of the source code to be removed. Note that, if uninformed,
231        /// `fn emit_with_fix()` falls back to the lint span.
232        span: Option<Span>,
233        /// The applicability of the suggested fix.
234        applicability: Applicability,
235        /// The style of the suggested fix.
236        style: SuggestionStyle,
237    },
238}
239
240// An emittable diagnostic suggestion.
241//
242// Depending on its [`SuggestionKind`] will be emitted as a simple note (examples), or a fix
243// suggestion.
244#[derive(Debug, Clone, Eq, PartialEq)]
245pub struct Suggestion {
246    /// An optional description displayed above the code block.
247    desc: Option<&'static str>,
248    /// The actual suggestion.
249    content: String,
250    /// The suggestion type and its specific data.
251    kind: SuggestionKind,
252}
253
254impl Suggestion {
255    /// Creates a new [`SuggestionKind::Example`] suggestion.
256    pub const fn example(content: String) -> Self {
257        Self { desc: None, content, kind: SuggestionKind::Example }
258    }
259
260    /// Creates a new [`SuggestionKind::Fix`] suggestion.
261    ///
262    /// When possible, will attempt to inline the suggestion.
263    pub const fn fix(content: String, applicability: Applicability) -> Self {
264        Self {
265            desc: None,
266            content,
267            kind: SuggestionKind::Fix {
268                span: None,
269                applicability,
270                style: SuggestionStyle::ShowCode,
271            },
272        }
273    }
274
275    /// Sets the description for the suggestion.
276    pub const fn with_desc(mut self, desc: &'static str) -> Self {
277        self.desc = Some(desc);
278        self
279    }
280
281    /// Sets the span for a [`SuggestionKind::Fix`] suggestion.
282    pub const fn with_span(mut self, span: Span) -> Self {
283        if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
284            *s = Some(span);
285        }
286        self
287    }
288
289    /// Sets the style for a [`SuggestionKind::Fix`] suggestion.
290    pub const fn with_style(mut self, style: SuggestionStyle) -> Self {
291        if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
292            *s = style;
293        }
294        self
295    }
296
297    fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
298        if let SuggestionKind::Fix { .. } = &self.kind {
299            return None;
300        };
301
302        let mut output = if let Some(desc) = self.desc {
303            vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
304        } else {
305            vec![(DiagMsg::from(" \n"), Style::NoStyle)]
306        };
307
308        output.extend(
309            self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
310        );
311        output.push((DiagMsg::from("\n"), Style::NoStyle));
312        Some(output)
313    }
314}
315
316/// Creates a hyperlink of the input url.
317fn hyperlink(url: &'static str) -> String {
318    format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
319}