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, configure_pcx_from_solc, get_solar_sources_from_compile_output},
9    utils::{FoundryPathExt, LoadConfig},
10};
11use foundry_common::{compile::ProjectCompiler, shell};
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    #[command(flatten)]
35    pub(crate) build: BuildOpts,
36}
37
38foundry_config::impl_figment_convert!(LintArgs, build);
39
40impl LintArgs {
41    pub fn run(self) -> Result<()> {
42        let config = self.load_config()?;
43        let project = config.solar_project()?;
44        let path_config = config.project_paths();
45
46        // Expand ignore globs and canonicalize from the get go
47        let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
48            .iter()
49            .flat_map(foundry_common::fs::canonicalize_path)
50            .collect::<Vec<_>>();
51
52        let cwd = std::env::current_dir()?;
53        let input = match &self.paths[..] {
54            [] => {
55                // Retrieve the project paths, and filter out the ignored ones.
56                config
57                    .project_paths::<SolcLanguage>()
58                    .input_files_iter()
59                    .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
60                    .collect()
61            }
62            paths => {
63                // Override default excluded paths and only lint the input files.
64                let mut inputs = Vec::with_capacity(paths.len());
65                for path in paths {
66                    if path.is_dir() {
67                        inputs
68                            .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
69                    } else if path.is_sol() {
70                        inputs.push(path.to_path_buf());
71                    } else {
72                        warn!("cannot process path {}", path.display());
73                    }
74                }
75                inputs
76            }
77        };
78
79        if input.is_empty() {
80            sh_println!("nothing to lint")?;
81            return Ok(());
82        }
83
84        let parse_lints = |lints: &[String]| -> Result<Vec<SolLint>, SolLintError> {
85            lints.iter().map(|s| SolLint::try_from(s.as_str())).collect()
86        };
87
88        // Override default lint config with user-defined lints
89        let (include, exclude) = match &self.lint {
90            Some(cli_lints) => (Some(parse_lints(cli_lints)?), None),
91            None => (None, Some(parse_lints(&config.lint.exclude_lints)?)),
92        };
93
94        // Override default severity config with user-defined severity
95        let severity = self.severity.unwrap_or(config.lint.severity.clone());
96
97        if project.compiler.solc.is_none() {
98            return Err(eyre!("linting not supported for this language"));
99        }
100
101        let linter = SolidityLinter::new(path_config)
102            .with_json_emitter(shell::is_json())
103            .with_description(true)
104            .with_lints(include)
105            .without_lints(exclude)
106            .with_severity(if severity.is_empty() { None } else { Some(severity) })
107            .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
108
109        let output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?;
110        let solar_sources = get_solar_sources_from_compile_output(&config, &output, Some(&input))?;
111        if solar_sources.input.sources.is_empty() {
112            return Err(eyre!(
113                "unable to lint. Solar only supports Solidity versions prior to 0.8.0"
114            ));
115        }
116
117        // NOTE(rusowsky): Once solar can drop unsupported versions, rather than creating a new
118        // compiler, we should reuse the parser from the project output.
119        let mut compiler = solar::sema::Compiler::new(
120            solar::interface::Session::builder().with_stderr_emitter().build(),
121        );
122
123        // Load the solar-compatible sources to the pcx before linting
124        compiler.enter_mut(|compiler| {
125            let mut pcx = compiler.parse();
126            pcx.set_resolve_imports(true);
127            configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
128            pcx.parse();
129        });
130        linter.lint(&input, config.deny, &mut compiler)?;
131
132        Ok(())
133    }
134}