Skip to main content

forge/cmd/
lint.rs

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