1mod early;
2mod late;
3mod project;
4
5pub use early::{EarlyLintPass, EarlyLintVisitor};
6pub use late::{LateLintPass, LateLintVisitor};
7pub use project::{ProjectLintEmitter, ProjectLintPass, ProjectSource};
8
9use foundry_common::comments::inline_config::InlineConfig;
10use foundry_compilers::Language;
11use foundry_config::{
12 DenyLevel,
13 lint::{LintSpecificConfig, Severity},
14};
15use solar::{
16 interface::{
17 Session, Span,
18 diagnostics::{
19 Applicability, DiagBuilder, DiagId, DiagMsg, MultiSpan, Style, SuggestionStyle,
20 },
21 source_map::SourceFile,
22 },
23 sema::Compiler,
24};
25use std::{cell::RefCell, collections::HashSet, path::PathBuf, sync::Arc};
26
27pub trait Linter: Send + Sync {
32 type Language: Language;
34 type Lint: Lint;
36
37 fn lint(&self, input: &[PathBuf], deny: DenyLevel, compiler: &mut Compiler)
44 -> eyre::Result<()>;
45}
46
47pub trait Lint {
48 fn id(&self) -> &'static str;
49 fn severity(&self) -> Severity;
50 fn description(&self) -> &'static str;
51 fn help(&self) -> &'static str;
52}
53
54pub struct LintContext<'s, 'c> {
55 sess: &'s Session,
56 with_description: bool,
57 with_json_emitter: bool,
58 pub config: LinterConfig<'c>,
59 active_lints: Vec<&'static str>,
60 source_file: Option<Arc<SourceFile>>,
62 emitted: RefCell<HashSet<(&'static str, Span)>>,
63}
64
65pub struct LinterConfig<'s> {
66 pub inline: &'s InlineConfig<Vec<String>>,
67 pub lint_specific: &'s LintSpecificConfig,
68}
69
70impl<'s, 'c> LintContext<'s, 'c> {
71 pub fn new(
72 sess: &'s Session,
73 with_description: bool,
74 with_json_emitter: bool,
75 config: LinterConfig<'c>,
76 active_lints: Vec<&'static str>,
77 source_file: Option<Arc<SourceFile>>,
78 ) -> Self {
79 Self {
80 sess,
81 with_description,
82 with_json_emitter,
83 config,
84 active_lints,
85 source_file,
86 emitted: RefCell::default(),
87 }
88 }
89
90 fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
91 if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
93 }
94
95 pub const fn session(&self) -> &'s Session {
96 self.sess
97 }
98
99 pub const fn source_file(&self) -> Option<&Arc<SourceFile>> {
101 self.source_file.as_ref()
102 }
103
104 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
109 self.active_lints.contains(&id)
110 }
111
112 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
114 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
115 return;
116 }
117 if !self.emitted.borrow_mut().insert((lint.id(), span)) {
118 return;
119 }
120
121 let desc = if self.with_description { lint.description() } else { "" };
122 let mut diag: DiagBuilder<'_, ()> = self
123 .sess
124 .dcx
125 .diag(lint.severity().into(), desc)
126 .code(DiagId::new_str(lint.id()))
127 .span(MultiSpan::from_span(span));
128
129 diag = self.add_help(diag, lint.help());
130
131 diag.emit();
132 }
133
134 pub fn emit_with_msg<L: Lint>(&self, lint: &'static L, span: Span, msg: impl Into<DiagMsg>) {
139 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
140 return;
141 }
142
143 let diag: DiagBuilder<'_, ()> = self
144 .sess
145 .dcx
146 .diag(lint.severity().into(), msg.into())
147 .code(DiagId::new_str(lint.id()))
148 .span(MultiSpan::from_span(span));
149
150 self.add_help(diag, lint.help()).emit();
151 }
152
153 pub fn emit_with_suggestion<L: Lint>(
157 &self,
158 lint: &'static L,
159 span: Span,
160 suggestion: Suggestion,
161 ) {
162 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
163 return;
164 }
165
166 let desc = if self.with_description { lint.description() } else { "" };
167 let mut diag: DiagBuilder<'_, ()> = self
168 .sess
169 .dcx
170 .diag(lint.severity().into(), desc)
171 .code(DiagId::new_str(lint.id()))
172 .span(MultiSpan::from_span(span));
173
174 diag = match suggestion.kind {
175 SuggestionKind::Fix { span: fix_span, applicability, style } => diag
176 .span_suggestion_with_style(
177 fix_span.unwrap_or(span),
178 suggestion.desc.unwrap_or_default(),
179 suggestion.content,
180 applicability,
181 style,
182 ),
183 SuggestionKind::Example => {
184 if let Some(note) = suggestion.to_note() {
185 diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
186 } else {
187 diag
188 }
189 }
190 };
191
192 diag = self.add_help(diag, lint.help());
193
194 diag.emit();
195 }
196
197 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
199 self.sess.source_map().span_to_snippet(span).ok()
200 }
201
202 pub fn get_span_indentation(&self, span: Span) -> usize {
204 if !span.is_dummy() {
205 let loc = self.sess.source_map().lookup_char_pos(span.lo());
207 if let Some(line_text) = loc.file.get_line(loc.line) {
208 let col_offset = loc.col.to_usize();
209 if col_offset <= line_text.len() {
210 let prev_text = &line_text[..col_offset];
211 return prev_text.len() - prev_text.trim().len();
212 }
213 }
214 }
215
216 0
217 }
218}
219
220#[derive(Debug, Clone, Eq, PartialEq)]
221pub enum SuggestionKind {
222 Example,
226
227 Fix {
230 span: Option<Span>,
233 applicability: Applicability,
235 style: SuggestionStyle,
237 },
238}
239
240#[derive(Debug, Clone, Eq, PartialEq)]
245pub struct Suggestion {
246 desc: Option<&'static str>,
248 content: String,
250 kind: SuggestionKind,
252}
253
254impl Suggestion {
255 pub const fn example(content: String) -> Self {
257 Self { desc: None, content, kind: SuggestionKind::Example }
258 }
259
260 pub const fn fix(content: String, applicability: Applicability) -> Self {
264 Self {
265 desc: None,
266 content,
267 kind: SuggestionKind::Fix {
268 span: None,
269 applicability,
270 style: SuggestionStyle::ShowCode,
271 },
272 }
273 }
274
275 pub const fn with_desc(mut self, desc: &'static str) -> Self {
277 self.desc = Some(desc);
278 self
279 }
280
281 pub const fn with_span(mut self, span: Span) -> Self {
283 if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
284 *s = Some(span);
285 }
286 self
287 }
288
289 pub const fn with_style(mut self, style: SuggestionStyle) -> Self {
291 if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
292 *s = style;
293 }
294 self
295 }
296
297 fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
298 if let SuggestionKind::Fix { .. } = &self.kind {
299 return None;
300 };
301
302 let mut output = if let Some(desc) = self.desc {
303 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
304 } else {
305 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
306 };
307
308 output.extend(
309 self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
310 );
311 output.push((DiagMsg::from("\n"), Style::NoStyle));
312 Some(output)
313 }
314}
315
316fn hyperlink(url: &'static str) -> String {
318 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
319}