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, severity) = match &self.lint {
91 Some(cli_lints) => (Some(parse_lints(cli_lints)?), None, vec![]),
92 None => {
93 let severity = self.severity.clone().unwrap_or(config.lint.severity.clone());
94 (None, Some(parse_lints(&config.lint.exclude_lints)?), severity)
95 }
96 };
97
98 if project.compiler.solc.is_none() {
99 return Err(eyre!("linting not supported for this language"));
100 }
101
102 let linter = SolidityLinter::new(path_config)
103 .with_json_emitter(shell::is_json())
104 .with_description(true)
105 .with_lints(include)
106 .without_lints(exclude)
107 .with_severity(if severity.is_empty() { None } else { Some(severity) })
108 .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions);
109
110 let output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?;
111 let solar_sources = get_solar_sources_from_compile_output(&config, &output, Some(&input))?;
112 if solar_sources.input.sources.is_empty() {
113 return Err(eyre!(
114 "unable to lint. Solar only supports Solidity versions prior to 0.8.0"
115 ));
116 }
117
118 let mut compiler = solar::sema::Compiler::new(
121 solar::interface::Session::builder().with_stderr_emitter().build(),
122 );
123
124 compiler.enter_mut(|compiler| {
126 let mut pcx = compiler.parse();
127 pcx.set_resolve_imports(true);
128 configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true);
129 pcx.parse();
130 });
131 linter.lint(&input, config.deny, &mut compiler)?;
132
133 Ok(())
134 }
135}