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_compilers::Language;
8use foundry_config::lint::Severity;
9use solar_interface::{
10    Session, Span,
11    diagnostics::{DiagBuilder, DiagId, DiagMsg, MultiSpan, Style},
12};
13use solar_sema::ParsingContext;
14use std::path::PathBuf;
15
16use crate::inline_config::InlineConfig;
17
18/// Trait representing a generic linter for analyzing and reporting issues in smart contract source
19/// code files. A linter can be implemented for any smart contract language supported by Foundry.
20///
21/// # Type Parameters
22///
23/// - `Language`: Represents the target programming language. Must implement the [`Language`] trait.
24/// - `Lint`: Represents the types of lints performed by the linter. Must implement the [`Lint`]
25///   trait.
26///
27/// # Required Methods
28///
29/// - `init`: Creates a new solar `Session` with the appropriate linter configuration.
30/// - `early_lint`: Scans the source files (using the AST) emitting a diagnostic for lints found.
31/// - `late_lint`: Scans the source files (using the HIR) emitting a diagnostic for lints found.
32///
33/// # Note:
34///
35/// - For `early_lint` and `late_lint`, the `ParsingContext` should have the sources pre-loaded.
36pub trait Linter: Send + Sync + Clone {
37    type Language: Language;
38    type Lint: Lint;
39
40    fn init(&self) -> Session;
41    fn early_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
42    fn late_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
43}
44
45pub trait Lint {
46    fn id(&self) -> &'static str;
47    fn severity(&self) -> Severity;
48    fn description(&self) -> &'static str;
49    fn help(&self) -> &'static str;
50}
51
52pub struct LintContext<'s> {
53    sess: &'s Session,
54    with_description: bool,
55    pub inline_config: InlineConfig,
56    active_lints: Vec<&'static str>,
57}
58
59impl<'s> LintContext<'s> {
60    pub fn new(
61        sess: &'s Session,
62        with_description: bool,
63        config: InlineConfig,
64        active_lints: Vec<&'static str>,
65    ) -> Self {
66        Self { sess, with_description, inline_config: config, active_lints }
67    }
68
69    pub fn session(&self) -> &'s Session {
70        self.sess
71    }
72
73    // Helper method to check if a lint id is enabled.
74    //
75    // For performance reasons, some passes check several lints at once. Thus, this method is
76    // required to avoid unintended warnings.
77    pub fn is_lint_enabled(&self, id: &'static str) -> bool {
78        self.active_lints.contains(&id)
79    }
80
81    /// Helper method to emit diagnostics easily from passes
82    pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
83        if self.inline_config.is_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
84            return;
85        }
86
87        let desc = if self.with_description { lint.description() } else { "" };
88        let diag: DiagBuilder<'_, ()> = self
89            .sess
90            .dcx
91            .diag(lint.severity().into(), desc)
92            .code(DiagId::new_str(lint.id()))
93            .span(MultiSpan::from_span(span))
94            .help(lint.help());
95
96        diag.emit();
97    }
98
99    /// Emit a diagnostic with a code fix proposal.
100    ///
101    /// For Diff snippets, if no span is provided, it will use the lint's span.
102    /// If unable to get code from the span, it will fall back to a Block snippet.
103    pub fn emit_with_fix<L: Lint>(&self, lint: &'static L, span: Span, snippet: Snippet) {
104        if self.inline_config.is_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
105            return;
106        }
107
108        // Convert the snippet to ensure we have the appropriate type
109        let snippet = match snippet {
110            Snippet::Diff { desc, span: diff_span, add } => {
111                // Use the provided span or fall back to the lint span
112                let target_span = diff_span.unwrap_or(span);
113
114                // Check if we can get the original code
115                if self.span_to_snippet(target_span).is_some() {
116                    Snippet::Diff { desc, span: Some(target_span), add }
117                } else {
118                    // Fall back to Block if we can't get the original code
119                    Snippet::Block { desc, code: add }
120                }
121            }
122            // Block snippets remain unchanged
123            block => block,
124        };
125
126        let desc = if self.with_description { lint.description() } else { "" };
127        let diag: DiagBuilder<'_, ()> = self
128            .sess
129            .dcx
130            .diag(lint.severity().into(), desc)
131            .code(DiagId::new_str(lint.id()))
132            .span(MultiSpan::from_span(span))
133            .highlighted_note(snippet.to_note(self))
134            .help(lint.help());
135
136        diag.emit();
137    }
138
139    pub fn span_to_snippet(&self, span: Span) -> Option<String> {
140        self.sess.source_map().span_to_snippet(span).ok()
141    }
142}
143
144#[derive(Debug, Clone, Eq, PartialEq)]
145pub enum Snippet {
146    /// Represents a code block. Can have an optional description.
147    Block { desc: Option<&'static str>, code: String },
148    /// Represents a code diff. Can have an optional description and a span for the code to remove.
149    Diff { desc: Option<&'static str>, span: Option<Span>, add: String },
150}
151
152impl Snippet {
153    pub fn to_note(self, ctx: &LintContext<'_>) -> Vec<(DiagMsg, Style)> {
154        let mut output = Vec::new();
155        match self.desc() {
156            Some(desc) => {
157                output.push((DiagMsg::from(desc), Style::NoStyle));
158                output.push((DiagMsg::from("\n\n"), Style::NoStyle));
159            }
160            None => output.push((DiagMsg::from(" \n"), Style::NoStyle)),
161        }
162        match self {
163            Self::Diff { span, add, .. } => {
164                // Get the original code from the span if provided
165                if let Some(span) = span
166                    && let Some(rmv) = ctx.span_to_snippet(span)
167                {
168                    for line in rmv.lines() {
169                        output.push((DiagMsg::from(format!("- {line}\n")), Style::Removal));
170                    }
171                }
172                for line in add.lines() {
173                    output.push((DiagMsg::from(format!("+ {line}\n")), Style::Addition));
174                }
175            }
176            Self::Block { code, .. } => {
177                for line in code.lines() {
178                    output.push((DiagMsg::from(format!("- {line}\n")), Style::NoStyle));
179                }
180            }
181        }
182        output.push((DiagMsg::from("\n"), Style::NoStyle));
183        output
184    }
185
186    pub fn desc(&self) -> Option<&'static str> {
187        match self {
188            Self::Diff { desc, .. } => *desc,
189            Self::Block { desc, .. } => *desc,
190        }
191    }
192}