1use crate::{
2 inline_config::{InlineConfig, InlineConfigItem},
3 linter::{
4 EarlyLintPass, EarlyLintVisitor, LateLintPass, LateLintVisitor, Lint, LintContext, Linter,
5 LinterConfig,
6 },
7};
8use foundry_common::comments::Comments;
9use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage};
10use foundry_config::lint::Severity;
11use rayon::prelude::*;
12use solar::{
13 ast::{self as ast, visit::Visit as VisitAST},
14 interface::{
15 Session,
16 diagnostics::{self, HumanEmitter, JsonEmitter},
17 source_map::{FileName, SourceFile},
18 },
19 sema::{
20 Compiler, Gcx,
21 hir::{self, Visit as VisitHIR},
22 },
23};
24use std::{
25 path::{Path, PathBuf},
26 sync::LazyLock,
27};
28use thiserror::Error;
29
30#[macro_use]
31pub mod macros;
32
33pub mod codesize;
34pub mod gas;
35pub mod high;
36pub mod info;
37pub mod med;
38
39static ALL_REGISTERED_LINTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
40 let mut lints = Vec::new();
41 lints.extend_from_slice(high::REGISTERED_LINTS);
42 lints.extend_from_slice(med::REGISTERED_LINTS);
43 lints.extend_from_slice(info::REGISTERED_LINTS);
44 lints.extend_from_slice(gas::REGISTERED_LINTS);
45 lints.extend_from_slice(codesize::REGISTERED_LINTS);
46 lints.into_iter().map(|lint| lint.id()).collect()
47});
48
49#[derive(Debug)]
52pub struct SolidityLinter<'a> {
53 path_config: ProjectPathsConfig,
54 severity: Option<Vec<Severity>>,
55 lints_included: Option<Vec<SolLint>>,
56 lints_excluded: Option<Vec<SolLint>>,
57 with_description: bool,
58 with_json_emitter: bool,
59 mixed_case_exceptions: &'a [String],
60}
61
62impl<'a> SolidityLinter<'a> {
63 pub fn new(path_config: ProjectPathsConfig) -> Self {
64 Self {
65 path_config,
66 with_description: true,
67 severity: None,
68 lints_included: None,
69 lints_excluded: None,
70 with_json_emitter: false,
71 mixed_case_exceptions: &[],
72 }
73 }
74
75 pub fn with_severity(mut self, severity: Option<Vec<Severity>>) -> Self {
76 self.severity = severity;
77 self
78 }
79
80 pub fn with_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
81 self.lints_included = lints;
82 self
83 }
84
85 pub fn without_lints(mut self, lints: Option<Vec<SolLint>>) -> Self {
86 self.lints_excluded = lints;
87 self
88 }
89
90 pub fn with_description(mut self, with: bool) -> Self {
91 self.with_description = with;
92 self
93 }
94
95 pub fn with_json_emitter(mut self, with: bool) -> Self {
96 self.with_json_emitter = with;
97 self
98 }
99
100 pub fn with_mixed_case_exceptions(mut self, exceptions: &'a [String]) -> Self {
101 self.mixed_case_exceptions = exceptions;
102 self
103 }
104
105 fn config(&self, inline: InlineConfig) -> LinterConfig<'_> {
106 LinterConfig { inline, mixed_case_exceptions: self.mixed_case_exceptions }
107 }
108
109 fn include_lint(&self, lint: SolLint) -> bool {
110 self.severity.as_ref().is_none_or(|sev| sev.contains(&lint.severity()))
111 && self.lints_included.as_ref().is_none_or(|incl| incl.contains(&lint))
112 && !self.lints_excluded.as_ref().is_some_and(|excl| excl.contains(&lint))
113 }
114
115 fn process_source_ast<'gcx>(
116 &self,
117 sess: &'gcx Session,
118 ast: &'gcx ast::SourceUnit<'gcx>,
119 file: &SourceFile,
120 path: &Path,
121 ) -> Result<(), diagnostics::ErrorGuaranteed> {
122 let mut passes_and_lints = Vec::new();
124 passes_and_lints.extend(high::create_early_lint_passes());
125 passes_and_lints.extend(med::create_early_lint_passes());
126 passes_and_lints.extend(info::create_early_lint_passes());
127
128 if !self.path_config.is_test_or_script(path) {
130 passes_and_lints.extend(gas::create_early_lint_passes());
131 passes_and_lints.extend(codesize::create_early_lint_passes());
132 }
133
134 let (mut passes, lints): (Vec<Box<dyn EarlyLintPass<'_>>>, Vec<_>) = passes_and_lints
136 .into_iter()
137 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
138 let included_ids: Vec<_> = lints
139 .iter()
140 .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
141 .collect();
142
143 if !included_ids.is_empty() {
144 passes.push(pass);
145 ids.extend(included_ids);
146 }
147
148 (passes, ids)
149 });
150
151 let comments = Comments::new(file);
153 let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Ast(ast));
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 file: &'gcx SourceFile,
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 let FileName::Real(path) = &file.name
184 && !self.path_config.is_test_or_script(path)
185 {
186 passes_and_lints.extend(gas::create_late_lint_passes());
187 passes_and_lints.extend(codesize::create_late_lint_passes());
188 }
189
190 let (mut passes, lints): (Vec<Box<dyn LateLintPass<'_>>>, Vec<_>) = passes_and_lints
192 .into_iter()
193 .fold((Vec::new(), Vec::new()), |(mut passes, mut ids), (pass, lints)| {
194 let included_ids: Vec<_> = lints
195 .iter()
196 .filter_map(|lint| if self.include_lint(*lint) { Some(lint.id) } else { None })
197 .collect();
198
199 if !included_ids.is_empty() {
200 passes.push(pass);
201 ids.extend(included_ids);
202 }
203
204 (passes, ids)
205 });
206
207 let comments = Comments::new(file);
209 let inline_config = parse_inline_config(
210 gcx.sess,
211 &comments,
212 InlineConfigSource::Hir((&gcx.hir, source_id)),
213 );
214
215 let ctx = LintContext::new(
217 gcx.sess,
218 self.with_description,
219 self.with_json_emitter,
220 self.config(inline_config),
221 lints,
222 );
223 let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
224
225 let _ = late_visitor.visit_nested_source(source_id);
227
228 Ok(())
229 }
230}
231
232impl<'a> Linter for SolidityLinter<'a> {
233 type Language = SolcLanguage;
234 type Lint = SolLint;
235
236 fn configure(&self, compiler: &mut Compiler) {
237 let dcx = compiler.dcx_mut();
238 let sm = dcx.source_map_mut().unwrap().clone();
239 dcx.set_emitter(if self.with_json_emitter {
240 let writer = Box::new(std::io::BufWriter::new(std::io::stderr()));
241 let json_emitter = JsonEmitter::new(writer, sm).rustc_like(true).ui_testing(false);
242 Box::new(json_emitter)
243 } else {
244 Box::new(HumanEmitter::stderr(Default::default()).source_map(Some(sm)))
245 });
246 dcx.set_flags_mut(|f| f.track_diagnostics = false);
247 }
248
249 fn lint(&self, input: &[PathBuf], compiler: &mut Compiler) {
250 compiler.enter_mut(|compiler| {
251 let gcx = compiler.gcx();
252
253 gcx.sources.raw.par_iter().for_each(|source| {
255 if let (FileName::Real(path), Some(ast)) = (&source.file.name, &source.ast)
256 && input.iter().any(|input_path| path.ends_with(input_path))
257 {
258 let _ = self.process_source_ast(gcx.sess, ast, &source.file, path);
259 }
260 });
261
262 gcx.hir.par_sources_enumerated().for_each(|(source_id, source)| {
264 if let FileName::Real(path) = &source.file.name
265 && input.iter().any(|input_path| path.ends_with(input_path))
266 {
267 let _ = self.process_source_hir(gcx, source_id, &source.file);
268 }
269 });
270 });
271 }
272}
273
274enum InlineConfigSource<'ast, 'hir> {
275 Ast(&'ast ast::SourceUnit<'ast>),
276 Hir((&'hir hir::Hir<'hir>, hir::SourceId)),
277}
278
279fn parse_inline_config<'ast, 'hir>(
280 sess: &Session,
281 comments: &Comments,
282 source: InlineConfigSource<'ast, 'hir>,
283) -> InlineConfig {
284 let items = comments.iter().filter_map(|comment| {
285 let mut item = comment.lines.first()?.as_str();
286 if let Some(prefix) = comment.prefix() {
287 item = item.strip_prefix(prefix).unwrap_or(item);
288 }
289 if let Some(suffix) = comment.suffix() {
290 item = item.strip_suffix(suffix).unwrap_or(item);
291 }
292 let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
293 let span = comment.span;
294 match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
295 Ok(item) => Some((span, item)),
296 Err(e) => {
297 sess.dcx.warn(e.to_string()).span(span).emit();
298 None
299 }
300 }
301 });
302
303 match source {
304 InlineConfigSource::Ast(ast) => InlineConfig::from_ast(items, ast, sess.source_map()),
305 InlineConfigSource::Hir((hir, id)) => {
306 InlineConfig::from_hir(items, hir, id, sess.source_map())
307 }
308 }
309}
310
311#[derive(Error, Debug)]
312pub enum SolLintError {
313 #[error("Unknown lint ID: {0}")]
314 InvalidId(String),
315}
316
317#[derive(Debug, Clone, Copy, Eq, PartialEq)]
318pub struct SolLint {
319 id: &'static str,
320 description: &'static str,
321 help: &'static str,
322 severity: Severity,
323}
324
325impl Lint for SolLint {
326 fn id(&self) -> &'static str {
327 self.id
328 }
329 fn severity(&self) -> Severity {
330 self.severity
331 }
332 fn description(&self) -> &'static str {
333 self.description
334 }
335 fn help(&self) -> &'static str {
336 self.help
337 }
338}
339
340impl<'a> TryFrom<&'a str> for SolLint {
341 type Error = SolLintError;
342
343 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
344 for &lint in high::REGISTERED_LINTS {
345 if lint.id() == value {
346 return Ok(lint);
347 }
348 }
349
350 for &lint in med::REGISTERED_LINTS {
351 if lint.id() == value {
352 return Ok(lint);
353 }
354 }
355
356 for &lint in info::REGISTERED_LINTS {
357 if lint.id() == value {
358 return Ok(lint);
359 }
360 }
361
362 for &lint in gas::REGISTERED_LINTS {
363 if lint.id() == value {
364 return Ok(lint);
365 }
366 }
367
368 for &lint in codesize::REGISTERED_LINTS {
369 if lint.id() == value {
370 return Ok(lint);
371 }
372 }
373
374 Err(SolLintError::InvalidId(value.to_string()))
375 }
376}