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::{
14    SkipBuildFilters,
15    filter::{expand_globs, is_ignored_path},
16    lint::Severity,
17};
18use std::path::PathBuf;
19
20/// CLI arguments for `forge lint`.
21#[derive(Clone, Debug, Parser)]
22pub struct LintArgs {
23    /// Path to the file to be checked. Overrides the `ignore` project config.
24    #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
25    pub(crate) paths: Vec<PathBuf>,
26
27    /// Specifies which lints to run based on severity. Overrides the `severity` project config.
28    ///
29    /// Supported values: `high`, `med`, `low`, `info`, `gas`.
30    #[arg(long, value_name = "SEVERITY", num_args(1..))]
31    pub(crate) severity: Option<Vec<Severity>>,
32
33    /// Specifies which lints to run based on their ID (e.g., "incorrect-shift"). Overrides the
34    /// `exclude_lints` project config.
35    #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))]
36    pub(crate) lint: Option<Vec<String>>,
37
38    /// Report inline suppression comments (e.g. `// forge-lint: disable-next-line(...)`) that did
39    /// not suppress any diagnostic during the run.
40    #[arg(long)]
41    pub(crate) report_unused_suppressions: bool,
42
43    #[command(flatten)]
44    pub(crate) build: BuildOpts,
45}
46
47foundry_config::impl_figment_convert!(LintArgs, build);
48
49impl LintArgs {
50    pub async fn run(self) -> Result<()> {
51        let format_json = shell::is_json();
52        let config = self.load_config_with_dependencies()?;
53
54        let project = config.ephemeral_project()?;
55        let path_config = config.project_paths();
56
57        // Expand ignore globs and canonicalize from the get go
58        let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
59            .iter()
60            .flat_map(foundry_common::fs::canonicalize_path)
61            .collect::<Vec<_>>();
62
63        let cwd = std::env::current_dir()?;
64        let mut input = match &self.paths[..] {
65            [] => {
66                // Retrieve the project paths, and filter out the ignored ones.
67                config
68                    .project_paths::<SolcLanguage>()
69                    .input_files_iter()
70                    .filter(|p| !is_ignored_path(p, &ignored, &cwd))
71                    .collect()
72            }
73            paths => {
74                // Override default excluded paths and only lint the input files.
75                let mut inputs = Vec::with_capacity(paths.len());
76                for path in paths {
77                    if path.is_dir() {
78                        inputs
79                            .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
80                    } else if path.is_sol() {
81                        inputs.push(path.clone());
82                    } else {
83                        warn!("cannot process path {}", path.display());
84                    }
85                }
86                inputs
87            }
88        };
89        let skip = SkipBuildFilters::new(config.skip.clone(), config.root.clone());
90        input.retain(|path| skip.is_match(path));
91
92        if input.is_empty() {
93            sh_status!("nothing to lint")?;
94            return Ok(());
95        }
96
97        let parse_lints = |lints: &[String]| -> Result<Vec<SolLint>, SolLintError> {
98            lints.iter().map(|s| SolLint::try_from(s.as_str())).collect()
99        };
100
101        // Override default lint config with user-defined lints
102        // When --only-lint is used, bypass the severity filter by setting it to None
103        let (include, exclude, severity) = match &self.lint {
104            Some(cli_lints) => (Some(parse_lints(cli_lints)?), None, vec![]),
105            None => {
106                let severity = self.severity.clone().unwrap_or(config.lint.severity.clone());
107                (None, Some(parse_lints(&config.lint.exclude_lints)?), severity)
108            }
109        };
110
111        if project.compiler.solc.is_none() {
112            return Err(eyre!("linting not supported for this language"));
113        }
114
115        let linter = SolidityLinter::new(path_config)
116            .with_json_emitter(format_json)
117            .with_json_emitter_stdout(format_json)
118            .with_description(true)
119            .with_lints(include)
120            .without_lints(exclude)
121            .with_severity(if severity.is_empty() { None } else { Some(severity) })
122            .with_report_unused_suppressions(self.report_unused_suppressions)
123            .with_lint_specific(&config.lint.lint_specific);
124
125        let mut opts = solar::interface::config::CompileOpts::default();
126        if format_json {
127            opts.error_format = solar::interface::config::ErrorFormat::RustcJson;
128        }
129        let session = solar::interface::Session::builder().opts(opts);
130        let session =
131            if format_json { session.build() } else { session.with_stderr_emitter().build() };
132        if format_json {
133            let writer = Box::new(std::io::BufWriter::new(std::io::stdout()));
134            let emitter = solar::interface::diagnostics::JsonEmitter::new(
135                writer,
136                session.clone_source_map(),
137                solar::interface::ColorChoice::Never,
138            )
139            .rustc_like(true);
140            session.dcx.set_emitter(Box::new(emitter));
141        }
142        let mut compiler = solar::sema::Compiler::new(session);
143
144        // Load the solar-compatible sources to the pcx before linting
145        let has_sources = compiler.enter_mut(|compiler| -> Result<bool> {
146            let mut pcx = compiler.parse();
147            pcx.set_resolve_imports(true);
148            let has_sources =
149                configure_pcx_all_sources(&mut pcx, &config, Some(&project), Some(&input))?;
150            pcx.parse();
151            Ok(has_sources)
152        })?;
153        if !has_sources {
154            return Err(eyre!("unable to lint. Solar only supports Solidity versions >=0.8.0"));
155        }
156        if let Err(err) = linter.lint(&input, config.deny, &mut compiler) {
157            if format_json && compiler.dcx().has_errors().is_err() {
158                // Solar already emitted the error, so bypass the top-level error printer.
159                std::process::exit(1);
160            }
161            return Err(err);
162        }
163
164        Ok(())
165    }
166}