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 appropiate 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.
32pub trait Linter: Send + Sync + Clone {
33    type Language: Language;
34    type Lint: Lint;
35
36    fn init(&self) -> Session;
37    fn early_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
38    fn late_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
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> {
49    sess: &'s Session,
50    with_description: bool,
51    pub inline_config: InlineConfig,
52}
53
54impl<'s> LintContext<'s> {
55    pub fn new(sess: &'s Session, with_description: bool, config: InlineConfig) -> Self {
56        Self { sess, with_description, inline_config: config }
57    }
58
59    pub fn session(&self) -> &'s Session {
60        self.sess
61    }
62
63    /// Helper method to emit diagnostics easily from passes
64    pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
65        if self.inline_config.is_disabled(span, lint.id()) {
66            return;
67        }
68
69        let desc = if self.with_description { lint.description() } else { "" };
70        let diag: DiagBuilder<'_, ()> = self
71            .sess
72            .dcx
73            .diag(lint.severity().into(), desc)
74            .code(DiagId::new_str(lint.id()))
75            .span(MultiSpan::from_span(span))
76            .help(lint.help());
77
78        diag.emit();
79    }
80
81    /// Emit a diagnostic with a code fix proposal.
82    ///
83    /// For Diff snippets, if no span is provided, it will use the lint's span.
84    /// If unable to get code from the span, it will fall back to a Block snippet.
85    pub fn emit_with_fix<L: Lint>(&self, lint: &'static L, span: Span, snippet: Snippet) {
86        if self.inline_config.is_disabled(span, lint.id()) {
87            return;
88        }
89
90        // Convert the snippet to ensure we have the appropriate type
91        let snippet = match snippet {
92            Snippet::Diff { desc, span: diff_span, add } => {
93                // Use the provided span or fall back to the lint span
94                let target_span = diff_span.unwrap_or(span);
95
96                // Check if we can get the original code
97                if self.span_to_snippet(target_span).is_some() {
98                    Snippet::Diff { desc, span: Some(target_span), add }
99                } else {
100                    // Fall back to Block if we can't get the original code
101                    Snippet::Block { desc, code: add }
102                }
103            }
104            // Block snippets remain unchanged
105            block => block,
106        };
107
108        let desc = if self.with_description { lint.description() } else { "" };
109        let diag: DiagBuilder<'_, ()> = self
110            .sess
111            .dcx
112            .diag(lint.severity().into(), desc)
113            .code(DiagId::new_str(lint.id()))
114            .span(MultiSpan::from_span(span))
115            .highlighted_note(snippet.to_note(self))
116            .help(lint.help());
117
118        diag.emit();
119    }
120
121    pub fn span_to_snippet(&self, span: Span) -> Option<String> {
122        self.sess.source_map().span_to_snippet(span).ok()
123    }
124}
125
126#[derive(Debug, Clone, Eq, PartialEq)]
127pub enum Snippet {
128    /// Represents a code block. Can have an optional description.
129    Block { desc: Option<&'static str>, code: String },
130    /// Represents a code diff. Can have an optional description and a span for the code to remove.
131    Diff { desc: Option<&'static str>, span: Option<Span>, add: String },
132}
133
134impl Snippet {
135    pub fn to_note(self, ctx: &LintContext<'_>) -> Vec<(DiagMsg, Style)> {
136        let mut output = Vec::new();
137        match self.desc() {
138            Some(desc) => {
139                output.push((DiagMsg::from(desc), Style::NoStyle));
140                output.push((DiagMsg::from("\n\n"), Style::NoStyle));
141            }
142            None => output.push((DiagMsg::from(" \n"), Style::NoStyle)),
143        }
144        match self {
145            Self::Diff { span, add, .. } => {
146                // Get the original code from the span if provided
147                if let Some(span) = span
148                    && let Some(rmv) = ctx.span_to_snippet(span)
149                {
150                    for line in rmv.lines() {
151                        output.push((DiagMsg::from(format!("- {line}\n")), Style::Removal));
152                    }
153                }
154                for line in add.lines() {
155                    output.push((DiagMsg::from(format!("+ {line}\n")), Style::Addition));
156                }
157            }
158            Self::Block { code, .. } => {
159                for line in code.lines() {
160                    output.push((DiagMsg::from(format!("- {line}\n")), Style::NoStyle));
161                }
162            }
163        }
164        output.push((DiagMsg::from("\n"), Style::NoStyle));
165        output
166    }
167
168    pub fn desc(&self) -> Option<&'static str> {
169        match self {
170            Self::Diff { desc, .. } => *desc,
171            Self::Block { desc, .. } => *desc,
172        }
173    }
174}