forge_lint/sol/
mod.rs

1use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter};
2use foundry_compilers::{solc::SolcLanguage, ProjectPathsConfig};
3use foundry_config::lint::Severity;
4use rayon::iter::{IntoParallelIterator, ParallelIterator};
5use solar_ast::{visit::Visit, Arena};
6use solar_interface::{
7    diagnostics::{self, DiagCtxt, JsonEmitter},
8    Session, SourceMap,
9};
10use std::{
11    path::{Path, PathBuf},
12    sync::Arc,
13};
14use thiserror::Error;
15
16pub mod macros;
17
18pub mod gas;
19pub mod high;
20pub mod info;
21pub mod med;
22
23/// Linter implementation to analyze Solidity source code responsible for identifying
24/// vulnerabilities gas optimizations, and best practices.
25#[derive(Debug, Clone)]
26pub struct SolidityLinter {
27    path_config: ProjectPathsConfig,
28    severity: Option<Vec<Severity>>,
29    lints_included: Option<Vec<SolLint>>,
30    lints_excluded: Option<Vec<SolLint>>,
31    with_description: bool,
32    with_json_emitter: bool,
33}
34
35impl SolidityLinter {
36    pub fn new(path_config: ProjectPathsConfig) -> Self {
37        Self {
38            path_config,
39            severity: None,
40            lints_included: None,
41            lints_excluded: None,
42            with_description: true,
43            with_json_emitter: false,
44        }
45    }
46
47    pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
48        self.severity = severity;
49        self
50    }
51
52    pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
53        self.lints_included = lints;
54        self
55    }
56
57    pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
58        self.lints_excluded = lints;
59        self
60    }
61
62    pub fn with_description(mut self, with: bool) -> Self {
63        self.with_description = with;
64        self
65    }
66
67    pub fn with_json_emitter(mut self, with: bool) -> Self {
68        self.with_json_emitter = with;
69        self
70    }
71
72    fn process_file(&self, sess: &Session, file: &Path) {
73        let arena = Arena::new();
74
75        let _ = sess.enter(|| -> Result<(), diagnostics::ErrorGuaranteed> {
76            // Declare all available passes and lints
77            let mut passes_and_lints = Vec::new();
78            passes_and_lints.extend(high::create_lint_passes());
79            passes_and_lints.extend(med::create_lint_passes());
80            passes_and_lints.extend(info::create_lint_passes());
81
82            // Do not apply gas-severity rules on tests and scripts
83            if !self.path_config.is_test_or_script(file) {
84                passes_and_lints.extend(gas::create_lint_passes());
85            }
86
87            // Filter based on linter config
88            let mut passes: Vec<Box<dyn EarlyLintPass<'_>>> = passes_and_lints
89                .into_iter()
90                .filter_map(|(pass, lint)| {
91                    let matches_severity = match self.severity {
92                        Some(ref target) => target.contains(&lint.severity()),
93                        None => true,
94                    };
95                    let matches_lints_inc = match self.lints_included {
96                        Some(ref target) => target.contains(&lint),
97                        None => true,
98                    };
99                    let matches_lints_exc = match self.lints_excluded {
100                        Some(ref target) => target.contains(&lint),
101                        None => false,
102                    };
103
104                    if matches_severity && matches_lints_inc && !matches_lints_exc {
105                        Some(pass)
106                    } else {
107                        None
108                    }
109                })
110                .collect();
111
112            // Initialize the parser and get the AST
113            let mut parser = solar_parse::Parser::from_file(sess, &arena, file)?;
114            let ast = parser.parse_file().map_err(|e| e.emit())?;
115
116            // Initialize and run the visitor
117            let ctx = LintContext::new(sess, self.with_description);
118            let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes };
119            _ = visitor.visit_source_unit(&ast);
120
121            Ok(())
122        });
123    }
124}
125
126impl Linter for SolidityLinter {
127    type Language = SolcLanguage;
128    type Lint = SolLint;
129
130    fn lint(&self, input: &[PathBuf]) {
131        let mut builder = Session::builder();
132
133        // Build session based on the linter config
134        if self.with_json_emitter {
135            let map = Arc::<SourceMap>::default();
136            let json_emitter = JsonEmitter::new(Box::new(std::io::stderr()), map.clone())
137                .rustc_like(true)
138                .ui_testing(false);
139
140            builder = builder.dcx(DiagCtxt::new(Box::new(json_emitter))).source_map(map);
141        } else {
142            builder = builder.with_stderr_emitter();
143        };
144
145        // Create a single session for all files
146        let mut sess = builder.build();
147        sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
148
149        // Process the files in parallel
150        sess.enter_parallel(|| {
151            input.into_par_iter().for_each(|file| {
152                self.process_file(&sess, file);
153            });
154        });
155    }
156}
157
158#[derive(Error, Debug)]
159pub enum SolLintError {
160    #[error("Unknown lint ID: {0}")]
161    InvalidId(String),
162}
163
164#[derive(Debug, Clone, Copy, Eq, PartialEq)]
165pub struct SolLint {
166    id: &'static str,
167    description: &'static str,
168    help: &'static str,
169    severity: Severity,
170}
171
172impl Lint for SolLint {
173    fn id(&self) -> &'static str {
174        self.id
175    }
176    fn severity(&self) -> Severity {
177        self.severity
178    }
179    fn description(&self) -> &'static str {
180        self.description
181    }
182    fn help(&self) -> &'static str {
183        self.help
184    }
185}
186
187impl<'a> TryFrom<&'a str> for SolLint {
188    type Error = SolLintError;
189
190    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
191        for &lint in high::REGISTERED_LINTS {
192            if lint.id() == value {
193                return Ok(lint);
194            }
195        }
196
197        for &lint in med::REGISTERED_LINTS {
198            if lint.id() == value {
199                return Ok(lint);
200            }
201        }
202
203        for &lint in info::REGISTERED_LINTS {
204            if lint.id() == value {
205                return Ok(lint);
206            }
207        }
208
209        for &lint in gas::REGISTERED_LINTS {
210            if lint.id() == value {
211                return Ok(lint);
212            }
213        }
214
215        Err(SolLintError::InvalidId(value.to_string()))
216    }
217}