forge_lint/sol/
mod.rs

1use crate::linter::{
2    EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter,
3    LinterConfig,
4};
5use foundry_common::{
6    comments::{
7        Comments,
8        inline_config::{InlineConfig, InlineConfigItem},
9    },
10    errors::convert_solar_errors,
11};
12use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage};
13use foundry_config::{DenyLevel, lint::Severity};
14use rayon::prelude::*;
15use solar::{
16    ast::{self as ast, visit::Visit as _},
17    interface::{
18        Session,
19        diagnostics::{self, HumanEmitter, JsonEmitter},
20    },
21    sema::{
22        Compiler, Gcx,
23        hir::{self, Visit as _},
24    },
25};
26use std::{
27    path::{Path, PathBuf},
28    sync::LazyLock,
29};
30use thiserror::Error;
31
32#[macro_use]
33pub mod macros;
34
35pub mod codesize;
36pub mod gas;
37pub mod high;
38pub mod info;
39pub mod med;
40
41static ALL_REGISTERED_LINTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
42    let mut lints = Vec::new();
43    lints.extend_from_slice(high::REGISTERED_LINTS);
44    lints.extend_from_slice(med::REGISTERED_LINTS);
45    lints.extend_from_slice(info::REGISTERED_LINTS);
46    lints.extend_from_slice(gas::REGISTERED_LINTS);
47    lints.extend_from_slice(codesize::REGISTERED_LINTS);
48    lints.into_iter().map(|lint| lint.id()).collect()
49});
50
51/// Linter implementation to analyze Solidity source code responsible for identifying
52/// vulnerabilities gas optimizations, and best practices.
53#[derive(Debug)]
54pub struct SolidityLinter<'a> {
55    path_config: ProjectPathsConfig,
56    severity: Option<Vec<Severity>>,
57    lints_included: Option<Vec<SolLint>>,
58    lints_excluded: Option<Vec<SolLint>>,
59    with_description: bool,
60    with_json_emitter: bool,
61    // lint-specific configuration
62    mixed_case_exceptions: &'a [String],
63}
64
65impl<'a> SolidityLinter<'a> {
66    pub fn new(path_config: ProjectPathsConfig) -> Self {
67        Self {
68            path_config,
69            with_description: true,
70            severity: None,
71            lints_included: None,
72            lints_excluded: None,
73            with_json_emitter: false,
74            mixed_case_exceptions: &[],
75        }
76    }
77
78    pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
79        self.severity = severity;
80        self
81    }
82
83    pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
84        self.lints_included = lints;
85        self
86    }
87
88    pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
89        self.lints_excluded = lints;
90        self
91    }
92
93    pub fn with_description(mut self, with: bool) -> Self {
94        self.with_description = with;
95        self
96    }
97
98    pub fn with_json_emitter(mut self, with: bool) -> Self {
99        self.with_json_emitter = with;
100        self
101    }
102
103    pub fn with_mixed_case_exceptions(mut self, exceptions: &'a [String]) -> Self {
104        self.mixed_case_exceptions = exceptions;
105        self
106    }
107
108    fn config(&'a self, inline: &'a InlineConfig<Vec<String>>) -> LinterConfig<'a> {
109        LinterConfig { inline, mixed_case_exceptions: self.mixed_case_exceptions }
110    }
111
112    fn include_lint(&self, lint: SolLint) -> bool {
113        self.severity.as_ref().is_none_or(|sev| sev.contains(&lint.severity()))
114            && self.lints_included.as_ref().is_none_or(|incl| incl.contains(&lint))
115            && !self.lints_excluded.as_ref().is_some_and(|excl| excl.contains(&lint))
116    }
117
118    fn process_source_ast<'gcx>(
119        &self,
120        sess: &'gcx Session,
121        ast: &'gcx ast::SourceUnit<'gcx>,
122        path: &Path,
123        inline_config: &InlineConfig<Vec<String>>,
124    ) -> Result<(), diagnostics::ErrorGuaranteed> {
125        // Declare all available passes and lints
126        let mut passes_and_lints = Vec::new();
127        passes_and_lints.extend(high::create_early_lint_passes());
128        passes_and_lints.extend(med::create_early_lint_passes());
129        passes_and_lints.extend(info::create_early_lint_passes());
130
131        // Do not apply 'gas' and 'codesize' severity rules on tests and scripts
132        if !self.path_config.is_test_or_script(path) {
133            passes_and_lints.extend(gas::create_early_lint_passes());
134            passes_and_lints.extend(codesize::create_early_lint_passes());
135        }
136
137        // Filter passes based on linter config
138        let (mut passes, lints): (Vec<Box<dyn EarlyLintPass<'_>>>, Vec<_>) = passes_and_lints
139            .into_iter()
140            .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
141                let included_ids: Vec<_> = lints
142                    .iter()
143                    .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
144                    .collect();
145
146                if !included_ids.is_empty() {
147                    passes.push(pass);
148                    ids.extend(included_ids);
149                }
150
151                (passes, ids)
152            });
153
154        // Initialize and run the early lint visitor
155        let ctx = LintContext::new(
156            sess,
157            self.with_description,
158            self.with_json_emitter,
159            self.config(inline_config),
160            lints,
161        );
162        let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes);
163        _ = early_visitor.visit_source_unit(ast);
164        early_visitor.post_source_unit(ast);
165
166        Ok(())
167    }
168
169    fn process_source_hir<'gcx>(
170        &self,
171        gcx: Gcx<'gcx>,
172        source_id: hir::SourceId,
173        path: &Path,
174        inline_config: &InlineConfig<Vec<String>>,
175    ) -> Result<(), diagnostics::ErrorGuaranteed> {
176        // Declare all available passes and lints
177        let mut passes_and_lints = Vec::new();
178        passes_and_lints.extend(high::create_late_lint_passes());
179        passes_and_lints.extend(med::create_late_lint_passes());
180        passes_and_lints.extend(info::create_late_lint_passes());
181
182        // Do not apply 'gas' and 'codesize' severity rules on tests and scripts
183        if !self.path_config.is_test_or_script(path) {
184            passes_and_lints.extend(gas::create_late_lint_passes());
185            passes_and_lints.extend(codesize::create_late_lint_passes());
186        }
187
188        // Filter passes based on config
189        let (mut passes, lints): (Vec<Box<dyn LateLintPass<'_>>>, Vec<_>) = passes_and_lints
190            .into_iter()
191            .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
192                let included_ids: Vec<_> = lints
193                    .iter()
194                    .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
195                    .collect();
196
197                if !included_ids.is_empty() {
198                    passes.push(pass);
199                    ids.extend(included_ids);
200                }
201
202                (passes, ids)
203            });
204
205        // Run late lint visitor
206        let ctx = LintContext::new(
207            gcx.sess,
208            self.with_description,
209            self.with_json_emitter,
210            self.config(inline_config),
211            lints,
212        );
213        let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
214
215        // Visit this specific source
216        let _ = late_visitor.visit_nested_source(source_id);
217
218        Ok(())
219    }
220}
221
222impl<'a> Linter for SolidityLinter<'a> {
223    type Language = SolcLanguage;
224    type Lint = SolLint;
225
226    fn lint(
227        &self,
228        input: &[PathBuf],
229        deny: DenyLevel,
230        compiler: &mut Compiler,
231    ) -> eyre::Result<()> {
232        convert_solar_errors(compiler.dcx())?;
233
234        let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
235
236        let sm = compiler.sess().clone_source_map();
237        let prev_emitter = compiler.dcx().set_emitter(if self.with_json_emitter {
238            let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
239            let json_emitter = JsonEmitter::new(writer, sm).rustc_like(true).ui_testing(ui_testing);
240            Box::new(json_emitter)
241        } else {
242            Box::new(HumanEmitter::stderr(Default::default()).source_map(Some(sm)))
243        });
244        let sess = compiler.sess_mut();
245        sess.dcx.set_flags_mut(|f| f.track_diagnostics = false);
246        if ui_testing {
247            sess.opts.unstable.ui_testing = true;
248            sess.reconfigure();
249        }
250
251        compiler.enter_mut(|compiler| -> eyre::Result<()> {
252            if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
253                let _ = compiler.lower_asts();
254            }
255
256            let gcx = compiler.gcx();
257
258            input.par_iter().for_each(|path| {
259                let path = &self.path_config.root.join(path);
260                let Some((_, ast_source)) = gcx.get_ast_source(path) else {
261                    panic!("AST source not found for {}", path.display());
262                };
263                let Some(ast) = &ast_source.ast else {
264                    panic!("AST missing for {}", path.display());
265                };
266
267                // Parse inline config.
268                let file = &ast_source.file;
269                let comments = Comments::new(file, gcx.sess.source_map(), false, false, None);
270                let inline_config = parse_inline_config(gcx.sess, &comments, ast);
271
272                // Early lints.
273                let _ = self.process_source_ast(gcx.sess, ast, path, &inline_config);
274
275                // Late lints.
276                let Some((hir_source_id, _)) = gcx.get_hir_source(path) else {
277                    panic!("HIR source not found for {}", path.display());
278                };
279                let _ = self.process_source_hir(gcx, hir_source_id, path, &inline_config);
280            });
281
282            convert_solar_errors(compiler.dcx())
283        })?;
284
285        let sess = compiler.sess_mut();
286        sess.dcx.set_emitter(prev_emitter);
287        if ui_testing {
288            sess.opts.unstable.ui_testing = false;
289            sess.reconfigure();
290        }
291
292        // Handle diagnostics and fail if necessary.
293        const MSG: &str = "aborting due to ";
294        match (deny, compiler.dcx().warn_count(), compiler.dcx().note_count()) {
295            // Deny warnings.
296            (DenyLevel::Warnings, w, n) if w > 0 => {
297                if n > 0 {
298                    Err(eyre::eyre!("{MSG}{w} linter warning(s); {n} note(s) were also emitted\n"))
299                } else {
300                    Err(eyre::eyre!("{MSG}{w} linter warning(s)\n"))
301                }
302            }
303
304            // Deny any diagnostic.
305            (DenyLevel::Notes, w, n) if w > 0 || n > 0 => match (w, n) {
306                (w, n) if w > 0 && n > 0 => {
307                    Err(eyre::eyre!("{MSG}{w} linter warning(s) and {n} note(s)\n"))
308                }
309                (w, 0) => Err(eyre::eyre!("{MSG}{w} linter warning(s)\n")),
310                (0, n) => Err(eyre::eyre!("{MSG}{n} linter note(s)\n")),
311                _ => unreachable!(),
312            },
313
314            // Otherwise, succeed.
315            _ => Ok(()),
316        }
317    }
318}
319
320fn parse_inline_config<'ast>(
321    sess: &Session,
322    comments: &Comments,
323    ast: &'ast ast::SourceUnit<'ast>,
324) -> InlineConfig<Vec<String>> {
325    let items = comments.iter().filter_map(|comment| {
326        let mut item = comment.lines.first()?.as_str();
327        if let Some(prefix) = comment.prefix() {
328            item = item.strip_prefix(prefix).unwrap_or(item);
329        }
330        if let Some(suffix) = comment.suffix() {
331            item = item.strip_suffix(suffix).unwrap_or(item);
332        }
333        let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
334        let span = comment.span;
335        match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
336            Ok(item) => Some((span, item)),
337            Err(e) => {
338                sess.dcx.warn(e.to_string()).span(span).emit();
339                None
340            }
341        }
342    });
343
344    InlineConfig::from_ast(items, ast, sess.source_map())
345}
346
347#[derive(Error, Debug)]
348pub enum SolLintError {
349    #[error("Unknown lint ID: {0}")]
350    InvalidId(String),
351}
352
353#[derive(Debug, Clone, Copy, Eq, PartialEq)]
354pub struct SolLint {
355    id: &'static str,
356    description: &'static str,
357    help: &'static str,
358    severity: Severity,
359}
360
361impl Lint for SolLint {
362    fn id(&self) -> &'static str {
363        self.id
364    }
365    fn severity(&self) -> Severity {
366        self.severity
367    }
368    fn description(&self) -> &'static str {
369        self.description
370    }
371    fn help(&self) -> &'static str {
372        self.help
373    }
374}
375
376impl<'a> TryFrom<&'a str> for SolLint {
377    type Error = SolLintError;
378
379    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
380        for &lint in high::REGISTERED_LINTS {
381            if lint.id() == value {
382                return Ok(lint);
383            }
384        }
385
386        for &lint in med::REGISTERED_LINTS {
387            if lint.id() == value {
388                return Ok(lint);
389            }
390        }
391
392        for &lint in info::REGISTERED_LINTS {
393            if lint.id() == value {
394                return Ok(lint);
395            }
396        }
397
398        for &lint in gas::REGISTERED_LINTS {
399            if lint.id() == value {
400                return Ok(lint);
401            }
402        }
403
404        for &lint in codesize::REGISTERED_LINTS {
405            if lint.id() == value {
406                return Ok(lint);
407            }
408        }
409
410        Err(SolLintError::InvalidId(value.to_string()))
411    }
412}