1use crate::linter::{
2 EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter,
3 LinterConfig, ProjectLintEmitter, ProjectLintPass, ProjectSource,
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::{
15 DenyLevel,
16 lint::{LintSpecificConfig, Severity},
17};
18use rayon::prelude::*;
19use solar::{
20 ast::{self as ast, visit::Visit as _},
21 interface::{
22 Session,
23 diagnostics::{self, HumanEmitter, JsonEmitter, SilentEmitter},
24 source_map::SourceFile,
25 },
26 sema::{
27 Compiler, Gcx,
28 hir::{self, Visit as _},
29 },
30};
31use std::{
32 path::{Path, PathBuf},
33 sync::{Arc, LazyLock},
34};
35use thiserror::Error;
36
37#[macro_use]
38pub mod macros;
39
40pub mod analysis;
41mod calls;
42pub mod codesize;
43pub mod gas;
44pub mod high;
45pub mod info;
46pub mod low;
47pub mod med;
48pub mod naming;
49
50static ALL_REGISTERED_LINTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
51 let mut lints = Vec::new();
52 lints.extend_from_slice(high::REGISTERED_LINTS);
53 lints.extend_from_slice(med::REGISTERED_LINTS);
54 lints.extend_from_slice(low::REGISTERED_LINTS);
55 lints.extend_from_slice(info::REGISTERED_LINTS);
56 lints.extend_from_slice(gas::REGISTERED_LINTS);
57 lints.extend_from_slice(codesize::REGISTERED_LINTS);
58 lints.into_iter().map(|lint| lint.id()).collect()
59});
60
61static DEFAULT_LINT_SPECIFIC_CONFIG: LazyLock<LintSpecificConfig> =
62 LazyLock::new(LintSpecificConfig::default);
63
64#[derive(Debug)]
67pub struct SolidityLinter<'a> {
68 path_config: ProjectPathsConfig,
69 severity: Option<Vec<Severity>>,
70 lints_included: Option<Vec<SolLint>>,
71 lints_excluded: Option<Vec<SolLint>>,
72 with_description: bool,
73 with_json_emitter: bool,
74 lint_specific: &'a LintSpecificConfig,
76}
77
78impl<'a> SolidityLinter<'a> {
79 pub fn new(path_config: ProjectPathsConfig) -> Self {
80 Self {
81 path_config,
82 with_description: true,
83 severity: None,
84 lints_included: None,
85 lints_excluded: None,
86 with_json_emitter: false,
87 lint_specific: &DEFAULT_LINT_SPECIFIC_CONFIG,
88 }
89 }
90
91 pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
92 self.severity = severity;
93 self
94 }
95
96 pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
97 self.lints_included = lints;
98 self
99 }
100
101 pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
102 self.lints_excluded = lints;
103 self
104 }
105
106 pub const fn with_description(mut self, with: bool) -> Self {
107 self.with_description = with;
108 self
109 }
110
111 pub const fn with_json_emitter(mut self, with: bool) -> Self {
112 self.with_json_emitter = with;
113 self
114 }
115
116 pub const fn with_lint_specific(mut self, lint_specific: &'a LintSpecificConfig) -> Self {
117 self.lint_specific = lint_specific;
118 self
119 }
120
121 const fn config(&'a self, inline: &'a InlineConfig<Vec<String>>) -> LinterConfig<'a> {
122 LinterConfig { inline, lint_specific: self.lint_specific }
123 }
124
125 fn include_lint(&self, lint: SolLint) -> bool {
126 self.severity.as_ref().is_none_or(|sev| sev.contains(&lint.severity()))
127 && self.lints_included.as_ref().is_none_or(|incl| incl.contains(&lint))
128 && self.lints_excluded.as_ref().is_none_or(|excl| !excl.contains(&lint))
129 }
130
131 fn process_source_ast<'gcx>(
132 &self,
133 sess: &'gcx Session,
134 ast: &'gcx ast::SourceUnit<'gcx>,
135 path: &Path,
136 inline_config: &InlineConfig<Vec<String>>,
137 source_file: Option<Arc<SourceFile>>,
138 ) -> Result<(), diagnostics::ErrorGuaranteed> {
139 let mut passes_and_lints = Vec::new();
141 passes_and_lints.extend(high::create_early_lint_passes());
142 passes_and_lints.extend(med::create_early_lint_passes());
143 passes_and_lints.extend(low::create_early_lint_passes());
144 passes_and_lints.extend(info::create_early_lint_passes());
145
146 if !self.path_config.is_test_or_script(path) {
148 passes_and_lints.extend(gas::create_early_lint_passes());
149 passes_and_lints.extend(codesize::create_early_lint_passes());
150 }
151
152 let (mut passes, lints): (Vec<Box<dyn EarlyLintPass<'_>>>, Vec<_>) = passes_and_lints
154 .into_iter()
155 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
156 let included_ids: Vec<_> = lints
157 .iter()
158 .filter_map(|lint| self.include_lint(*lint).then_some(lint.id))
159 .collect();
160
161 if !included_ids.is_empty() {
162 passes.push(pass);
163 ids.extend(included_ids);
164 }
165
166 (passes, ids)
167 });
168
169 let ctx = LintContext::new(
171 sess,
172 self.with_description,
173 self.with_json_emitter,
174 self.config(inline_config),
175 lints,
176 source_file,
177 );
178 let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes);
179 _ = early_visitor.visit_source_unit(ast);
180 early_visitor.post_source_unit(ast);
181
182 Ok(())
183 }
184
185 fn process_project<'gcx>(&self, gcx: Gcx<'gcx>, input: &[PathBuf]) {
187 let mut passes_and_lints: Vec<(Box<dyn ProjectLintPass<'_>>, &'static [SolLint])> =
189 Vec::new();
190 passes_and_lints.extend(high::create_project_lint_passes());
191 passes_and_lints.extend(med::create_project_lint_passes());
192 passes_and_lints.extend(low::create_project_lint_passes());
193 passes_and_lints.extend(info::create_project_lint_passes());
194 passes_and_lints.extend(gas::create_project_lint_passes());
195 passes_and_lints.extend(codesize::create_project_lint_passes());
196
197 let (mut passes, lint_ids): (Vec<Box<dyn ProjectLintPass<'_>>>, Vec<_>) = passes_and_lints
198 .into_iter()
199 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
200 let included: Vec<_> = lints
201 .iter()
202 .filter_map(|lint| self.include_lint(*lint).then_some(lint.id))
203 .collect();
204 if !included.is_empty() {
205 passes.push(pass);
206 ids.extend(included);
207 }
208 (passes, ids)
209 });
210
211 if passes.is_empty() {
212 return;
213 }
214
215 let sources: Vec<ProjectSource<'_>> = input
217 .iter()
218 .filter_map(|path| {
219 let path = self.path_config.root.join(path);
220 let (_, source) = gcx.get_ast_source(&path)?;
221 let ast = source.ast.as_ref()?;
222 let comments =
223 Comments::new(&source.file, gcx.sess.source_map(), false, false, None);
224 let inline_config = parse_inline_config(gcx.sess, &comments, ast);
225 Some(ProjectSource { path, file: source.file.clone(), ast, inline_config })
226 })
227 .collect();
228
229 let emitter = ProjectLintEmitter::new(
230 gcx.sess,
231 gcx,
232 self.with_description,
233 self.with_json_emitter,
234 self.lint_specific,
235 lint_ids,
236 );
237 for pass in &mut passes {
238 pass.check_project(&emitter, &sources);
239 }
240 }
241
242 fn process_source_hir<'gcx>(
243 &self,
244 gcx: Gcx<'gcx>,
245 source_id: hir::SourceId,
246 path: &Path,
247 inline_config: &InlineConfig<Vec<String>>,
248 source_file: Option<Arc<SourceFile>>,
249 ) -> Result<(), diagnostics::ErrorGuaranteed> {
250 let mut passes_and_lints = Vec::new();
252 passes_and_lints.extend(high::create_late_lint_passes());
253 passes_and_lints.extend(med::create_late_lint_passes());
254 passes_and_lints.extend(low::create_late_lint_passes());
255 passes_and_lints.extend(info::create_late_lint_passes());
256
257 if !self.path_config.is_test_or_script(path) {
259 passes_and_lints.extend(gas::create_late_lint_passes());
260 passes_and_lints.extend(codesize::create_late_lint_passes());
261 }
262
263 let (mut passes, lints): (Vec<Box<dyn LateLintPass<'_>>>, Vec<_>) = passes_and_lints
265 .into_iter()
266 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
267 let included_ids: Vec<_> = lints
268 .iter()
269 .filter_map(|lint| self.include_lint(*lint).then_some(lint.id))
270 .collect();
271
272 if !included_ids.is_empty() {
273 passes.push(pass);
274 ids.extend(included_ids);
275 }
276
277 (passes, ids)
278 });
279
280 let ctx = LintContext::new(
282 gcx.sess,
283 self.with_description,
284 self.with_json_emitter,
285 self.config(inline_config),
286 lints,
287 source_file,
288 );
289 let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, gcx, &gcx.hir);
290
291 let _ = late_visitor.visit_nested_source(source_id);
293
294 Ok(())
295 }
296}
297
298impl<'a> Linter for SolidityLinter<'a> {
299 type Language = SolcLanguage;
300 type Lint = SolLint;
301
302 fn lint(
303 &self,
304 input: &[PathBuf],
305 deny: DenyLevel,
306 compiler: &mut Compiler,
307 ) -> eyre::Result<()> {
308 convert_solar_errors(compiler.dcx())?;
309
310 let mut warn_count_before = compiler.dcx().warn_count();
312 let mut note_count_before = compiler.dcx().note_count();
313
314 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
315
316 let sm = compiler.sess().clone_source_map();
317 let prev_emitter = compiler.dcx().set_emitter(if self.with_json_emitter {
318 let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
319 let json_emitter = JsonEmitter::new(writer, sm).rustc_like(true).ui_testing(ui_testing);
320 Box::new(json_emitter)
321 } else {
322 Box::new(HumanEmitter::stderr(Default::default()).source_map(Some(sm)))
323 });
324 let sess = compiler.sess_mut();
325 sess.dcx.set_flags_mut(|f| f.track_diagnostics = false);
326 if ui_testing {
327 sess.opts.unstable.ui_testing = true;
328 sess.reconfigure();
329 }
330
331 compiler.enter_mut(|compiler| -> eyre::Result<()> {
332 if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
333 let _ = compiler.lower_asts();
334 }
335 convert_solar_errors(compiler.dcx())?;
336 if compiler.gcx().stage() < Some(solar::config::CompilerStage::Analysis) {
337 let prev_emitter =
340 compiler.dcx().set_emitter(Box::new(SilentEmitter::new_boxed(None)));
341 let _ = compiler.analysis();
342 compiler.dcx().set_emitter(prev_emitter);
343 }
344 warn_count_before = compiler.dcx().warn_count();
345 note_count_before = compiler.dcx().note_count();
346
347 let gcx = compiler.gcx();
348
349 input.par_iter().for_each(|path| {
350 let path = &self.path_config.root.join(path);
351 let Some((_, ast_source)) = gcx.get_ast_source(path) else {
352 _ = sh_warn!("AST source not found for {}", path.display());
355 return;
356 };
357 let Some(ast) = &ast_source.ast else {
358 panic!("AST missing for {}", path.display());
359 };
360
361 let file = &ast_source.file;
363 let comments = Comments::new(file, gcx.sess.source_map(), false, false, None);
364 let inline_config = parse_inline_config(gcx.sess, &comments, ast);
365
366 let _ = self.process_source_ast(
368 gcx.sess,
369 ast,
370 path,
371 &inline_config,
372 Some(file.clone()),
373 );
374
375 let Some((hir_source_id, _)) = gcx.get_hir_source(path) else {
377 panic!("HIR source not found for {}", path.display());
378 };
379 let _ = self.process_source_hir(
380 gcx,
381 hir_source_id,
382 path,
383 &inline_config,
384 Some(file.clone()),
385 );
386 });
387
388 self.process_project(gcx, input);
390
391 Ok(())
392 })?;
393
394 let sess = compiler.sess_mut();
395 sess.dcx.set_emitter(prev_emitter);
396 if ui_testing {
397 sess.opts.unstable.ui_testing = false;
398 sess.reconfigure();
399 }
400
401 let lint_warn_count = compiler.dcx().warn_count().saturating_sub(warn_count_before);
402 let lint_note_count = compiler.dcx().note_count().saturating_sub(note_count_before);
403
404 const MSG: &str = "aborting due to ";
405 match (deny, lint_warn_count, lint_note_count) {
406 (DenyLevel::Warnings, w, n) if w > 0 => {
408 if n > 0 {
409 Err(eyre::eyre!("{MSG}{w} linter warning(s); {n} note(s) were also emitted\n"))
410 } else {
411 Err(eyre::eyre!("{MSG}{w} linter warning(s)\n"))
412 }
413 }
414
415 (DenyLevel::Notes, w, n) if w > 0 || n > 0 => match (w, n) {
417 (w, n) if w > 0 && n > 0 => {
418 Err(eyre::eyre!("{MSG}{w} linter warning(s) and {n} note(s)\n"))
419 }
420 (w, 0) => Err(eyre::eyre!("{MSG}{w} linter warning(s)\n")),
421 (0, n) => Err(eyre::eyre!("{MSG}{n} linter note(s)\n")),
422 _ => unreachable!(),
423 },
424
425 _ => Ok(()),
427 }
428 }
429}
430
431fn parse_inline_config<'ast>(
432 sess: &Session,
433 comments: &Comments,
434 ast: &'ast ast::SourceUnit<'ast>,
435) -> InlineConfig<Vec<String>> {
436 let items = comments.iter().filter_map(|comment| {
437 let mut item = comment.lines.first()?.as_str();
438 if let Some(prefix) = comment.prefix() {
439 item = item.strip_prefix(prefix).unwrap_or(item);
440 }
441 if let Some(suffix) = comment.suffix() {
442 item = item.strip_suffix(suffix).unwrap_or(item);
443 }
444 let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
445 let span = comment.span;
446 match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
447 Ok(item) => Some((span, item)),
448 Err(e) => {
449 sess.dcx.warn(e.to_string()).span(span).emit();
450 None
451 }
452 }
453 });
454
455 InlineConfig::from_ast(items, ast, sess.source_map())
456}
457
458#[derive(Error, Debug)]
459pub enum SolLintError {
460 #[error("Unknown lint ID: {0}")]
461 InvalidId(String),
462}
463
464#[derive(Debug, Clone, Copy, Eq, PartialEq)]
465pub struct SolLint {
466 id: &'static str,
467 description: &'static str,
468 help: &'static str,
469 severity: Severity,
470}
471
472impl Lint for SolLint {
473 fn id(&self) -> &'static str {
474 self.id
475 }
476 fn severity(&self) -> Severity {
477 self.severity
478 }
479 fn description(&self) -> &'static str {
480 self.description
481 }
482 fn help(&self) -> &'static str {
483 self.help
484 }
485}
486
487impl<'a> TryFrom<&'a str> for SolLint {
488 type Error = SolLintError;
489
490 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
491 for &lint in high::REGISTERED_LINTS {
492 if lint.id() == value {
493 return Ok(lint);
494 }
495 }
496
497 for &lint in med::REGISTERED_LINTS {
498 if lint.id() == value {
499 return Ok(lint);
500 }
501 }
502
503 for &lint in low::REGISTERED_LINTS {
504 if lint.id() == value {
505 return Ok(lint);
506 }
507 }
508
509 for &lint in info::REGISTERED_LINTS {
510 if lint.id() == value {
511 return Ok(lint);
512 }
513 }
514
515 for &lint in gas::REGISTERED_LINTS {
516 if lint.id() == value {
517 return Ok(lint);
518 }
519 }
520
521 for &lint in codesize::REGISTERED_LINTS {
522 if lint.id() == value {
523 return Ok(lint);
524 }
525 }
526
527 Err(SolLintError::InvalidId(value.to_string()))
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
543 fn registered_lints_have_docs() {
544 let docs_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs");
545 assert!(docs_dir.is_dir(), "missing docs directory at {}", docs_dir.display());
546
547 let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS
548 .iter()
549 .chain(med::REGISTERED_LINTS)
550 .chain(low::REGISTERED_LINTS)
551 .chain(info::REGISTERED_LINTS)
552 .chain(gas::REGISTERED_LINTS)
553 .chain(codesize::REGISTERED_LINTS)
554 .collect();
555
556 let mut missing: Vec<&'static str> = Vec::new();
557 let mut empty: Vec<&'static str> = Vec::new();
558 for lint in &all_lints {
559 let path = docs_dir.join(format!("{}.md", lint.id()));
560 match std::fs::read_to_string(&path) {
561 Ok(content) => {
562 if content.trim().is_empty() || !content.contains(lint.id()) {
564 empty.push(lint.id());
565 }
566 }
567 Err(_) => missing.push(lint.id()),
568 }
569 }
570
571 assert!(
572 missing.is_empty(),
573 "the following registered lints are missing a docs file at \
574 `crates/lint/docs/<id>.md`: {missing:?}\n\
575 See `crates/lint/docs/_template.md` for the expected structure."
576 );
577 assert!(
578 empty.is_empty(),
579 "the following lint docs files are empty or do not reference the lint id: {empty:?}"
580 );
581 }
582
583 #[test]
586 fn registered_lints_have_canonical_help_url() {
587 let all_lints: Vec<&'static SolLint> = high::REGISTERED_LINTS
588 .iter()
589 .chain(med::REGISTERED_LINTS)
590 .chain(low::REGISTERED_LINTS)
591 .chain(info::REGISTERED_LINTS)
592 .chain(gas::REGISTERED_LINTS)
593 .chain(codesize::REGISTERED_LINTS)
594 .collect();
595
596 for lint in all_lints {
597 let expected = format!("https://getfoundry.sh/forge/linting/{}", lint.id());
598 assert_eq!(lint.help(), expected, "lint `{}` has a non-canonical help URL", lint.id());
599 }
600 }
601}