1use crate::linter::{
2 EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter,
3 LinterConfig,
4};
5use foundry_common::{
6 comments::{
7 Comments,
8 inline_config::{InlineConfig, InlineConfigItem},
9 },
10 errors::convert_solar_errors,
11 sh_warn,
12};
13use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage};
14use foundry_config::{DenyLevel, lint::Severity};
15use rayon::prelude::*;
16use solar::{
17 ast::{self as ast, visit::Visit as _},
18 interface::{
19 Session,
20 diagnostics::{self, HumanEmitter, JsonEmitter},
21 },
22 sema::{
23 Compiler, Gcx,
24 hir::{self, Visit as _},
25 },
26};
27use std::{
28 path::{Path, PathBuf},
29 sync::LazyLock,
30};
31use thiserror::Error;
32
33#[macro_use]
34pub mod macros;
35
36pub mod codesize;
37pub mod gas;
38pub mod high;
39pub mod info;
40pub mod med;
41
42static ALL_REGISTERED_LINTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
43 let mut lints = Vec::new();
44 lints.extend_from_slice(high::REGISTERED_LINTS);
45 lints.extend_from_slice(med::REGISTERED_LINTS);
46 lints.extend_from_slice(info::REGISTERED_LINTS);
47 lints.extend_from_slice(gas::REGISTERED_LINTS);
48 lints.extend_from_slice(codesize::REGISTERED_LINTS);
49 lints.into_iter().map(|lint| lint.id()).collect()
50});
51
52#[derive(Debug)]
55pub struct SolidityLinter<'a> {
56 path_config: ProjectPathsConfig,
57 severity: Option<Vec<Severity>>,
58 lints_included: Option<Vec<SolLint>>,
59 lints_excluded: Option<Vec<SolLint>>,
60 with_description: bool,
61 with_json_emitter: bool,
62 mixed_case_exceptions: &'a [String],
64}
65
66impl<'a> SolidityLinter<'a> {
67 pub fn new(path_config: ProjectPathsConfig) -> Self {
68 Self {
69 path_config,
70 with_description: true,
71 severity: None,
72 lints_included: None,
73 lints_excluded: None,
74 with_json_emitter: false,
75 mixed_case_exceptions: &[],
76 }
77 }
78
79 pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
80 self.severity = severity;
81 self
82 }
83
84 pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
85 self.lints_included = lints;
86 self
87 }
88
89 pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
90 self.lints_excluded = lints;
91 self
92 }
93
94 pub fn with_description(mut self, with: bool) -> Self {
95 self.with_description = with;
96 self
97 }
98
99 pub fn with_json_emitter(mut self, with: bool) -> Self {
100 self.with_json_emitter = with;
101 self
102 }
103
104 pub fn with_mixed_case_exceptions(mut self, exceptions: &'a [String]) -> Self {
105 self.mixed_case_exceptions = exceptions;
106 self
107 }
108
109 fn config(&'a self, inline: &'a InlineConfig<Vec<String>>) -> LinterConfig<'a> {
110 LinterConfig { inline, mixed_case_exceptions: self.mixed_case_exceptions }
111 }
112
113 fn include_lint(&self, lint: SolLint) -> bool {
114 self.severity.as_ref().is_none_or(|sev| sev.contains(&lint.severity()))
115 && self.lints_included.as_ref().is_none_or(|incl| incl.contains(&lint))
116 && !self.lints_excluded.as_ref().is_some_and(|excl| excl.contains(&lint))
117 }
118
119 fn process_source_ast<'gcx>(
120 &self,
121 sess: &'gcx Session,
122 ast: &'gcx ast::SourceUnit<'gcx>,
123 path: &Path,
124 inline_config: &InlineConfig<Vec<String>>,
125 ) -> Result<(), diagnostics::ErrorGuaranteed> {
126 let mut passes_and_lints = Vec::new();
128 passes_and_lints.extend(high::create_early_lint_passes());
129 passes_and_lints.extend(med::create_early_lint_passes());
130 passes_and_lints.extend(info::create_early_lint_passes());
131
132 if !self.path_config.is_test_or_script(path) {
134 passes_and_lints.extend(gas::create_early_lint_passes());
135 passes_and_lints.extend(codesize::create_early_lint_passes());
136 }
137
138 let (mut passes, lints): (Vec<Box<dyn EarlyLintPass<'_>>>, Vec<_>) = passes_and_lints
140 .into_iter()
141 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
142 let included_ids: Vec<_> = lints
143 .iter()
144 .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
145 .collect();
146
147 if !included_ids.is_empty() {
148 passes.push(pass);
149 ids.extend(included_ids);
150 }
151
152 (passes, ids)
153 });
154
155 let ctx = LintContext::new(
157 sess,
158 self.with_description,
159 self.with_json_emitter,
160 self.config(inline_config),
161 lints,
162 );
163 let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes);
164 _ = early_visitor.visit_source_unit(ast);
165 early_visitor.post_source_unit(ast);
166
167 Ok(())
168 }
169
170 fn process_source_hir<'gcx>(
171 &self,
172 gcx: Gcx<'gcx>,
173 source_id: hir::SourceId,
174 path: &Path,
175 inline_config: &InlineConfig<Vec<String>>,
176 ) -> Result<(), diagnostics::ErrorGuaranteed> {
177 let mut passes_and_lints = Vec::new();
179 passes_and_lints.extend(high::create_late_lint_passes());
180 passes_and_lints.extend(med::create_late_lint_passes());
181 passes_and_lints.extend(info::create_late_lint_passes());
182
183 if !self.path_config.is_test_or_script(path) {
185 passes_and_lints.extend(gas::create_late_lint_passes());
186 passes_and_lints.extend(codesize::create_late_lint_passes());
187 }
188
189 let (mut passes, lints): (Vec<Box<dyn LateLintPass<'_>>>, Vec<_>) = passes_and_lints
191 .into_iter()
192 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
193 let included_ids: Vec<_> = lints
194 .iter()
195 .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
196 .collect();
197
198 if !included_ids.is_empty() {
199 passes.push(pass);
200 ids.extend(included_ids);
201 }
202
203 (passes, ids)
204 });
205
206 let ctx = LintContext::new(
208 gcx.sess,
209 self.with_description,
210 self.with_json_emitter,
211 self.config(inline_config),
212 lints,
213 );
214 let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
215
216 let _ = late_visitor.visit_nested_source(source_id);
218
219 Ok(())
220 }
221}
222
223impl<'a> Linter for SolidityLinter<'a> {
224 type Language = SolcLanguage;
225 type Lint = SolLint;
226
227 fn lint(
228 &self,
229 input: &[PathBuf],
230 deny: DenyLevel,
231 compiler: &mut Compiler,
232 ) -> eyre::Result<()> {
233 convert_solar_errors(compiler.dcx())?;
234
235 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
236
237 let sm = compiler.sess().clone_source_map();
238 let prev_emitter = compiler.dcx().set_emitter(if self.with_json_emitter {
239 let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
240 let json_emitter = JsonEmitter::new(writer, sm).rustc_like(true).ui_testing(ui_testing);
241 Box::new(json_emitter)
242 } else {
243 Box::new(HumanEmitter::stderr(Default::default()).source_map(Some(sm)))
244 });
245 let sess = compiler.sess_mut();
246 sess.dcx.set_flags_mut(|f| f.track_diagnostics = false);
247 if ui_testing {
248 sess.opts.unstable.ui_testing = true;
249 sess.reconfigure();
250 }
251
252 compiler.enter_mut(|compiler| -> eyre::Result<()> {
253 if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
254 let _ = compiler.lower_asts();
255 }
256
257 let gcx = compiler.gcx();
258
259 input.par_iter().for_each(|path| {
260 let path = &self.path_config.root.join(path);
261 let Some((_, ast_source)) = gcx.get_ast_source(path) else {
262 _ = sh_warn!("AST source not found for {}", path.display());
265 return;
266 };
267 let Some(ast) = &ast_source.ast else {
268 panic!("AST missing for {}", path.display());
269 };
270
271 let file = &ast_source.file;
273 let comments = Comments::new(file, gcx.sess.source_map(), false, false, None);
274 let inline_config = parse_inline_config(gcx.sess, &comments, ast);
275
276 let _ = self.process_source_ast(gcx.sess, ast, path, &inline_config);
278
279 let Some((hir_source_id, _)) = gcx.get_hir_source(path) else {
281 panic!("HIR source not found for {}", path.display());
282 };
283 let _ = self.process_source_hir(gcx, hir_source_id, path, &inline_config);
284 });
285
286 convert_solar_errors(compiler.dcx())
287 })?;
288
289 let sess = compiler.sess_mut();
290 sess.dcx.set_emitter(prev_emitter);
291 if ui_testing {
292 sess.opts.unstable.ui_testing = false;
293 sess.reconfigure();
294 }
295
296 const MSG: &str = "aborting due to ";
298 match (deny, compiler.dcx().warn_count(), compiler.dcx().note_count()) {
299 (DenyLevel::Warnings, w, n) if w > 0 => {
301 if n > 0 {
302 Err(eyre::eyre!("{MSG}{w} linter warning(s); {n} note(s) were also emitted\n"))
303 } else {
304 Err(eyre::eyre!("{MSG}{w} linter warning(s)\n"))
305 }
306 }
307
308 (DenyLevel::Notes, w, n) if w > 0 || n > 0 => match (w, n) {
310 (w, n) if w > 0 && n > 0 => {
311 Err(eyre::eyre!("{MSG}{w} linter warning(s) and {n} note(s)\n"))
312 }
313 (w, 0) => Err(eyre::eyre!("{MSG}{w} linter warning(s)\n")),
314 (0, n) => Err(eyre::eyre!("{MSG}{n} linter note(s)\n")),
315 _ => unreachable!(),
316 },
317
318 _ => Ok(()),
320 }
321 }
322}
323
324fn parse_inline_config<'ast>(
325 sess: &Session,
326 comments: &Comments,
327 ast: &'ast ast::SourceUnit<'ast>,
328) -> InlineConfig<Vec<String>> {
329 let items = comments.iter().filter_map(|comment| {
330 let mut item = comment.lines.first()?.as_str();
331 if let Some(prefix) = comment.prefix() {
332 item = item.strip_prefix(prefix).unwrap_or(item);
333 }
334 if let Some(suffix) = comment.suffix() {
335 item = item.strip_suffix(suffix).unwrap_or(item);
336 }
337 let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
338 let span = comment.span;
339 match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
340 Ok(item) => Some((span, item)),
341 Err(e) => {
342 sess.dcx.warn(e.to_string()).span(span).emit();
343 None
344 }
345 }
346 });
347
348 InlineConfig::from_ast(items, ast, sess.source_map())
349}
350
351#[derive(Error, Debug)]
352pub enum SolLintError {
353 #[error("Unknown lint ID: {0}")]
354 InvalidId(String),
355}
356
357#[derive(Debug, Clone, Copy, Eq, PartialEq)]
358pub struct SolLint {
359 id: &'static str,
360 description: &'static str,
361 help: &'static str,
362 severity: Severity,
363}
364
365impl Lint for SolLint {
366 fn id(&self) -> &'static str {
367 self.id
368 }
369 fn severity(&self) -> Severity {
370 self.severity
371 }
372 fn description(&self) -> &'static str {
373 self.description
374 }
375 fn help(&self) -> &'static str {
376 self.help
377 }
378}
379
380impl<'a> TryFrom<&'a str> for SolLint {
381 type Error = SolLintError;
382
383 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
384 for &lint in high::REGISTERED_LINTS {
385 if lint.id() == value {
386 return Ok(lint);
387 }
388 }
389
390 for &lint in med::REGISTERED_LINTS {
391 if lint.id() == value {
392 return Ok(lint);
393 }
394 }
395
396 for &lint in info::REGISTERED_LINTS {
397 if lint.id() == value {
398 return Ok(lint);
399 }
400 }
401
402 for &lint in gas::REGISTERED_LINTS {
403 if lint.id() == value {
404 return Ok(lint);
405 }
406 }
407
408 for &lint in codesize::REGISTERED_LINTS {
409 if lint.id() == value {
410 return Ok(lint);
411 }
412 }
413
414 Err(SolLintError::InvalidId(value.to_string()))
415 }
416}