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 fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
73 if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
75 }
76
77 pub fn session(&self) -> &'s Session {
78 self.sess
79 }
80
81 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
86 self.active_lints.contains(&id)
87 }
88
89 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
91 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
92 return;
93 }
94
95 let desc = if self.with_description { lint.description() } else { "" };
96 let mut diag: DiagBuilder<'_, ()> = self
97 .sess
98 .dcx
99 .diag(lint.severity().into(), desc)
100 .code(DiagId::new_str(lint.id()))
101 .span(MultiSpan::from_span(span));
102
103 diag = self.add_help(diag, lint.help());
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 diag = self.add_help(diag, lint.help());
148
149 diag.emit();
150 }
151
152 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
154 self.sess.source_map().span_to_snippet(span).ok()
155 }
156
157 pub fn get_span_indentation(&self, span: Span) -> usize {
159 if !span.is_dummy() {
160 let loc = self.sess.source_map().lookup_char_pos(span.lo());
162 if let Some(line_text) = loc.file.get_line(loc.line) {
163 let col_offset = loc.col.to_usize();
164 if col_offset <= line_text.len() {
165 let prev_text = &line_text[..col_offset];
166 return prev_text.len() - prev_text.trim().len();
167 }
168 }
169 }
170
171 0
172 }
173}
174
175#[derive(Debug, Clone, Eq, PartialEq)]
176pub enum SuggestionKind {
177 Example,
181
182 Fix {
185 span: Option<Span>,
188 applicability: Applicability,
190 style: SuggestionStyle,
192 },
193}
194
195#[derive(Debug, Clone, Eq, PartialEq)]
200pub struct Suggestion {
201 desc: Option<&'static str>,
203 content: String,
205 kind: SuggestionKind,
207}
208
209impl Suggestion {
210 pub fn example(content: String) -> Self {
212 Self { desc: None, content, kind: SuggestionKind::Example }
213 }
214
215 pub fn fix(content: String, applicability: Applicability) -> Self {
219 Self {
220 desc: None,
221 content,
222 kind: SuggestionKind::Fix {
223 span: None,
224 applicability,
225 style: SuggestionStyle::ShowCode,
226 },
227 }
228 }
229
230 pub fn with_desc(mut self, desc: &'static str) -> Self {
232 self.desc = Some(desc);
233 self
234 }
235
236 pub fn with_span(mut self, span: Span) -> Self {
238 if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
239 *s = Some(span);
240 }
241 self
242 }
243
244 pub fn with_style(mut self, style: SuggestionStyle) -> Self {
246 if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
247 *s = style;
248 }
249 self
250 }
251
252 fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
253 if let SuggestionKind::Fix { .. } = &self.kind {
254 return None;
255 };
256
257 let mut output = if let Some(desc) = self.desc {
258 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
259 } else {
260 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
261 };
262
263 output.extend(
264 self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
265 );
266 output.push((DiagMsg::from("\n"), Style::NoStyle));
267 Some(output)
268 }
269}
270
271fn hyperlink(url: &'static str) -> String {
273 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
274}