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,
9 utils::{FoundryPathExt, LoadConfig},
10};
11use foundry_common::compile::ProjectCompiler;
12use foundry_compilers::{solc::SolcLanguage, utils::SOLC_EXTENSIONS};
13use foundry_config::{filter::expand_globs, lint::Severity};
14use std::path::PathBuf;
15
16#[derive(Clone, Debug, Parser)]
18pub struct LintArgs {
19 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
21 pub(crate) paths: Vec<PathBuf>,
22
23 #[arg(long, value_name = "SEVERITY", num_args(1..))]
27 pub(crate) severity: Option<Vec<Severity>>,
28
29 #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))]
32 pub(crate) lint: Option<Vec<String>>,
33
34 #[arg(long)]
36 pub(crate) json: bool,
37
38 #[command(flatten)]
39 pub(crate) build: BuildOpts,
40}
41
42foundry_config::impl_figment_convert!(LintArgs, build);
43
44impl LintArgs {
45 pub fn run(self) -> Result<()> {
46 let config = self.load_config()?;
47 let project = config.solar_project()?;
48 let path_config = config.project_paths();
49
50 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 config
61 .project_paths::<SolcLanguage>()
62 .input_files_iter()
63 .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
64 .collect()
65 }
66 paths => {
67 let mut inputs = Vec::with_capacity(paths.len());
69 for path in paths {
70 if path.is_dir() {
71 inputs
72 .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
73 } else if path.is_sol() {
74 inputs.push(path.to_path_buf());
75 } else {
76 warn!("Cannot process path {}", path.display());
77 }
78 }
79 inputs
80 }
81 };
82
83 if input.is_empty() {
84 sh_println!("Nothing to lint")?;
85 return Ok(());
86 }
87
88 let parse_lints = |lints: &[String]| -> Result<Vec<SolLint>, SolLintError> {
89 lints.iter().map(|s| SolLint::try_from(s.as_str())).collect()
90 };
91
92 let (include, exclude) = match &self.lint {
94 Some(cli_lints) => (Some(parse_lints(cli_lints)?), None),
95 None => (None, Some(parse_lints(&config.lint.exclude_lints)?)),
96 };
97
98 let severity = self.severity.unwrap_or(config.lint.severity.clone());
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(self.json)
107 .with_description(true)
108 .with_lints(include)
109 .without_lints(exclude)
110 .with_severity(if severity.is_empty() { None } else { Some(severity) })
111 .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
112
113 let mut output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?;
114 let compiler = output.parser_mut().solc_mut().compiler_mut();
115 linter.lint(&input, config.deny, compiler)?;
116
117 Ok(())
118 }
119}