1use solar::{
2 ast::{Item, SourceUnit, visit::Visit as VisitAst},
3 interface::SourceMap,
4 parse::ast::Span,
5 sema::hir::{self, Visit as VisitHir},
6};
7use std::{collections::HashMap, fmt, marker::PhantomData, ops::ControlFlow};
8
9#[derive(Clone, Debug)]
11pub enum InlineConfigItem {
12 DisableNextItem(Vec<String>),
14 DisableLine(Vec<String>),
16 DisableNextLine(Vec<String>),
18 DisableStart(Vec<String>),
20 DisableEnd(Vec<String>),
22}
23
24impl InlineConfigItem {
25 pub fn parse(s: &str, lint_ids: &[&str]) -> Result<Self, InvalidInlineConfigItem> {
27 let (disable, relevant) = s.split_once('(').unwrap_or((s, ""));
28 let lints = if relevant.is_empty() || relevant == "all)" {
29 vec!["all".to_string()]
30 } else {
31 match relevant.split_once(')') {
32 Some((lint, _)) => lint.split(",").map(|s| s.trim().to_string()).collect(),
33 None => return Err(InvalidInlineConfigItem::Syntax(s.into())),
34 }
35 };
36
37 let mut invalid_ids = Vec::new();
39 'ids: for id in &lints {
40 if id == "all" {
41 continue;
42 }
43 for lint in lint_ids {
44 if *lint == id {
45 continue 'ids;
46 }
47 }
48 invalid_ids.push(id.to_owned());
49 }
50
51 if !invalid_ids.is_empty() {
52 return Err(InvalidInlineConfigItem::LintIds(invalid_ids));
53 }
54
55 let res = match disable {
56 "disable-next-item" => Self::DisableNextItem(lints),
57 "disable-line" => Self::DisableLine(lints),
58 "disable-next-line" => Self::DisableNextLine(lints),
59 "disable-start" => Self::DisableStart(lints),
60 "disable-end" => Self::DisableEnd(lints),
61 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
62 };
63
64 Ok(res)
65 }
66}
67
68#[derive(Debug)]
69pub enum InvalidInlineConfigItem {
70 Syntax(String),
71 LintIds(Vec<String>),
72}
73
74impl fmt::Display for InvalidInlineConfigItem {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::Syntax(s) => write!(f, "invalid inline config item: {s}"),
78 Self::LintIds(ids) => {
79 write!(f, "unknown lint id: '{}'", ids.join("', '"))
80 }
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy)]
89struct DisabledRange {
90 start: usize,
91 end: usize,
92 loose: bool,
93}
94
95impl DisabledRange {
96 fn includes(&self, range: std::ops::Range<usize>) -> bool {
97 range.start >= self.start && (if self.loose { range.start } else { range.end } <= self.end)
98 }
99}
100
101#[derive(Debug, Default)]
103pub struct InlineConfig {
104 disabled_ranges: HashMap<String, Vec<DisabledRange>>,
105}
106
107impl InlineConfig {
108 pub fn from_ast<'ast>(
115 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
116 ast: &'ast SourceUnit<'ast>,
117 source_map: &SourceMap,
118 ) -> Self {
119 Self::build(items, source_map, |offset| NextItemFinderAst::new(offset).find(ast))
120 }
121
122 pub fn from_hir<'hir>(
129 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
130 hir: &'hir hir::Hir<'hir>,
131 source_id: hir::SourceId,
132 source_map: &SourceMap,
133 ) -> Self {
134 Self::build(items, source_map, |offset| NextItemFinderHir::new(offset, hir).find(source_id))
135 }
136
137 fn build(
138 items: impl IntoIterator<Item = (Span, InlineConfigItem)>,
139 source_map: &SourceMap,
140 mut find_next_item: impl FnMut(usize) -> Option<Span>,
141 ) -> Self {
142 let mut disabled_ranges: HashMap<String, Vec<DisabledRange>> = HashMap::new();
143 let mut disabled_blocks: HashMap<String, (usize, usize, usize)> = HashMap::new();
144
145 let mut prev_sp = Span::DUMMY;
146 for (sp, item) in items {
147 if cfg!(debug_assertions) {
148 assert!(sp >= prev_sp, "InlineConfig::new: unsorted items: {sp:?} < {prev_sp:?}");
149 prev_sp = sp;
150 }
151
152 let Ok((file, comment_range)) = source_map.span_to_source(sp) else { continue };
153 let src = file.src.as_str();
154 match item {
155 InlineConfigItem::DisableNextItem(lints) => {
156 if let Some(next_item) = find_next_item(sp.hi().to_usize()) {
157 for lint in lints {
158 disabled_ranges.entry(lint).or_default().push(DisabledRange {
159 start: next_item.lo().to_usize(),
160 end: next_item.hi().to_usize(),
161 loose: false,
162 });
163 }
164 }
165 }
166 InlineConfigItem::DisableLine(lints) => {
167 let start = src[..comment_range.start].rfind('\n').map_or(0, |i| i);
168 let end = src[comment_range.end..]
169 .find('\n')
170 .map_or(src.len(), |i| comment_range.end + i);
171
172 for lint in lints {
173 disabled_ranges.entry(lint).or_default().push(DisabledRange {
174 start: start + file.start_pos.to_usize(),
175 end: end + file.start_pos.to_usize(),
176 loose: false,
177 })
178 }
179 }
180 InlineConfigItem::DisableNextLine(lints) => {
181 if let Some(offset) = src[comment_range.end..].find('\n') {
182 let start = comment_range.end + offset + 1;
183 if start < src.len() {
184 let end = src[start..].find('\n').map_or(src.len(), |i| start + i);
185 for lint in lints {
186 disabled_ranges.entry(lint).or_default().push(DisabledRange {
187 start: start + file.start_pos.to_usize(),
188 end: end + file.start_pos.to_usize(),
189 loose: false,
190 })
191 }
192 }
193 }
194 }
195 InlineConfigItem::DisableStart(lints) => {
196 for lint in lints {
197 disabled_blocks
198 .entry(lint)
199 .and_modify(|(_, depth, _)| *depth += 1)
200 .or_insert((
201 sp.hi().to_usize(),
202 1,
203 file.start_pos.to_usize() + src.len(),
205 ));
206 }
207 }
208 InlineConfigItem::DisableEnd(lints) => {
209 for lint in lints {
210 if let Some((start, depth, _)) = disabled_blocks.get_mut(&lint) {
211 *depth = depth.saturating_sub(1);
212
213 if *depth == 0 {
214 let start = *start;
215 _ = disabled_blocks.remove(&lint);
216
217 disabled_ranges.entry(lint).or_default().push(DisabledRange {
218 start,
219 end: sp.lo().to_usize(),
220 loose: false,
221 })
222 }
223 }
224 }
225 }
226 }
227 }
228
229 for (lint, (start, _, file_end)) in disabled_blocks {
230 disabled_ranges.entry(lint).or_default().push(DisabledRange {
231 start,
232 end: file_end,
233 loose: false,
234 });
235 }
236
237 Self { disabled_ranges }
238 }
239
240 pub fn is_disabled(&self, span: Span, lint: &str) -> bool {
242 if let Some(ranges) = self.disabled_ranges.get(lint) {
243 return ranges.iter().any(|range| range.includes(span.to_range()));
244 }
245
246 if let Some(ranges) = self.disabled_ranges.get("all") {
247 return ranges.iter().any(|range| range.includes(span.to_range()));
248 }
249
250 false
251 }
252}
253
254#[derive(Debug, Default)]
256struct NextItemFinderAst<'ast> {
257 offset: usize,
259 _pd: PhantomData<&'ast ()>,
260}
261
262impl<'ast> NextItemFinderAst<'ast> {
263 fn new(offset: usize) -> Self {
264 Self { offset, _pd: PhantomData }
265 }
266
267 fn find(&mut self, ast: &'ast SourceUnit<'ast>) -> Option<Span> {
269 match self.visit_source_unit(ast) {
270 ControlFlow::Break(span) => Some(span),
271 ControlFlow::Continue(()) => None,
272 }
273 }
274}
275
276impl<'ast> VisitAst<'ast> for NextItemFinderAst<'ast> {
277 type BreakValue = Span;
278
279 fn visit_item(&mut self, item: &'ast Item<'ast>) -> ControlFlow<Self::BreakValue> {
280 if item.span.lo().to_usize() > self.offset {
282 return ControlFlow::Break(item.span);
283 }
284
285 self.walk_item(item)
287 }
288}
289
290#[derive(Debug)]
292struct NextItemFinderHir<'hir> {
293 hir: &'hir hir::Hir<'hir>,
294 offset: usize,
296}
297
298impl<'hir> NextItemFinderHir<'hir> {
299 fn new(offset: usize, hir: &'hir hir::Hir<'hir>) -> Self {
300 Self { offset, hir }
301 }
302
303 fn find(&mut self, id: hir::SourceId) -> Option<Span> {
305 match self.visit_nested_source(id) {
306 ControlFlow::Break(span) => Some(span),
307 ControlFlow::Continue(()) => None,
308 }
309 }
310}
311
312impl<'hir> VisitHir<'hir> for NextItemFinderHir<'hir> {
313 type BreakValue = Span;
314
315 fn hir(&self) -> &'hir hir::Hir<'hir> {
316 self.hir
317 }
318
319 fn visit_item(&mut self, item: hir::Item<'hir, 'hir>) -> ControlFlow<Self::BreakValue> {
320 if item.span().lo().to_usize() > self.offset {
322 return ControlFlow::Break(item.span());
323 }
324
325 if item.span().hi().to_usize() < self.offset {
327 return ControlFlow::Continue(());
328 }
329
330 self.walk_item(item)
332 }
333}