Skip to main content

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