Skip to main content

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_all_sources},
9    utils::{FoundryPathExt, LoadConfig},
10};
11use foundry_common::shell;
12use foundry_compilers::{FileFilter, solc::SolcLanguage, utils::SOLC_EXTENSIONS};
13use foundry_config::{SkipBuildFilters, 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 format_json = shell::is_json();
43        let config = self.load_config()?;
44        let project = config.ephemeral_project()?;
45        let path_config = config.project_paths();
46
47        // Expand ignore globs and canonicalize from the get go
48        let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
49            .iter()
50            .flat_map(foundry_common::fs::canonicalize_path)
51            .collect::<Vec<_>>();
52
53        let cwd = std::env::current_dir()?;
54        let mut input = match &self.paths[..] {
55            [] => {
56                // Retrieve the project paths, and filter out the ignored ones.
57                config
58                    .project_paths::<SolcLanguage>()
59                    .input_files_iter()
60                    .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
61                    .collect()
62            }
63            paths => {
64                // Override default excluded paths and only lint the input files.
65                let mut inputs = Vec::with_capacity(paths.len());
66                for path in paths {
67                    if path.is_dir() {
68                        inputs
69                            .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
70                    } else if path.is_sol() {
71                        inputs.push(path.clone());
72                    } else {
73                        warn!("cannot process path {}", path.display());
74                    }
75                }
76                inputs
77            }
78        };
79        let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
80        input.retain(|path| skip.is_match(path));
81
82        if input.is_empty() {
83            sh_status!("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        // When --only-lint is used, bypass the severity filter by setting it to None
93        let (include, exclude, severity) = match &self.lint {
94            Some(cli_lints) => (Some(parse_lints(cli_lints)?), None, vec![]),
95            None => {
96                let severity = self.severity.clone().unwrap_or(config.lint.severity.clone());
97                (None, Some(parse_lints(&config.lint.exclude_lints)?), severity)
98            }
99        };
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(format_json)
107            .with_json_emitter_stdout(format_json)
108            .with_description(true)
109            .with_lints(include)
110            .without_lints(exclude)
111            .with_severity(if severity.is_empty() { None } else { Some(severity) })
112            .with_lint_specific(&config.lint.lint_specific);
113
114        let mut opts = solar::interface::config::CompileOpts::default();
115        opts.unstable.typeck = true;
116        if format_json {
117            opts.error_format = solar::interface::config::ErrorFormat::RustcJson;
118        }
119        let session = solar::interface::Session::builder().opts(opts);
120        let session =
121            if format_json { session.build() } else { session.with_stderr_emitter().build() };
122        if format_json {
123            let writer = Box::new(std::io::BufWriter::new(std::io::stdout()));
124            let emitter =
125                solar::interface::diagnostics::JsonEmitter::new(writer, session.clone_source_map())
126                    .rustc_like(true);
127            session.dcx.set_emitter(Box::new(emitter));
128        }
129        let mut compiler = solar::sema::Compiler::new(session);
130
131        // Load the solar-compatible sources to the pcx before linting
132        let has_sources = compiler.enter_mut(|compiler| -> Result<bool> {
133            let mut pcx = compiler.parse();
134            pcx.set_resolve_imports(true);
135            let has_sources =
136                configure_pcx_all_sources(&mut pcx, &config, Some(&project), Some(&input))?;
137            pcx.parse();
138            Ok(has_sources)
139        })?;
140        if !has_sources {
141            return Err(eyre!("unable to lint. Solar only supports Solidity versions >=0.8.0"));
142        }
143        if let Err(err) = linter.lint(&input, config.deny, &mut compiler) {
144            if format_json && compiler.dcx().has_errors().is_err() {
145                // Solar already emitted the error, so bypass the top-level error printer.
146                std::process::exit(1);
147            }
148            return Err(err);
149        }
150
151        Ok(())
152    }
153}