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::{
10 DenyLevel,
11 lint::{LintSpecificConfig, Severity},
12};
13use solar::{
14 interface::{
15 Session, Span,
16 diagnostics::{
17 Applicability, DiagBuilder, DiagId, DiagMsg, MultiSpan, Style, SuggestionStyle,
18 },
19 },
20 sema::Compiler,
21};
22use std::path::PathBuf;
23
24pub trait Linter: Send + Sync {
29 type Language: Language;
31 type Lint: Lint;
33
34 fn lint(&self, input: &[PathBuf], deny: DenyLevel, compiler: &mut Compiler)
41 -> eyre::Result<()>;
42}
43
44pub trait Lint {
45 fn id(&self) -> &'static str;
46 fn severity(&self) -> Severity;
47 fn description(&self) -> &'static str;
48 fn help(&self) -> &'static str;
49}
50
51pub struct LintContext<'s, 'c> {
52 sess: &'s Session,
53 with_description: bool,
54 with_json_emitter: bool,
55 pub config: LinterConfig<'c>,
56 active_lints: Vec<&'static str>,
57}
58
59pub struct LinterConfig<'s> {
60 pub inline: &'s InlineConfig<Vec<String>>,
61 pub lint_specific: &'s LintSpecificConfig,
62}
63
64impl<'s, 'c> LintContext<'s, 'c> {
65 pub fn new(
66 sess: &'s Session,
67 with_description: bool,
68 with_json_emitter: bool,
69 config: LinterConfig<'c>,
70 active_lints: Vec<&'static str>,
71 ) -> Self {
72 Self { sess, with_description, with_json_emitter, config, active_lints }
73 }
74
75 fn add_help<'a>(&self, diag: DiagBuilder<'a, ()>, help: &'static str) -> DiagBuilder<'a, ()> {
76 if self.with_json_emitter { diag.help(help) } else { diag.help(hyperlink(help)) }
78 }
79
80 pub fn session(&self) -> &'s Session {
81 self.sess
82 }
83
84 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
89 self.active_lints.contains(&id)
90 }
91
92 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
94 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
95 return;
96 }
97
98 let desc = if self.with_description { lint.description() } else { "" };
99 let mut diag: DiagBuilder<'_, ()> = self
100 .sess
101 .dcx
102 .diag(lint.severity().into(), desc)
103 .code(DiagId::new_str(lint.id()))
104 .span(MultiSpan::from_span(span));
105
106 diag = self.add_help(diag, lint.help());
107
108 diag.emit();
109 }
110
111 pub fn emit_with_suggestion<L: Lint>(
115 &self,
116 lint: &'static L,
117 span: Span,
118 suggestion: Suggestion,
119 ) {
120 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
121 return;
122 }
123
124 let desc = if self.with_description { lint.description() } else { "" };
125 let mut diag: DiagBuilder<'_, ()> = self
126 .sess
127 .dcx
128 .diag(lint.severity().into(), desc)
129 .code(DiagId::new_str(lint.id()))
130 .span(MultiSpan::from_span(span));
131
132 diag = match suggestion.kind {
133 SuggestionKind::Fix { span: fix_span, applicability, style } => diag
134 .span_suggestion_with_style(
135 fix_span.unwrap_or(span),
136 suggestion.desc.unwrap_or_default(),
137 suggestion.content,
138 applicability,
139 style,
140 ),
141 SuggestionKind::Example => {
142 if let Some(note) = suggestion.to_note() {
143 diag.note(note.iter().map(|l| l.0.as_str()).collect::<String>())
144 } else {
145 diag
146 }
147 }
148 };
149
150 diag = self.add_help(diag, lint.help());
151
152 diag.emit();
153 }
154
155 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
157 self.sess.source_map().span_to_snippet(span).ok()
158 }
159
160 pub fn get_span_indentation(&self, span: Span) -> usize {
162 if !span.is_dummy() {
163 let loc = self.sess.source_map().lookup_char_pos(span.lo());
165 if let Some(line_text) = loc.file.get_line(loc.line) {
166 let col_offset = loc.col.to_usize();
167 if col_offset <= line_text.len() {
168 let prev_text = &line_text[..col_offset];
169 return prev_text.len() - prev_text.trim().len();
170 }
171 }
172 }
173
174 0
175 }
176}
177
178#[derive(Debug, Clone, Eq, PartialEq)]
179pub enum SuggestionKind {
180 Example,
184
185 Fix {
188 span: Option<Span>,
191 applicability: Applicability,
193 style: SuggestionStyle,
195 },
196}
197
198#[derive(Debug, Clone, Eq, PartialEq)]
203pub struct Suggestion {
204 desc: Option<&'static str>,
206 content: String,
208 kind: SuggestionKind,
210}
211
212impl Suggestion {
213 pub fn example(content: String) -> Self {
215 Self { desc: None, content, kind: SuggestionKind::Example }
216 }
217
218 pub fn fix(content: String, applicability: Applicability) -> Self {
222 Self {
223 desc: None,
224 content,
225 kind: SuggestionKind::Fix {
226 span: None,
227 applicability,
228 style: SuggestionStyle::ShowCode,
229 },
230 }
231 }
232
233 pub fn with_desc(mut self, desc: &'static str) -> Self {
235 self.desc = Some(desc);
236 self
237 }
238
239 pub fn with_span(mut self, span: Span) -> Self {
241 if let SuggestionKind::Fix { span: ref mut s, .. } = self.kind {
242 *s = Some(span);
243 }
244 self
245 }
246
247 pub fn with_style(mut self, style: SuggestionStyle) -> Self {
249 if let SuggestionKind::Fix { style: ref mut s, .. } = self.kind {
250 *s = style;
251 }
252 self
253 }
254
255 fn to_note(&self) -> Option<Vec<(DiagMsg, Style)>> {
256 if let SuggestionKind::Fix { .. } = &self.kind {
257 return None;
258 };
259
260 let mut output = if let Some(desc) = self.desc {
261 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
262 } else {
263 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
264 };
265
266 output.extend(
267 self.content.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
268 );
269 output.push((DiagMsg::from("\n"), Style::NoStyle));
270 Some(output)
271 }
272}
273
274fn hyperlink(url: &'static str) -> String {
276 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
277}