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