Skip to main content

forge_lint/sol/
mod.rs

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