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