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#[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 #[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 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 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 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 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 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 std::process::exit(1);
147 }
148 return Err(err);
149 }
150
151 Ok(())
152 }
153}