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#[derive(Clone, Debug, Parser)]
22pub struct LintArgs {
23 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
25 pub(crate) paths: Vec<PathBuf>,
26
27 #[arg(long, value_name = "SEVERITY", num_args(1..))]
31 pub(crate) severity: Option<Vec<Severity>>,
32
33 #[arg(long = "only-lint", value_name = "LINT_ID", num_args(1..))]
36 pub(crate) lint: Option<Vec<String>>,
37
38 #[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 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 config
68 .project_paths::<SolcLanguage>()
69 .input_files_iter()
70 .filter(|p| !is_ignored_path(p, &ignored, &cwd))
71 .collect()
72 }
73 paths => {
74 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 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 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 std::process::exit(1);
160 }
161 return Err(err);
162 }
163
164 Ok(())
165 }
166}