1mod early;
2mod late;
3
4pub use early::{EarlyLintPass, EarlyLintVisitor};
5pub use late::{LateLintPass, LateLintVisitor};
6
7use foundry_common::comments::inline_config::InlineConfig;
8use foundry_compilers::Language;
9use foundry_config::{DenyLevel, lint::Severity};
10use solar::{
11 interface::{
12 Session, Span,
13 diagnostics::{
14 Applicability, DiagBuilder, DiagId, DiagMsg, MultiSpan, Style, SuggestionStyle,
15 },
16 },
17 sema::Compiler,
18};
19use std::path::PathBuf;
20
21pub trait Linter: Send + Sync {
26 type Language: Language;
28 type Lint: Lint;
30
31 fn lint(&self, input: &[PathBuf], deny: DenyLevel, compiler: &mut Compiler)
38 -> eyre::Result<()>;
39}
40
41pub trait Lint {
42 fn id(&self) -> &'static str;
43 fn severity(&self) -> Severity;
44 fn description(&self) -> &'static str;
45 fn help(&self) -> &'static str;
46}
47
48pub struct LintContext<'s, 'c> {
49 sess: &'s Session,
50 with_description: bool,
51 with_json_emitter: bool,
52 pub config: LinterConfig<'c>,
53 active_lints: Vec<&'static str>,
54}
55
56pub struct LinterConfig<'s> {
57 pub inline: &'s InlineConfig<Vec<String>>,
58 pub mixed_case_exceptions: &'s [String],
59}
60
61impl<'s, 'c> LintContext<'s, 'c> {
62 pub fn new(
63 sess: &'s Session,
64 with_description: bool,
65 with_json_emitter: bool,
66 config: LinterConfig<'c>,
67 active_lints: Vec<&'static str>,
68 ) -> Self {
69 Self { sess, with_description, with_json_emitter, config, active_lints }
70 }
71
72 pub fn session(&self) -> &'s Session {
73 self.sess
74 }
75
76 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
81 self.active_lints.contains(&id)
82 }
83
84 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
86 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
87 return;
88 }
89
90 let desc = if self.with_description { lint.description() } else { "" };
91 let mut diag: DiagBuilder<'_, ()> = self
92 .sess
93 .dcx
94 .diag(lint.severity().into(), desc)
95 .code(DiagId::new_str(lint.id()))
96 .span(MultiSpan::from_span(span));
97
98 if self.with_json_emitter {
100 diag = diag.help(lint.help());
101 } else {
102 diag = diag.help(hyperlink(lint.help()));
103 }
104
105 diag.emit();
106 }
107
108 pub fn emit_with_suggestion<L: Lint>(
112 &self,
113 lint: &'static L,
114 span: Span,
115 suggestion: Suggestion,
116 ) {
117 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
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 = match suggestion.kind {
130 SuggestionKind::Fix { span: fix_span, applicability, style } => diag
131 .span_suggestion_with_style(
132 fix_span.unwrap_or(span),
133 suggestion.desc.unwrap_or_default(),
134 suggestion.content,
135 applicability,
136 style,
137 ),
138 SuggestionKind::Example => {
139 if let Some(note) = suggestion.to_note() {
140 diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
141 } else {
142 diag
143 }
144 }
145 };
146
147 if self.with_json_emitter {
149 diag = diag.help(lint.help());
150 } else {
151 diag = diag.help(hyperlink(lint.help()));
152 }
153
154 diag.emit();
155 }
156
157 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
159 self.sess.source_map().span_to_snippet(span).ok()
160 }
161
162 pub fn get_span_indentation(&self, span: Span) -> usize {
164 if !span.is_dummy() {
165 let loc = self.sess.source_map().lookup_char_pos(span.lo());
167 if let Some(line_text) = loc.file.get_line(loc.line) {
168 let col_offset = loc.col.to_usize();
169 if col_offset <= line_text.len() {
170 let prev_text = &line_text[..col_offset];
171 return prev_text.len() - prev_text.trim().len();
172 }
173 }
174 }
175
176 0
177 }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq)]
181pub enum SuggestionKind {
182 Example,
186
187 Fix {
190 span: Option<Span>,
193 applicability: Applicability,
195 style: SuggestionStyle,
197 },
198}
199
200#[derive(Debug, Clone, Eq, PartialEq)]
205pub struct Suggestion {
206 desc: Option<&'static str>,
208 content: String,
210 kind: SuggestionKind,
212}
213
214impl Suggestion {
215 pub fn example(content: String) -> Self {
217 Self { desc: None, content, kind: SuggestionKind::Example }
218 }
219
220 pub fn fix(content: String, applicability: Applicability) -> Self {
224 Self {
225 desc: None,
226 content,
227 kind: SuggestionKind::Fix {
228 span: None,
229 applicability,
230 style: SuggestionStyle::ShowCode,
231 },
232 }
233 }
234
235 pub fn with_desc(mut self, desc: &'static str) -> Self {
237 self.desc = Some(desc);
238 self
239 }
240
241 pub fn with_span(mut self, span: Span) -> Self {
243 if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
244 *s = Some(span);
245 }
246 self
247 }
248
249 pub fn with_style(mut self, style: SuggestionStyle) -> Self {
251 if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
252 *s = style;
253 }
254 self
255 }
256
257 fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
258 if let SuggestionKind::Fix { .. } = &self.kind {
259 return None;
260 };
261
262 let mut output = if let Some(desc) = self.desc {
263 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
264 } else {
265 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
266 };
267
268 output.extend(
269 self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
270 );
271 output.push((DiagMsg::from("\n"), Style::NoStyle));
272 Some(output)
273 }
274}
275
276fn hyperlink(url: &'static str) -> String {
278 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
279}