forge_lint/sol/
mod.rs

1use crate::{
2    inline_config::{InlineConfig, InlineConfigItem},
3    linter::{
4        EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter,
5        LinterConfig,
6    },
7};
8use foundry_common::comments::Comments;
9use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage};
10use foundry_config::lint::Severity;
11use rayon::prelude::*;
12use solar::{
13    ast::{self as ast, visit::Visit as VisitAST},
14    interface::{
15        Session,
16        diagnostics::{self, HumanEmitter, JsonEmitter},
17        source_map::{FileName, SourceFile},
18    },
19    sema::{
20        Compiler, Gcx,
21        hir::{self, Visit as VisitHIR},
22    },
23};
24use std::{
25    path::{Path, PathBuf},
26    sync::LazyLock,
27};
28use thiserror::Error;
29
30#[macro_use]
31pub mod macros;
32
33pub mod codesize;
34pub mod gas;
35pub mod high;
36pub mod info;
37pub mod med;
38
39static ALL_REGISTERED_LINTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
40    let mut lints = Vec::new();
41    lints.extend_from_slice(high::REGISTERED_LINTS);
42    lints.extend_from_slice(med::REGISTERED_LINTS);
43    lints.extend_from_slice(info::REGISTERED_LINTS);
44    lints.extend_from_slice(gas::REGISTERED_LINTS);
45    lints.extend_from_slice(codesize::REGISTERED_LINTS);
46    lints.into_iter().map(|lint| lint.id()).collect()
47});
48
49/// Linter implementation to analyze Solidity source code responsible for identifying
50/// vulnerabilities gas optimizations, and best practices.
51#[derive(Debug)]
52pub struct SolidityLinter<'a> {
53    path_config: ProjectPathsConfig,
54    severity: Option<Vec<Severity>>,
55    lints_included: Option<Vec<SolLint>>,
56    lints_excluded: Option<Vec<SolLint>>,
57    with_description: bool,
58    with_json_emitter: bool,
59    mixed_case_exceptions: &'a [String],
60}
61
62impl<'a> SolidityLinter<'a> {
63    pub fn new(path_config: ProjectPathsConfig) -> Self {
64        Self {
65            path_config,
66            with_description: true,
67            severity: None,
68            lints_included: None,
69            lints_excluded: None,
70            with_json_emitter: false,
71            mixed_case_exceptions: &[],
72        }
73    }
74
75    pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
76        self.severity = severity;
77        self
78    }
79
80    pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
81        self.lints_included = lints;
82        self
83    }
84
85    pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
86        self.lints_excluded = lints;
87        self
88    }
89
90    pub fn with_description(mut self, with: bool) -> Self {
91        self.with_description = with;
92        self
93    }
94
95    pub fn with_json_emitter(mut self, with: bool) -> Self {
96        self.with_json_emitter = with;
97        self
98    }
99
100    pub fn with_mixed_case_exceptions(mut self, exceptions: &'a [String]) -> Self {
101        self.mixed_case_exceptions = exceptions;
102        self
103    }
104
105    fn config(&self, inline: InlineConfig) -> LinterConfig<'_> {
106        LinterConfig { inline, mixed_case_exceptions: self.mixed_case_exceptions }
107    }
108
109    fn include_lint(&self, lint: SolLint) -> bool {
110        self.severity.as_ref().is_none_or(|sev| sev.contains(&lint.severity()))
111            && self.lints_included.as_ref().is_none_or(|incl| incl.contains(&lint))
112            && !self.lints_excluded.as_ref().is_some_and(|excl| excl.contains(&lint))
113    }
114
115    fn process_source_ast<'gcx>(
116        &self,
117        sess: &'gcx Session,
118        ast: &'gcx ast::SourceUnit<'gcx>,
119        file: &SourceFile,
120        path: &Path,
121    ) -> Result<(), diagnostics::ErrorGuaranteed> {
122        // Declare all available passes and lints
123        let mut passes_and_lints = Vec::new();
124        passes_and_lints.extend(high::create_early_lint_passes());
125        passes_and_lints.extend(med::create_early_lint_passes());
126        passes_and_lints.extend(info::create_early_lint_passes());
127
128        // Do not apply 'gas' and 'codesize' severity rules on tests and scripts
129        if !self.path_config.is_test_or_script(path) {
130            passes_and_lints.extend(gas::create_early_lint_passes());
131            passes_and_lints.extend(codesize::create_early_lint_passes());
132        }
133
134        // Filter passes based on linter config
135        let (mut passes, lints): (Vec<Box<dyn EarlyLintPass<'_>>>, Vec<_>) = passes_and_lints
136            .into_iter()
137            .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
138                let included_ids: Vec<_> = lints
139                    .iter()
140                    .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
141                    .collect();
142
143                if !included_ids.is_empty() {
144                    passes.push(pass);
145                    ids.extend(included_ids);
146                }
147
148                (passes, ids)
149            });
150
151        // Process the inline-config
152        let comments = Comments::new(file);
153        let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Ast(ast));
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        file: &'gcx SourceFile,
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 let FileName::Real(path) = &file.name
184            && !self.path_config.is_test_or_script(path)
185        {
186            passes_and_lints.extend(gas::create_late_lint_passes());
187            passes_and_lints.extend(codesize::create_late_lint_passes());
188        }
189
190        // Filter passes based on config
191        let (mut passes, lints): (Vec<Box<dyn LateLintPass<'_>>>, Vec<_>) = passes_and_lints
192            .into_iter()
193            .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
194                let included_ids: Vec<_> = lints
195                    .iter()
196                    .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
197                    .collect();
198
199                if !included_ids.is_empty() {
200                    passes.push(pass);
201                    ids.extend(included_ids);
202                }
203
204                (passes, ids)
205            });
206
207        // Process the inline-config
208        let comments = Comments::new(file);
209        let inline_config = parse_inline_config(
210            gcx.sess,
211            &comments,
212            InlineConfigSource::Hir((&gcx.hir, source_id)),
213        );
214
215        // Run late lint visitor
216        let ctx = LintContext::new(
217            gcx.sess,
218            self.with_description,
219            self.with_json_emitter,
220            self.config(inline_config),
221            lints,
222        );
223        let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
224
225        // Visit this specific source
226        let _ = late_visitor.visit_nested_source(source_id);
227
228        Ok(())
229    }
230}
231
232impl<'a> Linter for SolidityLinter<'a> {
233    type Language = SolcLanguage;
234    type Lint = SolLint;
235
236    fn configure(&self, compiler: &mut Compiler) {
237        let dcx = compiler.dcx_mut();
238        let sm = dcx.source_map_mut().unwrap().clone();
239        dcx.set_emitter(if self.with_json_emitter {
240            let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
241            let json_emitter = JsonEmitter::new(writer, sm).rustc_like(true).ui_testing(false);
242            Box::new(json_emitter)
243        } else {
244            Box::new(HumanEmitter::stderr(Default::default()).source_map(Some(sm)))
245        });
246        dcx.set_flags_mut(|f| f.track_diagnostics = false);
247    }
248
249    fn lint(&self, input: &[PathBuf], compiler: &mut Compiler) {
250        compiler.enter_mut(|compiler| {
251            let gcx = compiler.gcx();
252
253            // Early lints.
254            gcx.sources.raw.par_iter().for_each(|source| {
255                if let (FileName::Real(path), Some(ast)) = (&source.file.name, &source.ast)
256                    && input.iter().any(|input_path| path.ends_with(input_path))
257                {
258                    let _ = self.process_source_ast(gcx.sess, ast, &source.file, path);
259                }
260            });
261
262            // Late lints.
263            gcx.hir.par_sources_enumerated().for_each(|(source_id, source)| {
264                if let FileName::Real(path) = &source.file.name
265                    && input.iter().any(|input_path| path.ends_with(input_path))
266                {
267                    let _ = self.process_source_hir(gcx, source_id, &source.file);
268                }
269            });
270        });
271    }
272}
273
274enum InlineConfigSource<'ast, 'hir> {
275    Ast(&'ast ast::SourceUnit<'ast>),
276    Hir((&'hir hir::Hir<'hir>, hir::SourceId)),
277}
278
279fn parse_inline_config<'ast, 'hir>(
280    sess: &Session,
281    comments: &Comments,
282    source: InlineConfigSource<'ast, 'hir>,
283) -> InlineConfig {
284    let items = comments.iter().filter_map(|comment| {
285        let mut item = comment.lines.first()?.as_str();
286        if let Some(prefix) = comment.prefix() {
287            item = item.strip_prefix(prefix).unwrap_or(item);
288        }
289        if let Some(suffix) = comment.suffix() {
290            item = item.strip_suffix(suffix).unwrap_or(item);
291        }
292        let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
293        let span = comment.span;
294        match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
295            Ok(item) => Some((span, item)),
296            Err(e) => {
297                sess.dcx.warn(e.to_string()).span(span).emit();
298                None
299            }
300        }
301    });
302
303    match source {
304        InlineConfigSource::Ast(ast) => InlineConfig::from_ast(items, ast, sess.source_map()),
305        InlineConfigSource::Hir((hir, id)) => {
306            InlineConfig::from_hir(items, hir, id, sess.source_map())
307        }
308    }
309}
310
311#[derive(Error, Debug)]
312pub enum SolLintError {
313    #[error("Unknown lint ID: {0}")]
314    InvalidId(String),
315}
316
317#[derive(Debug, Clone, Copy, Eq, PartialEq)]
318pub struct SolLint {
319    id: &'static str,
320    description: &'static str,
321    help: &'static str,
322    severity: Severity,
323}
324
325impl Lint for SolLint {
326    fn id(&self) -> &'static str {
327        self.id
328    }
329    fn severity(&self) -> Severity {
330        self.severity
331    }
332    fn description(&self) -> &'static str {
333        self.description
334    }
335    fn help(&self) -> &'static str {
336        self.help
337    }
338}
339
340impl<'a> TryFrom<&'a str> for SolLint {
341    type Error = SolLintError;
342
343    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
344        for &lint in high::REGISTERED_LINTS {
345            if lint.id() == value {
346                return Ok(lint);
347            }
348        }
349
350        for &lint in med::REGISTERED_LINTS {
351            if lint.id() == value {
352                return Ok(lint);
353            }
354        }
355
356        for &lint in info::REGISTERED_LINTS {
357            if lint.id() == value {
358                return Ok(lint);
359            }
360        }
361
362        for &lint in gas::REGISTERED_LINTS {
363            if lint.id() == value {
364                return Ok(lint);
365            }
366        }
367
368        for &lint in codesize::REGISTERED_LINTS {
369            if lint.id() == value {
370                return Ok(lint);
371            }
372        }
373
374        Err(SolLintError::InvalidId(value.to_string()))
375    }
376}