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_from_solc, get_solar_sources_from_compile_output},
9 utils::{FoundryPathExt, LoadConfig},
10};
11use foundry_common::{compile::ProjectCompiler, shell};
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 #[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 config = self.load_config()?;
43 let project = config.solar_project()?;
44 let path_config = config.project_paths();
45
46 let ignored = expand_globs(&config.root, config.lint.ignore.iter())?
48 .iter()
49 .flat_map(foundry_common::fs::canonicalize_path)
50 .collect::<Vec<_>>();
51
52 let cwd = std::env::current_dir()?;
53 let input = match &self.paths[..] {
54 [] => {
55 config
57 .project_paths::<SolcLanguage>()
58 .input_files_iter()
59 .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
60 .collect()
61 }
62 paths => {
63 let mut inputs = Vec::with_capacity(paths.len());
65 for path in paths {
66 if path.is_dir() {
67 inputs
68 .extend(foundry_compilers::utils::source_files(path, SOLC_EXTENSIONS));
69 } else if path.is_sol() {
70 inputs.push(path.to_path_buf());
71 } else {
72 warn!("cannot process path {}", path.display());
73 }
74 }
75 inputs
76 }
77 };
78
79 if input.is_empty() {
80 sh_println!("nothing to lint")?;
81 return Ok(());
82 }
83
84 let parse_lints = |lints: &[String]| -> Result<Vec<SolLint>, SolLintError> {
85 lints.iter().map(|s| SolLint::try_from(s.as_str())).collect()
86 };
87
88 let (include, exclude) = match &self.lint {
90 Some(cli_lints) => (Some(parse_lints(cli_lints)?), None),
91 None => (None, Some(parse_lints(&config.lint.exclude_lints)?)),
92 };
93
94 let severity = self.severity.unwrap_or(config.lint.severity.clone());
96
97 if project.compiler.solc.is_none() {
98 return Err(eyre!("linting not supported for this language"));
99 }
100
101 let linter = SolidityLinter::new(path_config)
102 .with_json_emitter(shell::is_json())
103 .with_description(true)
104 .with_lints(include)
105 .without_lints(exclude)
106 .with_severity(if severity.is_empty() { None } else { Some(severity) })
107 .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
108
109 let output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?;
110 let solar_sources = get_solar_sources_from_compile_output(&config, &output, Some(&input))?;
111 if solar_sources.input.sources.is_empty() {
112 return Err(eyre!(
113 "unable to lint. Solar only supports Solidity versions prior to 0.8.0"
114 ));
115 }
116
117 let mut compiler = solar::sema::Compiler::new(
120 solar::interface::Session::builder().with_stderr_emitter().build(),
121 );
122
123 compiler.enter_mut(|compiler| {
125 let mut pcx = compiler.parse();
126 pcx.set_resolve_imports(true);
127 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
128 pcx.parse();
129 });
130 linter.lint(&input, config.deny, &mut compiler)?;
131
132 Ok(())
133 }
134}