1use solar::{
2 interface::{BytePos, RelativeBytePos, SourceMap, Span},
3 parse::ast::{self, Visit},
4};
5use std::{
6 borrow::Borrow,
7 collections::{HashMap, HashSet},
8 hash::Hash,
9 ops::ControlFlow,
10 sync::atomic::{AtomicBool, Ordering},
11};
12
13#[derive(Debug)]
15struct DisabledRange<T = BytePos> {
16 range: Option<(T, T)>,
18 block: bool,
20 directive: Span,
22 used: AtomicBool,
24}
25
26impl DisabledRange<BytePos> {
27 fn includes(&self, span: Span) -> bool {
28 self.range.is_some_and(|(lo, hi)| span.lo() >= lo && span.hi() <= hi)
29 }
30
31 fn mark_if_includes(&self, span: Span) -> bool {
33 if self.includes(span) {
34 self.used.store(true, Ordering::Relaxed);
35 return true;
36 }
37 false
38 }
39}
40
41#[derive(Clone, Debug)]
43pub enum InlineConfigItem<I> {
44 DisableNextItem(I),
46 DisableLine(I),
48 DisableNextLine(I),
50 DisableStart(I),
52 DisableEnd(I),
54}
55
56impl InlineConfigItem<Vec<String>> {
57 pub fn parse(s: &str, available_ids: &[&str]) -> Result<Self, InvalidInlineConfigItem> {
59 let (disable, relevant) = s.split_once('(').unwrap_or((s, ""));
60 let mut ids = if relevant.is_empty() || relevant == "all)" {
61 vec!["all".to_string()]
62 } else {
63 match relevant.split_once(')') {
64 Some((id_str, _)) => id_str.split(',').map(|s| s.trim().to_string()).collect(),
65 None => return Err(InvalidInlineConfigItem::Syntax(s.into())),
66 }
67 };
68 let mut seen = HashSet::new();
69 ids.retain(|id| seen.insert(id.clone()));
70
71 let mut invalid_ids = Vec::new();
73 'ids: for id in &ids {
74 if id == "all" {
75 continue;
76 }
77 for available_id in available_ids {
78 if *available_id == id {
79 continue 'ids;
80 }
81 }
82 invalid_ids.push(id.to_owned());
83 }
84
85 if !invalid_ids.is_empty() {
86 return Err(InvalidInlineConfigItem::Ids(invalid_ids));
87 }
88
89 let res = match disable {
90 "disable-next-item" => Self::DisableNextItem(ids),
91 "disable-line" => Self::DisableLine(ids),
92 "disable-next-line" => Self::DisableNextLine(ids),
93 "disable-start" => Self::DisableStart(ids),
94 "disable-end" => Self::DisableEnd(ids),
95 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
96 };
97
98 Ok(res)
99 }
100}
101
102impl std::str::FromStr for InlineConfigItem<()> {
103 type Err = InvalidInlineConfigItem;
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 Ok(match s {
106 "disable-next-item" => Self::DisableNextItem(()),
107 "disable-line" => Self::DisableLine(()),
108 "disable-next-line" => Self::DisableNextLine(()),
109 "disable-start" => Self::DisableStart(()),
110 "disable-end" => Self::DisableEnd(()),
111 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
112 })
113 }
114}
115
116#[derive(Debug)]
117pub enum InvalidInlineConfigItem {
118 Syntax(String),
119 Ids(Vec<String>),
120}
121
122impl std::fmt::Display for InvalidInlineConfigItem {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 match self {
125 Self::Syntax(s) => write!(f, "invalid inline config item: {s}"),
126 Self::Ids(ids) => {
127 write!(f, "unknown id: '{}'", ids.join("', '"))
128 }
129 }
130 }
131}
132
133pub trait ItemIdIterator {
135 type Item: Eq + Hash + Clone;
136 fn into_iter(self) -> impl IntoIterator<Item = Self::Item>;
137}
138
139impl ItemIdIterator for () {
140 type Item = ();
141 fn into_iter(self) -> impl IntoIterator<Item = Self::Item> {
142 std::iter::once(())
143 }
144}
145
146impl ItemIdIterator for Vec<String> {
147 type Item = String;
148 fn into_iter(self) -> impl IntoIterator<Item = Self::Item> {
149 self
150 }
151}
152
153#[derive(Debug, Default)]
154pub struct InlineConfig<I: ItemIdIterator> {
155 disabled_ranges: HashMap<I::Item, Vec<DisabledRange>>,
156}
157
158impl<I: ItemIdIterator> InlineConfig<I> {
159 pub fn from_ast<'ast>(
166 items: impl IntoIterator<Item = (Span, InlineConfigItem<I>)>,
167 ast: &'ast ast::SourceUnit<'ast>,
168 source_map: &SourceMap,
169 ) -> Self {
170 Self::build(items, source_map, |offset| NextItemFinder::new(offset).find(ast))
171 }
172
173 fn build(
174 items: impl IntoIterator<Item = (Span, InlineConfigItem<I>)>,
175 source_map: &SourceMap,
176 mut find_next_item: impl FnMut(BytePos) -> Option<Span>,
177 ) -> Self {
178 let mut cfg = Self::new();
179 let mut disabled_blocks = HashMap::<I::Item, Vec<(BytePos, BytePos, Span)>>::new();
180
181 let mut prev_sp = Span::DUMMY;
182 for (sp, item) in items {
183 if cfg!(debug_assertions) {
184 assert!(sp >= prev_sp, "InlineConfig::new: unsorted items: {sp:?} < {prev_sp:?}");
185 prev_sp = sp;
186 }
187
188 cfg.disable_item(sp, item, source_map, &mut disabled_blocks, &mut find_next_item);
189 }
190
191 for (id, blocks) in disabled_blocks {
192 for (lo, hi, directive) in blocks {
193 cfg.disable(id.clone(), Some((lo, hi)), true, directive);
194 }
195 }
196
197 cfg
198 }
199
200 fn new() -> Self {
201 Self { disabled_ranges: HashMap::new() }
202 }
203
204 fn disable_many(
205 &mut self,
206 ids: I,
207 range: Option<(BytePos, BytePos)>,
208 block: bool,
209 directive: Span,
210 ) {
211 for id in ids.into_iter() {
212 self.disable(id, range, block, directive);
213 }
214 }
215
216 fn disable(
217 &mut self,
218 id: I::Item,
219 range: Option<(BytePos, BytePos)>,
220 block: bool,
221 directive: Span,
222 ) {
223 self.disabled_ranges.entry(id).or_default().push(DisabledRange {
224 range,
225 block,
226 directive,
227 used: AtomicBool::new(false),
228 });
229 }
230
231 fn disable_item(
232 &mut self,
233 span: Span,
234 item: InlineConfigItem<I>,
235 source_map: &SourceMap,
236 disabled_blocks: &mut HashMap<I::Item, Vec<(BytePos, BytePos, Span)>>,
237 find_next_item: &mut dyn FnMut(BytePos) -> Option<Span>,
238 ) {
239 let result = source_map.span_to_source(span).unwrap();
240 let file = result.file;
241 let comment_range = result.data;
242 let src = file.src.as_str();
243
244 #[allow(clippy::collapsible_match)]
245 match item {
246 InlineConfigItem::DisableNextItem(ids) => {
247 let range = find_next_item(span.hi()).map(|item| (item.lo(), item.hi()));
248 self.disable_many(ids, range, false, span);
249 }
250 InlineConfigItem::DisableLine(ids) => {
251 let start = src[..comment_range.start].rfind('\n').unwrap_or(0);
252 let end = src[comment_range.end..]
253 .find('\n')
254 .map_or(src.len(), |i| comment_range.end + i);
255 self.disable_many(
256 ids,
257 Some((
258 file.absolute_position(RelativeBytePos::from_usize(start)),
259 file.absolute_position(RelativeBytePos::from_usize(end)),
260 )),
261 false,
262 span,
263 );
264 }
265 InlineConfigItem::DisableNextLine(ids) => {
266 let range = src[comment_range.end..].find('\n').and_then(|offset| {
267 let next_line = comment_range.end + offset + 1;
268 (next_line < src.len()).then(|| {
269 let end = src[next_line..].find('\n').map_or(src.len(), |i| next_line + i);
270 (
271 file.absolute_position(RelativeBytePos::from_usize(
272 comment_range.start,
273 )),
274 file.absolute_position(RelativeBytePos::from_usize(end)),
275 )
276 })
277 });
278 self.disable_many(ids, range, false, span);
279 }
280
281 InlineConfigItem::DisableStart(ids) => {
282 for id in ids.into_iter() {
283 disabled_blocks.entry(id).or_default().push((
284 span.lo(),
285 file.absolute_position(RelativeBytePos::from_usize(src.len())),
287 span,
288 ));
289 }
290 }
291 InlineConfigItem::DisableEnd(ids) => {
292 for id in ids.into_iter() {
293 if let Some(blocks) = disabled_blocks.get_mut(&id)
295 && let Some((lo, _, directive)) = blocks.pop()
296 {
297 self.disable(id, Some((lo, span.hi())), true, directive);
298 }
299 }
300 }
301 }
302 }
303}
304
305impl InlineConfig<()> {
306 pub fn is_disabled(&self, span: Span) -> bool {
308 if let Some(ranges) = self.disabled_ranges.get(&()) {
309 return ranges.iter().any(|range| range.includes(span));
310 }
311 false
312 }
313
314 pub fn is_disabled_block(&self, span: Span) -> bool {
317 if let Some(ranges) = self.disabled_ranges.get(&()) {
318 return ranges.iter().any(|range| range.block && range.includes(span));
319 }
320 false
321 }
322}
323
324impl<I: ItemIdIterator> InlineConfig<I>
325where
326 I::Item: Borrow<str>,
327{
328 pub fn is_id_disabled(&self, span: Span, id: &str) -> bool {
331 let id_disabled = self.is_id_disabled_inner(span, id);
332 let all_disabled = id != "all" && self.is_id_disabled_inner(span, "all");
333 id_disabled || all_disabled
334 }
335
336 fn is_id_disabled_inner(&self, span: Span, id: &str) -> bool {
337 let Some(ranges) = self.disabled_ranges.get(id) else { return false };
338 let mut disabled = false;
341 for range in ranges {
342 disabled |= range.mark_if_includes(span);
343 }
344 disabled
345 }
346
347 pub fn unused_suppressions(&self, active: &[&str]) -> Vec<(Span, String)> {
354 if active.is_empty() {
355 return Vec::new();
356 }
357 let mut unused = Vec::new();
358 for (id, ranges) in &self.disabled_ranges {
359 let id = id.borrow();
360 if id != "all" && !active.contains(&id) {
361 continue;
362 }
363 for range in ranges {
364 if !range.used.load(Ordering::Relaxed) {
365 unused.push((range.directive, id.to_string()));
366 }
367 }
368 }
369 unused.sort_by(|(a, ida), (b, idb)| a.lo().cmp(&b.lo()).then_with(|| ida.cmp(idb)));
370 unused
371 }
372}
373
374macro_rules! find_next_item {
375 ($self:expr, $x:expr, $span:expr, $walk:ident) => {{
376 let span = $span;
377 if span.hi() < $self.offset {
379 return ControlFlow::Continue(());
380 }
381 if span.lo() > $self.offset {
383 return ControlFlow::Break(span);
384 }
385 $self.$walk($x)
387 }};
388}
389
390#[derive(Debug)]
392struct NextItemFinder {
393 offset: BytePos,
395}
396
397impl NextItemFinder {
398 const fn new(offset: BytePos) -> Self {
399 Self { offset }
400 }
401
402 fn find<'ast>(&mut self, ast: &'ast ast::SourceUnit<'ast>) -> Option<Span> {
404 match self.visit_source_unit(ast) {
405 ControlFlow::Break(span) => Some(span),
406 ControlFlow::Continue(()) => None,
407 }
408 }
409}
410
411impl<'ast> ast::Visit<'ast> for NextItemFinder {
412 type BreakValue = Span;
413
414 fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
415 find_next_item!(self, item, item.span, walk_item)
416 }
417
418 fn visit_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
419 find_next_item!(self, stmt, stmt.span, walk_stmt)
420 }
421
422 fn visit_yul_stmt(
423 &mut self,
424 stmt: &'ast ast::yul::Stmt<'ast>,
425 ) -> ControlFlow<Self::BreakValue> {
426 find_next_item!(self, stmt, stmt.span, walk_yul_stmt)
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 impl DisabledRange<usize> {
435 fn to_byte_pos(&self) -> DisabledRange<BytePos> {
436 DisabledRange::<BytePos> {
437 range: self
438 .range
439 .map(|(lo, hi)| (BytePos::from_usize(lo), BytePos::from_usize(hi))),
440 block: self.block,
441 directive: Span::DUMMY,
442 used: AtomicBool::new(false),
443 }
444 }
445
446 fn includes(&self, range: std::ops::Range<usize>) -> bool {
447 self.to_byte_pos().includes(Span::new(
448 BytePos::from_usize(range.start),
449 BytePos::from_usize(range.end),
450 ))
451 }
452 }
453
454 #[test]
455 fn test_disabled_range_includes() {
456 let strict = DisabledRange {
457 range: Some((10, 20)),
458 block: false,
459 directive: Span::DUMMY,
460 used: AtomicBool::new(false),
461 };
462 assert!(strict.includes(10..20));
463 assert!(strict.includes(12..18));
464 assert!(!strict.includes(5..15)); }
466
467 #[test]
468 fn test_unused_suppressions_credits_all_overlapping_ranges() {
469 let mut config = InlineConfig::<Vec<String>>::new();
470 config.disable(
471 "lint1".to_string(),
472 Some((BytePos::from_usize(10), BytePos::from_usize(20))),
473 false,
474 Span::new(BytePos::from_usize(1), BytePos::from_usize(2)),
475 );
476 config.disable(
477 "all".to_string(),
478 Some((BytePos::from_usize(5), BytePos::from_usize(25))),
479 true,
480 Span::new(BytePos::from_usize(3), BytePos::from_usize(4)),
481 );
482 config.disable(
483 "lint1".to_string(),
484 Some((BytePos::from_usize(5), BytePos::from_usize(25))),
485 true,
486 Span::new(BytePos::from_usize(5), BytePos::from_usize(6)),
487 );
488
489 assert!(
490 config.is_id_disabled(
491 Span::new(BytePos::from_usize(12), BytePos::from_usize(18)),
492 "lint1"
493 )
494 );
495 assert!(config.unused_suppressions(&["lint1"]).is_empty());
496 }
497
498 #[test]
499 fn test_unused_suppressions_tracks_missing_targets_and_active_lints() {
500 let mut config = InlineConfig::<Vec<String>>::new();
501 let directive = Span::new(BytePos::from_usize(1), BytePos::from_usize(2));
502 config.disable_many(vec!["lint1".to_string(), "all".to_string()], None, false, directive);
503
504 assert!(config.unused_suppressions(&[]).is_empty());
505 assert_eq!(
506 config.unused_suppressions(&["lint1"]),
507 vec![(directive, "all".to_string()), (directive, "lint1".to_string())]
508 );
509 }
510
511 #[test]
512 fn test_inline_config_item_from_str() {
513 assert!(matches!(
514 "disable-next-item".parse::<InlineConfigItem<()>>().unwrap(),
515 InlineConfigItem::DisableNextItem(())
516 ));
517 assert!(matches!(
518 "disable-line".parse::<InlineConfigItem<()>>().unwrap(),
519 InlineConfigItem::DisableLine(())
520 ));
521 assert!(matches!(
522 "disable-start".parse::<InlineConfigItem<()>>().unwrap(),
523 InlineConfigItem::DisableStart(())
524 ));
525 assert!(matches!(
526 "disable-end".parse::<InlineConfigItem<()>>().unwrap(),
527 InlineConfigItem::DisableEnd(())
528 ));
529 assert!("invalid".parse::<InlineConfigItem<()>>().is_err());
530 }
531
532 #[test]
533 fn test_inline_config_item_parse_with_lints() {
534 let lint_ids = vec!["lint1", "lint2"];
535
536 match InlineConfigItem::parse("disable-line", &lint_ids).unwrap() {
538 InlineConfigItem::DisableLine(lints) => assert_eq!(lints, vec!["all"]),
539 _ => panic!("Wrong type"),
540 }
541
542 match InlineConfigItem::parse("disable-start(lint1)", &lint_ids).unwrap() {
544 InlineConfigItem::DisableStart(lints) => assert_eq!(lints, vec!["lint1"]),
545 _ => panic!("Wrong type"),
546 }
547
548 match InlineConfigItem::parse("disable-end(lint1, lint2)", &lint_ids).unwrap() {
550 InlineConfigItem::DisableEnd(lints) => assert_eq!(lints, vec!["lint1", "lint2"]),
551 _ => panic!("Wrong type"),
552 }
553
554 match InlineConfigItem::parse("disable-line(lint1, lint1)", &lint_ids).unwrap() {
556 InlineConfigItem::DisableLine(lints) => assert_eq!(lints, vec!["lint1"]),
557 _ => panic!("Wrong type"),
558 }
559
560 assert!(matches!(
562 InlineConfigItem::parse("disable-line(unknown)", &lint_ids),
563 Err(InvalidInlineConfigItem::Ids(_))
564 ));
565
566 assert!(matches!(
568 InlineConfigItem::parse("disable-line(lint1", &lint_ids),
569 Err(InvalidInlineConfigItem::Syntax(_))
570 ));
571 }
572}