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: Vec<Box<dyn EarlyLintPass<'_>>> = passes_and_lints
119            .into_iter()
120            .filter_map(|(pass, lint)| if self.include_lint(lint) { Some(pass) } else { None })
121            .collect();
122
123        // Process the inline-config
124        let comments = Comments::new(file);
125        let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Ast(ast));
126
127        // Initialize and run the early lint visitor
128        let ctx = LintContext::new(sess, self.with_description, inline_config);
129        let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes);
130        _ = early_visitor.visit_source_unit(ast);
131        early_visitor.post_source_unit(ast);
132
133        Ok(())
134    }
135
136    fn process_source_hir<'hir>(
137        &self,
138        sess: &Session,
139        gcx: &solar_sema::ty::Gcx<'hir>,
140        source_id: hir::SourceId,
141        file: &'hir SourceFile,
142    ) -> Result<(), diagnostics::ErrorGuaranteed> {
143        // Declare all available passes and lints
144        let mut passes_and_lints = Vec::new();
145        passes_and_lints.extend(high::create_late_lint_passes());
146        passes_and_lints.extend(med::create_late_lint_passes());
147        passes_and_lints.extend(info::create_late_lint_passes());
148
149        // Do not apply gas-severity rules on tests and scripts
150        if let FileName::Real(ref path) = file.name
151            && !self.path_config.is_test_or_script(path)
152        {
153            passes_and_lints.extend(gas::create_late_lint_passes());
154        }
155
156        // Filter passes based on config
157        let mut passes: Vec<Box<dyn LateLintPass<'_>>> = passes_and_lints
158            .into_iter()
159            .filter_map(|(pass, lint)| if self.include_lint(lint) { Some(pass) } else { None })
160            .collect();
161
162        // Process the inline-config
163        let comments = Comments::new(file);
164        let inline_config =
165            parse_inline_config(sess, &comments, InlineConfigSource::Hir((&gcx.hir, source_id)));
166
167        // Run late lint visitor
168        let ctx = LintContext::new(sess, self.with_description, inline_config);
169        let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
170
171        // Visit this specific source
172        _ = late_visitor.visit_nested_source(source_id);
173
174        Ok(())
175    }
176}
177
178impl Linter for SolidityLinter {
179    type Language = SolcLanguage;
180    type Lint = SolLint;
181
182    /// Build solar session based on the linter config
183    fn init(&self) -> Session {
184        let mut builder = Session::builder();
185        if self.with_json_emitter {
186            let map = Arc::<SourceMap>::default();
187            let json_emitter = JsonEmitter::new(Box::new(std::io::stderr()), map.clone())
188                .rustc_like(true)
189                .ui_testing(false);
190
191            builder = builder.dcx(DiagCtxt::new(Box::new(json_emitter))).source_map(map);
192        } else {
193            builder = builder.with_stderr_emitter();
194        };
195
196        // Create a single session for all files
197        let mut sess = builder.build();
198        sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
199        sess
200    }
201
202    /// Run AST-based lints
203    fn early_lint<'sess>(&self, input: &[PathBuf], mut pcx: ParsingContext<'sess>) {
204        let sess = pcx.sess;
205        _ = sess.enter_parallel(|| -> Result<(), diagnostics::ErrorGuaranteed> {
206            // Load all files into the parsing ctx
207            pcx.load_files(input)?;
208
209            // Parse the sources
210            let ast_arena = solar_sema::thread_local::ThreadLocal::new();
211            let ast_result = pcx.parse(&ast_arena);
212
213            // Process each source in parallel
214            ast_result.sources.iter().par_bridge().for_each(|source| {
215                if let (FileName::Real(path), Some(ast)) = (&source.file.name, &source.ast)
216                    && input.iter().any(|input_path| path.ends_with(input_path))
217                {
218                    _ = self.process_source_ast(sess, ast, &source.file, path)
219                }
220            });
221
222            Ok(())
223        });
224    }
225
226    /// Run HIR-based lints
227    fn late_lint<'sess>(&self, input: &[PathBuf], mut pcx: ParsingContext<'sess>) {
228        let sess = pcx.sess;
229        _ = sess.enter_parallel(|| -> Result<(), diagnostics::ErrorGuaranteed> {
230            // Load all files into the parsing ctx
231            pcx.load_files(input)?;
232
233            // Parse and lower to HIR
234            let hir_arena = solar_sema::thread_local::ThreadLocal::new();
235            let hir_result = pcx.parse_and_lower(&hir_arena);
236
237            if let Ok(Some(gcx_wrapper)) = hir_result {
238                let gcx = gcx_wrapper.get();
239
240                // Process each source in parallel
241                gcx.hir.sources_enumerated().par_bridge().for_each(|(source_id, source)| {
242                    if let FileName::Real(ref path) = source.file.name
243                        && input.iter().any(|input_path| path.ends_with(input_path))
244                    {
245                        _ = self.process_source_hir(sess, &gcx, source_id, &source.file);
246                    }
247                });
248            }
249
250            Ok(())
251        });
252    }
253}
254
255enum InlineConfigSource<'ast, 'hir> {
256    Ast(&'ast ast::SourceUnit<'ast>),
257    Hir((&'hir hir::Hir<'hir>, hir::SourceId)),
258}
259
260fn parse_inline_config<'ast, 'hir>(
261    sess: &Session,
262    comments: &Comments,
263    source: InlineConfigSource<'ast, 'hir>,
264) -> InlineConfig {
265    let items = comments.iter().filter_map(|comment| {
266        let mut item = comment.lines.first()?.as_str();
267        if let Some(prefix) = comment.prefix() {
268            item = item.strip_prefix(prefix).unwrap_or(item);
269        }
270        if let Some(suffix) = comment.suffix() {
271            item = item.strip_suffix(suffix).unwrap_or(item);
272        }
273        let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
274        let span = comment.span;
275        match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
276            Ok(item) => Some((span, item)),
277            Err(e) => {
278                sess.dcx.warn(e.to_string()).span(span).emit();
279                None
280            }
281        }
282    });
283
284    match source {
285        InlineConfigSource::Ast(ast) => InlineConfig::from_ast(items, ast, sess.source_map()),
286        InlineConfigSource::Hir((hir, id)) => {
287            InlineConfig::from_hir(items, hir, id, sess.source_map())
288        }
289    }
290}
291
292#[derive(Error, Debug)]
293pub enum SolLintError {
294    #[error("Unknown lint ID: {0}")]
295    InvalidId(String),
296}
297
298#[derive(Debug, Clone, Copy, Eq, PartialEq)]
299pub struct SolLint {
300    id: &'static str,
301    description: &'static str,
302    help: &'static str,
303    severity: Severity,
304}
305
306impl Lint for SolLint {
307    fn id(&self) -> &'static str {
308        self.id
309    }
310    fn severity(&self) -> Severity {
311        self.severity
312    }
313    fn description(&self) -> &'static str {
314        self.description
315    }
316    fn help(&self) -> &'static str {
317        self.help
318    }
319}
320
321impl<'a> TryFrom<&'a str> for SolLint {
322    type Error = SolLintError;
323
324    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
325        for &lint in high::REGISTERED_LINTS {
326            if lint.id() == value {
327                return Ok(lint);
328            }
329        }
330
331        for &lint in med::REGISTERED_LINTS {
332            if lint.id() == value {
333                return Ok(lint);
334            }
335        }
336
337        for &lint in info::REGISTERED_LINTS {
338            if lint.id() == value {
339                return Ok(lint);
340            }
341        }
342
343        for &lint in gas::REGISTERED_LINTS {
344            if lint.id() == value {
345                return Ok(lint);
346            }
347        }
348
349        Err(SolLintError::InvalidId(value.to_string()))
350    }
351}