forge/cmd/
lint.rs

1use clap::{Parser, ValueHint};
2use eyre::{Result, eyre};
3use forge_lint::{
4    linter::Linter,
5    sol::{SolLint, SolLintError, SolidityLinter},
6};
7use foundry_cli::{
8    opts::BuildOpts,
9    utils::{FoundryPathExt, LoadConfig},
10};
11use foundry_common::compile::ProjectCompiler;
12use foundry_compilers::{solc::SolcLanguage, utils::SOLC_EXTENSIONS};
13use foundry_config::{filter::expand_globs, lint::Severity};
14use std::path::PathBuf;
15
16/// CLI arguments for `forge lint`.
17#[derive(Clone, Debug, Parser)]
18pub struct LintArgs {
19    /// Path to the file to be checked. Overrides the `ignore` project config.
20    #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
21    pub(crate) paths: Vec<PathBuf>,
22
23    /// Specifies which lints to run based on severity. Overrides the `severity` project config.
24    ///
25    /// Supported values: `high`, `med`, `low`, `info`, `gas`.
26    #[arg(long, value_name = "SEVERITY", num_args(1..))]
27    pub(crate) severity: Option<Vec<Severity>>,
28
29    /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). Overrides the
30    /// `exclude_lints` project config.
31    #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))]
32    pub(crate) lint: Option<Vec<String>>,
33
34    /// Activates the linter's JSON formatter (rustc-compatible).
35    #[arg(long)]
36    pub(crate) json: bool,
37
38    #[command(flatten)]
39    pub(crate) build: BuildOpts,
40}
41
42foundry_config::impl_figment_convert!(LintArgs, build);
43
44impl LintArgs {
45    pub fn run(self) -> Result<()> {
46        let config = self.load_config()?;
47        let project = config.solar_project()?;
48        let path_config = config.project_paths();
49
50        // Expand ignore globs and canonicalize from the get go
51        let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
52            .iter()
53            .flat_map(foundry_common::fs::canonicalize_path)
54            .collect::<Vec<_>>();
55
56        let cwd = std::env::current_dir()?;
57        let input = match &self.paths[..] {
58            [] => {
59                // Retrieve the project paths, and filter out the ignored ones.
60                config
61                    .project_paths::<SolcLanguage>()
62                    .input_files_iter()
63                    .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
64                    .collect()
65            }
66            paths => {
67                // Override default excluded paths and only lint the input files.
68                let mut inputs = Vec::with_capacity(paths.len());
69                for path in paths {
70                    if path.is_dir() {
71                        inputs
72                            .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
73                    } else if path.is_sol() {
74                        inputs.push(path.to_path_buf());
75                    } else {
76                        warn!("Cannot process path {}", path.display());
77                    }
78                }
79                inputs
80            }
81        };
82
83        if input.is_empty() {
84            sh_println!("Nothing to lint")?;
85            return Ok(());
86        }
87
88        let parse_lints = |lints: &[String]| -> Result<Vec<SolLint>, SolLintError> {
89            lints.iter().map(|s| SolLint::try_from(s.as_str())).collect()
90        };
91
92        // Override default lint config with user-defined lints
93        let (include, exclude) = match &self.lint {
94            Some(cli_lints) => (Some(parse_lints(cli_lints)?), None),
95            None => (None, Some(parse_lints(&config.lint.exclude_lints)?)),
96        };
97
98        // Override default severity config with user-defined severity
99        let severity = self.severity.unwrap_or(config.lint.severity.clone());
100
101        if project.compiler.solc.is_none() {
102            return Err(eyre!("Linting not supported for this language"));
103        }
104
105        let linter = SolidityLinter::new(path_config)
106            .with_json_emitter(self.json)
107            .with_description(true)
108            .with_lints(include)
109            .without_lints(exclude)
110            .with_severity(if severity.is_empty() { None } else { Some(severity) })
111            .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
112
113        let mut output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?;
114        let compiler = output.parser_mut().solc_mut().compiler_mut();
115        linter.lint(&input, config.deny, compiler)?;
116
117        Ok(())
118    }
119}