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