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::lint::Severity;
10use solar::{
11 interface::{
12 Session, Span,
13 diagnostics::{DiagBuilder, DiagId, DiagMsg, MultiSpan, Style},
14 },
15 sema::Compiler,
16};
17use std::path::PathBuf;
18
19pub trait Linter: Send + Sync {
24 type Language: Language;
26 type Lint: Lint;
28
29 fn lint(&self, input: &[PathBuf], compiler: &mut Compiler) -> eyre::Result<()>;
34}
35
36pub trait Lint {
37 fn id(&self) -> &'static str;
38 fn severity(&self) -> Severity;
39 fn description(&self) -> &'static str;
40 fn help(&self) -> &'static str;
41}
42
43pub struct LintContext<'s, 'c> {
44 sess: &'s Session,
45 with_description: bool,
46 with_json_emitter: bool,
47 pub config: LinterConfig<'c>,
48 active_lints: Vec<&'static str>,
49}
50
51pub struct LinterConfig<'s> {
52 pub inline: &'s InlineConfig<Vec<String>>,
53 pub mixed_case_exceptions: &'s [String],
54}
55
56impl<'s, 'c> LintContext<'s, 'c> {
57 pub fn new(
58 sess: &'s Session,
59 with_description: bool,
60 with_json_emitter: bool,
61 config: LinterConfig<'c>,
62 active_lints: Vec<&'static str>,
63 ) -> Self {
64 Self { sess, with_description, with_json_emitter, config, active_lints }
65 }
66
67 pub fn session(&self) -> &'s Session {
68 self.sess
69 }
70
71 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
76 self.active_lints.contains(&id)
77 }
78
79 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
81 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
82 return;
83 }
84
85 let desc = if self.with_description { lint.description() } else { "" };
86 let mut diag: DiagBuilder<'_, ()> = self
87 .sess
88 .dcx
89 .diag(lint.severity().into(), desc)
90 .code(DiagId::new_str(lint.id()))
91 .span(MultiSpan::from_span(span));
92
93 if self.with_json_emitter {
95 diag = diag.help(lint.help());
96 } else {
97 diag = diag.help(hyperlink(lint.help()));
98 }
99
100 diag.emit();
101 }
102
103 pub fn emit_with_fix<L: Lint>(&self, lint: &'static L, span: Span, snippet: Snippet) {
108 if self.config.inline.is_id_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
109 return;
110 }
111
112 let snippet = match snippet {
114 Snippet::Diff { desc, span: diff_span, add, trim_code } => {
115 let target_span = diff_span.unwrap_or(span);
117
118 if self.span_to_snippet(target_span).is_some() {
120 Snippet::Diff { desc, span: Some(target_span), add, trim_code }
121 } else {
122 Snippet::Block { desc, code: add }
124 }
125 }
126 block => block,
128 };
129
130 let desc = if self.with_description { lint.description() } else { "" };
131 let mut diag: DiagBuilder<'_, ()> = self
132 .sess
133 .dcx
134 .diag(lint.severity().into(), desc)
135 .code(DiagId::new_str(lint.id()))
136 .span(MultiSpan::from_span(span));
137
138 if self.with_json_emitter {
140 diag = diag
141 .note(snippet.to_note(self).iter().map(|l| l.0.as_str()).collect::<String>())
142 .help(lint.help());
143 } else {
144 diag = diag.highlighted_note(snippet.to_note(self)).help(hyperlink(lint.help()));
145 }
146
147 diag.emit();
148 }
149
150 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
152 self.sess.source_map().span_to_snippet(span).ok()
153 }
154
155 pub fn get_span_indentation(&self, span: Span) -> usize {
157 if !span.is_dummy() {
158 let loc = self.sess.source_map().lookup_char_pos(span.lo());
160 if let Some(line_text) = loc.file.get_line(loc.line) {
161 let col_offset = loc.col.to_usize();
162 if col_offset <= line_text.len() {
163 let prev_text = &line_text[..col_offset];
164 return prev_text.len() - prev_text.trim().len();
165 }
166 }
167 }
168
169 0
170 }
171}
172
173#[derive(Debug, Clone, Eq, PartialEq)]
174pub enum Snippet {
175 Block {
177 desc: Option<&'static str>,
179 code: String,
181 },
182
183 Diff {
186 desc: Option<&'static str>,
188 span: Option<Span>,
191 add: String,
193 trim_code: bool,
197 },
198}
199
200impl Snippet {
201 pub fn to_note(self, ctx: &LintContext) -> Vec<(DiagMsg, Style)> {
202 let mut output = if let Some(desc) = self.desc() {
203 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
204 } else {
205 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
206 };
207
208 match self {
209 Self::Diff { span, add, trim_code: trim, .. } => {
210 if let Some(span) = span
212 && let Some(rmv) = ctx.span_to_snippet(span)
213 {
214 let ind = ctx.get_span_indentation(span);
215 let diag_msg = |line: &str, prefix: &str, style: Style| {
216 let content = if trim { Self::trim_start_limited(line, ind) } else { line };
217 (DiagMsg::from(format!("{prefix}{content}\n")), style)
218 };
219 output.extend(rmv.lines().map(|line| diag_msg(line, "- ", Style::Removal)));
220 output.extend(add.lines().map(|line| diag_msg(line, "+ ", Style::Addition)));
221 } else {
222 output.extend(
224 add.lines()
225 .map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
226 );
227 }
228 }
229 Self::Block { code, .. } => {
230 output.extend(
231 code.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
232 );
233 }
234 }
235 output.push((DiagMsg::from("\n"), Style::NoStyle));
236 output
237 }
238
239 pub fn desc(&self) -> Option<&'static str> {
240 match self {
241 Self::Diff { desc, .. } => *desc,
242 Self::Block { desc, .. } => *desc,
243 }
244 }
245
246 fn trim_start_limited(s: &str, max_chars: usize) -> &str {
248 let (mut chars, mut byte_offset) = (0, 0);
249 for c in s.chars() {
250 if chars >= max_chars || !c.is_whitespace() {
251 break;
252 }
253 chars += 1;
254 byte_offset += c.len_utf8();
255 }
256
257 &s[byte_offset..]
258 }
259}
260
261fn hyperlink(url: &'static str) -> String {
263 format!("\x1b]8;;{url}\x1b\\{url}\x1b]8;;\x1b\\")
264}