forge/cmd/
lint.rs

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