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