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: Vec<Box<dyn EarlyLintPass<'_>>> = passes_and_lints
119 .into_iter()
120 .filter_map(|(pass, lint)| if self.include_lint(lint) { Some(pass) } else { None })
121 .collect();
122
123 let comments = Comments::new(file);
125 let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Ast(ast));
126
127 let ctx = LintContext::new(sess, self.with_description, inline_config);
129 let mut early_visitor = EarlyLintVisitor::new(&ctx, &mut passes);
130 _ = early_visitor.visit_source_unit(ast);
131 early_visitor.post_source_unit(ast);
132
133 Ok(())
134 }
135
136 fn process_source_hir<'hir>(
137 &self,
138 sess: &Session,
139 gcx: &solar_sema::ty::Gcx<'hir>,
140 source_id: hir::SourceId,
141 file: &'hir SourceFile,
142 ) -> Result<(), diagnostics::ErrorGuaranteed> {
143 let mut passes_and_lints = Vec::new();
145 passes_and_lints.extend(high::create_late_lint_passes());
146 passes_and_lints.extend(med::create_late_lint_passes());
147 passes_and_lints.extend(info::create_late_lint_passes());
148
149 if let FileName::Real(ref path) = file.name
151 && !self.path_config.is_test_or_script(path)
152 {
153 passes_and_lints.extend(gas::create_late_lint_passes());
154 }
155
156 let mut passes: Vec<Box<dyn LateLintPass<'_>>> = passes_and_lints
158 .into_iter()
159 .filter_map(|(pass, lint)| if self.include_lint(lint) { Some(pass) } else { None })
160 .collect();
161
162 let comments = Comments::new(file);
164 let inline_config =
165 parse_inline_config(sess, &comments, InlineConfigSource::Hir((&gcx.hir, source_id)));
166
167 let ctx = LintContext::new(sess, self.with_description, inline_config);
169 let mut late_visitor = LateLintVisitor::new(&ctx, &mut passes, &gcx.hir);
170
171 _ = late_visitor.visit_nested_source(source_id);
173
174 Ok(())
175 }
176}
177
178impl Linter for SolidityLinter {
179 type Language = SolcLanguage;
180 type Lint = SolLint;
181
182 fn init(&self) -> Session {
184 let mut builder = Session::builder();
185 if self.with_json_emitter {
186 let map = Arc::<SourceMap>::default();
187 let json_emitter = JsonEmitter::new(Box::new(std::io::stderr()), map.clone())
188 .rustc_like(true)
189 .ui_testing(false);
190
191 builder = builder.dcx(DiagCtxt::new(Box::new(json_emitter))).source_map(map);
192 } else {
193 builder = builder.with_stderr_emitter();
194 };
195
196 let mut sess = builder.build();
198 sess.dcx = sess.dcx.set_flags(|flags| flags.track_diagnostics = false);
199 sess
200 }
201
202 fn early_lint<'sess>(&self, input: &[PathBuf], mut pcx: ParsingContext<'sess>) {
204 let sess = pcx.sess;
205 _ = sess.enter_parallel(|| -> Result<(), diagnostics::ErrorGuaranteed> {
206 pcx.load_files(input)?;
208
209 let ast_arena = solar_sema::thread_local::ThreadLocal::new();
211 let ast_result = pcx.parse(&ast_arena);
212
213 ast_result.sources.iter().par_bridge().for_each(|source| {
215 if let (FileName::Real(path), Some(ast)) = (&source.file.name, &source.ast)
216 && input.iter().any(|input_path| path.ends_with(input_path))
217 {
218 _ = self.process_source_ast(sess, ast, &source.file, path)
219 }
220 });
221
222 Ok(())
223 });
224 }
225
226 fn late_lint<'sess>(&self, input: &[PathBuf], mut pcx: ParsingContext<'sess>) {
228 let sess = pcx.sess;
229 _ = sess.enter_parallel(|| -> Result<(), diagnostics::ErrorGuaranteed> {
230 pcx.load_files(input)?;
232
233 let hir_arena = solar_sema::thread_local::ThreadLocal::new();
235 let hir_result = pcx.parse_and_lower(&hir_arena);
236
237 if let Ok(Some(gcx_wrapper)) = hir_result {
238 let gcx = gcx_wrapper.get();
239
240 gcx.hir.sources_enumerated().par_bridge().for_each(|(source_id, source)| {
242 if let FileName::Real(ref path) = source.file.name
243 && input.iter().any(|input_path| path.ends_with(input_path))
244 {
245 _ = self.process_source_hir(sess, &gcx, source_id, &source.file);
246 }
247 });
248 }
249
250 Ok(())
251 });
252 }
253}
254
255enum InlineConfigSource<'ast, 'hir> {
256 Ast(&'ast ast::SourceUnit<'ast>),
257 Hir((&'hir hir::Hir<'hir>, hir::SourceId)),
258}
259
260fn parse_inline_config<'ast, 'hir>(
261 sess: &Session,
262 comments: &Comments,
263 source: InlineConfigSource<'ast, 'hir>,
264) -> InlineConfig {
265 let items = comments.iter().filter_map(|comment| {
266 let mut item = comment.lines.first()?.as_str();
267 if let Some(prefix) = comment.prefix() {
268 item = item.strip_prefix(prefix).unwrap_or(item);
269 }
270 if let Some(suffix) = comment.suffix() {
271 item = item.strip_suffix(suffix).unwrap_or(item);
272 }
273 let item = item.trim_start().strip_prefix("forge-lint:")?.trim();
274 let span = comment.span;
275 match InlineConfigItem::parse(item, &ALL_REGISTERED_LINTS) {
276 Ok(item) => Some((span, item)),
277 Err(e) => {
278 sess.dcx.warn(e.to_string()).span(span).emit();
279 None
280 }
281 }
282 });
283
284 match source {
285 InlineConfigSource::Ast(ast) => InlineConfig::from_ast(items, ast, sess.source_map()),
286 InlineConfigSource::Hir((hir, id)) => {
287 InlineConfig::from_hir(items, hir, id, sess.source_map())
288 }
289 }
290}
291
292#[derive(Error, Debug)]
293pub enum SolLintError {
294 #[error("Unknown lint ID: {0}")]
295 InvalidId(String),
296}
297
298#[derive(Debug, Clone, Copy, Eq, PartialEq)]
299pub struct SolLint {
300 id: &'static str,
301 description: &'static str,
302 help: &'static str,
303 severity: Severity,
304}
305
306impl Lint for SolLint {
307 fn id(&self) -> &'static str {
308 self.id
309 }
310 fn severity(&self) -> Severity {
311 self.severity
312 }
313 fn description(&self) -> &'static str {
314 self.description
315 }
316 fn help(&self) -> &'static str {
317 self.help
318 }
319}
320
321impl<'a> TryFrom<&'a str> for SolLint {
322 type Error = SolLintError;
323
324 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
325 for &lint in high::REGISTERED_LINTS {
326 if lint.id() == value {
327 return Ok(lint);
328 }
329 }
330
331 for &lint in med::REGISTERED_LINTS {
332 if lint.id() == value {
333 return Ok(lint);
334 }
335 }
336
337 for &lint in info::REGISTERED_LINTS {
338 if lint.id() == value {
339 return Ok(lint);
340 }
341 }
342
343 for &lint in gas::REGISTERED_LINTS {
344 if lint.id() == value {
345 return Ok(lint);
346 }
347 }
348
349 Err(SolLintError::InvalidId(value.to_string()))
350 }
351}