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