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::{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}
63
64pub struct LinterConfig<'s> {
65 pub inline: &'s InlineConfig<Vec<String>>,
66 pub lint_specific: &'s LintSpecificConfig,
67}
68
69impl<'s, 'c> LintContext<'s, 'c> {
70 pub const fn new(
71 sess: &'s Session,
72 with_description: bool,
73 with_json_emitter: bool,
74 config: LinterConfig<'c>,
75 active_lints: Vec<&'static str>,
76 source_file: Option<Arc<SourceFile>>,
77 ) -> Self {
78 Self { sess, with_description, with_json_emitter, config, active_lints, source_file }
79 }
80
81 fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
82 if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
84 }
85
86 pub const fn session(&self) -> &'s Session {
87 self.sess
88 }
89
90 pub const fn source_file(&self) -> Option<&Arc<SourceFile>> {
92 self.source_file.as_ref()
93 }
94
95 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
100 self.active_lints.contains(&id)
101 }
102
103 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
105 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
106 return;
107 }
108
109 let desc = if self.with_description { lint.description() } else { "" };
110 let mut diag: DiagBuilder<'_, ()> = self
111 .sess
112 .dcx
113 .diag(lint.severity().into(), desc)
114 .code(DiagId::new_str(lint.id()))
115 .span(MultiSpan::from_span(span));
116
117 diag = self.add_help(diag, lint.help());
118
119 diag.emit();
120 }
121
122 pub fn emit_with_msg<L: Lint>(&self, lint: &'static L, span: Span, msg: impl Into<DiagMsg>) {
127 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
128 return;
129 }
130
131 let diag: DiagBuilder<'_, ()> = self
132 .sess
133 .dcx
134 .diag(lint.severity().into(), msg.into())
135 .code(DiagId::new_str(lint.id()))
136 .span(MultiSpan::from_span(span));
137
138 self.add_help(diag, lint.help()).emit();
139 }
140
141 pub fn emit_with_suggestion<L: Lint>(
145 &self,
146 lint: &'static L,
147 span: Span,
148 suggestion: Suggestion,
149 ) {
150 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
151 return;
152 }
153
154 let desc = if self.with_description { lint.description() } else { "" };
155 let mut diag: DiagBuilder<'_, ()> = self
156 .sess
157 .dcx
158 .diag(lint.severity().into(), desc)
159 .code(DiagId::new_str(lint.id()))
160 .span(MultiSpan::from_span(span));
161
162 diag = match suggestion.kind {
163 SuggestionKind::Fix { span: fix_span, applicability, style } => diag
164 .span_suggestion_with_style(
165 fix_span.unwrap_or(span),
166 suggestion.desc.unwrap_or_default(),
167 suggestion.content,
168 applicability,
169 style,
170 ),
171 SuggestionKind::Example => {
172 if let Some(note) = suggestion.to_note() {
173 diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
174 } else {
175 diag
176 }
177 }
178 };
179
180 diag = self.add_help(diag, lint.help());
181
182 diag.emit();
183 }
184
185 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
187 self.sess.source_map().span_to_snippet(span).ok()
188 }
189
190 pub fn get_span_indentation(&self, span: Span) -> usize {
192 if !span.is_dummy() {
193 let loc = self.sess.source_map().lookup_char_pos(span.lo());
195 if let Some(line_text) = loc.file.get_line(loc.line) {
196 let col_offset = loc.col.to_usize();
197 if col_offset <= line_text.len() {
198 let prev_text = &line_text[..col_offset];
199 return prev_text.len() - prev_text.trim().len();
200 }
201 }
202 }
203
204 0
205 }
206}
207
208#[derive(Debug, Clone, Eq, PartialEq)]
209pub enum SuggestionKind {
210 Example,
214
215 Fix {
218 span: Option<Span>,
221 applicability: Applicability,
223 style: SuggestionStyle,
225 },
226}
227
228#[derive(Debug, Clone, Eq, PartialEq)]
233pub struct Suggestion {
234 desc: Option<&'static str>,
236 content: String,
238 kind: SuggestionKind,
240}
241
242impl Suggestion {
243 pub const fn example(content: String) -> Self {
245 Self { desc: None, content, kind: SuggestionKind::Example }
246 }
247
248 pub const fn fix(content: String, applicability: Applicability) -> Self {
252 Self {
253 desc: None,
254 content,
255 kind: SuggestionKind::Fix {
256 span: None,
257 applicability,
258 style: SuggestionStyle::ShowCode,
259 },
260 }
261 }
262
263 pub const fn with_desc(mut self, desc: &'static str) -> Self {
265 self.desc = Some(desc);
266 self
267 }
268
269 pub const fn with_span(mut self, span: Span) -> Self {
271 if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
272 *s = Some(span);
273 }
274 self
275 }
276
277 pub const fn with_style(mut self, style: SuggestionStyle) -> Self {
279 if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
280 *s = style;
281 }
282 self
283 }
284
285 fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
286 if let SuggestionKind::Fix { .. } = &self.kind {
287 return None;
288 };
289
290 let mut output = if let Some(desc) = self.desc {
291 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
292 } else {
293 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
294 };
295
296 output.extend(
297 self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
298 );
299 output.push((DiagMsg::from("\n"), Style::NoStyle));
300 Some(output)
301 }
302}
303
304fn hyperlink(url: &'static str) -> String {
306 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
307}