1mod early;
2mod late;
3
4pub use early::{EarlyLintPass, EarlyLintVisitor};
5pub use late::{LateLintPass, LateLintVisitor};
6
7use foundry_compilers::Language;
8use foundry_config::lint::Severity;
9use solar_interface::{
10 Session, Span,
11 diagnostics::{DiagBuilder, DiagId, DiagMsg, MultiSpan, Style},
12};
13use solar_sema::ParsingContext;
14use std::path::PathBuf;
15
16use crate::inline_config::InlineConfig;
17
18pub trait Linter: Send + Sync {
37 type Language: Language;
38 type Lint: Lint;
39
40 fn init(&self) -> Session;
41 fn early_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
42 fn late_lint<'sess>(&self, input: &[PathBuf], pcx: ParsingContext<'sess>);
43}
44
45pub trait Lint {
46 fn id(&self) -> &'static str;
47 fn severity(&self) -> Severity;
48 fn description(&self) -> &'static str;
49 fn help(&self) -> &'static str;
50}
51
52pub struct LintContext<'s> {
53 sess: &'s Session,
54 with_description: bool,
55 pub config: LinterConfig<'s>,
56 active_lints: Vec<&'static str>,
57}
58
59pub struct LinterConfig<'s> {
60 pub inline: InlineConfig,
61 pub mixed_case_exceptions: &'s [String],
62}
63
64impl<'s> LintContext<'s> {
65 pub fn new(
66 sess: &'s Session,
67 with_description: bool,
68 config: LinterConfig<'s>,
69 active_lints: Vec<&'static str>,
70 ) -> Self {
71 Self { sess, with_description, config, active_lints }
72 }
73
74 pub fn session(&self) -> &'s Session {
75 self.sess
76 }
77
78 pub fn is_lint_enabled(&self, id: &'static str) -> bool {
83 self.active_lints.contains(&id)
84 }
85
86 pub fn emit<L: Lint>(&self, lint: &'static L, span: Span) {
88 if self.config.inline.is_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
89 return;
90 }
91
92 let desc = if self.with_description { lint.description() } else { "" };
93 let diag: DiagBuilder<'_, ()> = self
94 .sess
95 .dcx
96 .diag(lint.severity().into(), desc)
97 .code(DiagId::new_str(lint.id()))
98 .span(MultiSpan::from_span(span))
99 .help(lint.help());
100
101 diag.emit();
102 }
103
104 pub fn emit_with_fix<L: Lint>(&self, lint: &'static L, span: Span, snippet: Snippet) {
109 if self.config.inline.is_disabled(span, lint.id()) || !self.is_lint_enabled(lint.id()) {
110 return;
111 }
112
113 let snippet = match snippet {
115 Snippet::Diff { desc, span: diff_span, add, trim_code } => {
116 let target_span = diff_span.unwrap_or(span);
118
119 if self.span_to_snippet(target_span).is_some() {
121 Snippet::Diff { desc, span: Some(target_span), add, trim_code }
122 } else {
123 Snippet::Block { desc, code: add }
125 }
126 }
127 block => block,
129 };
130
131 let desc = if self.with_description { lint.description() } else { "" };
132 let diag: DiagBuilder<'_, ()> = self
133 .sess
134 .dcx
135 .diag(lint.severity().into(), desc)
136 .code(DiagId::new_str(lint.id()))
137 .span(MultiSpan::from_span(span))
138 .highlighted_note(snippet.to_note(self))
139 .help(lint.help());
140
141 diag.emit();
142 }
143
144 pub fn span_to_snippet(&self, span: Span) -> Option<String> {
146 self.sess.source_map().span_to_snippet(span).ok()
147 }
148
149 pub fn get_span_indentation(&self, span: Span) -> usize {
151 if !span.is_dummy() {
152 let loc = self.sess.source_map().lookup_char_pos(span.lo());
154 if let Some(line_text) = loc.file.get_line(loc.line) {
155 let col_offset = loc.col.to_usize();
156 if col_offset <= line_text.len() {
157 let prev_text = &line_text[..col_offset];
158 return prev_text.len() - prev_text.trim().len();
159 }
160 }
161 }
162
163 0
164 }
165}
166
167#[derive(Debug, Clone, Eq, PartialEq)]
168pub enum Snippet {
169 Block {
171 desc: Option<&'static str>,
173 code: String,
175 },
176
177 Diff {
180 desc: Option<&'static str>,
182 span: Option<Span>,
185 add: String,
187 trim_code: bool,
191 },
192}
193
194impl Snippet {
195 pub fn to_note(self, ctx: &LintContext<'_>) -> Vec<(DiagMsg, Style)> {
196 let mut output = if let Some(desc) = self.desc() {
197 vec![(DiagMsg::from(desc), Style::NoStyle), (DiagMsg::from("\n\n"), Style::NoStyle)]
198 } else {
199 vec![(DiagMsg::from(" \n"), Style::NoStyle)]
200 };
201
202 match self {
203 Self::Diff { span, add, trim_code: trim, .. } => {
204 if let Some(span) = span
206 && let Some(rmv) = ctx.span_to_snippet(span)
207 {
208 let ind = ctx.get_span_indentation(span);
209 let diag_msg = |line: &str, prefix: &str, style: Style| {
210 let content = if trim { Self::trim_start_limited(line, ind) } else { line };
211 (DiagMsg::from(format!("{prefix}{content}\n")), style)
212 };
213 output.extend(rmv.lines().map(|line| diag_msg(line, "- ", Style::Removal)));
214 output.extend(add.lines().map(|line| diag_msg(line, "+ ", Style::Addition)));
215 } else {
216 output.extend(
218 add.lines()
219 .map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
220 );
221 }
222 }
223 Self::Block { code, .. } => {
224 output.extend(
225 code.lines().map(|line| (DiagMsg::from(format!("{line}\n")), Style::NoStyle)),
226 );
227 }
228 }
229 output.push((DiagMsg::from("\n"), Style::NoStyle));
230 output
231 }
232
233 pub fn desc(&self) -> Option<&'static str> {
234 match self {
235 Self::Diff { desc, .. } => *desc,
236 Self::Block { desc, .. } => *desc,
237 }
238 }
239
240 fn trim_start_limited(s: &str, max_chars: usize) -> &str {
242 let (mut chars, mut byte_offset) = (0, 0);
243 for c in s.chars() {
244 if chars >= max_chars || !c.is_whitespace() {
245 break;
246 }
247 chars += 1;
248 byte_offset += c.len_utf8();
249 }
250
251 &s[byte_offset..]
252 }
253}