1use solar::{
2 interface::{BytePos, RelativeBytePos, SourceMap, Span},
3 parse::ast::{self, Visit},
4};
5use std::{
6 collections::{HashMap, hash_map::Entry},
7 hash::Hash,
8 ops::ControlFlow,
9};
10
11#[derive(Debug, Clone, Copy)]
13struct DisabledRange<T = BytePos> {
14 lo: T,
16 hi: T,
18 block: bool,
20}
21
22impl DisabledRange<BytePos> {
23 fn includes(&self, span: Span) -> bool {
24 span.lo() >= self.lo && span.hi() <= self.hi
25 }
26}
27
28#[derive(Clone, Debug)]
30pub enum InlineConfigItem<I> {
31 DisableNextItem(I),
33 DisableLine(I),
35 DisableNextLine(I),
37 DisableStart(I),
39 DisableEnd(I),
41}
42
43impl InlineConfigItem<Vec<String>> {
44 pub fn parse(s: &str, available_ids: &[&str]) -> Result<Self, InvalidInlineConfigItem> {
46 let (disable, relevant) = s.split_once('(').unwrap_or((s, ""));
47 let ids = if relevant.is_empty() || relevant == "all)" {
48 vec!["all".to_string()]
49 } else {
50 match relevant.split_once(')') {
51 Some((id_str, _)) => id_str.split(',').map(|s| s.trim().to_string()).collect(),
52 None => return Err(InvalidInlineConfigItem::Syntax(s.into())),
53 }
54 };
55
56 let mut invalid_ids = Vec::new();
58 'ids: for id in &ids {
59 if id == "all" {
60 continue;
61 }
62 for available_id in available_ids {
63 if *available_id == id {
64 continue 'ids;
65 }
66 }
67 invalid_ids.push(id.to_owned());
68 }
69
70 if !invalid_ids.is_empty() {
71 return Err(InvalidInlineConfigItem::Ids(invalid_ids));
72 }
73
74 let res = match disable {
75 "disable-next-item" => Self::DisableNextItem(ids),
76 "disable-line" => Self::DisableLine(ids),
77 "disable-next-line" => Self::DisableNextLine(ids),
78 "disable-start" => Self::DisableStart(ids),
79 "disable-end" => Self::DisableEnd(ids),
80 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
81 };
82
83 Ok(res)
84 }
85}
86
87impl std::str::FromStr for InlineConfigItem<()> {
88 type Err = InvalidInlineConfigItem;
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 Ok(match s {
91 "disable-next-item" => Self::DisableNextItem(()),
92 "disable-line" => Self::DisableLine(()),
93 "disable-next-line" => Self::DisableNextLine(()),
94 "disable-start" => Self::DisableStart(()),
95 "disable-end" => Self::DisableEnd(()),
96 s => return Err(InvalidInlineConfigItem::Syntax(s.into())),
97 })
98 }
99}
100
101#[derive(Debug)]
102pub enum InvalidInlineConfigItem {
103 Syntax(String),
104 Ids(Vec<String>),
105}
106
107impl std::fmt::Display for InvalidInlineConfigItem {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::Syntax(s) => write!(f, "invalid inline config item: {s}"),
111 Self::Ids(ids) => {
112 write!(f, "unknown id: '{}'", ids.join("', '"))
113 }
114 }
115 }
116}
117
118pub trait ItemIdIterator {
120 type Item: Eq + Hash + Clone;
121 fn into_iter(self) -> impl IntoIterator<Item = Self::Item>;
122}
123
124impl ItemIdIterator for () {
125 type Item = ();
126 fn into_iter(self) -> impl IntoIterator<Item = Self::Item> {
127 std::iter::once(())
128 }
129}
130
131impl ItemIdIterator for Vec<String> {
132 type Item = String;
133 fn into_iter(self) -> impl IntoIterator<Item = Self::Item> {
134 self
135 }
136}
137
138#[derive(Debug, Default)]
139pub struct InlineConfig<I: ItemIdIterator> {
140 disabled_ranges: HashMap<I::Item, Vec<DisabledRange>>,
141}
142
143impl<I: ItemIdIterator> InlineConfig<I> {
144 pub fn from_ast<'ast>(
151 items: impl IntoIterator<Item = (Span, InlineConfigItem<I>)>,
152 ast: &'ast ast::SourceUnit<'ast>,
153 source_map: &SourceMap,
154 ) -> Self {
155 Self::build(items, source_map, |offset| NextItemFinder::new(offset).find(ast))
156 }
157
158 fn build(
159 items: impl IntoIterator<Item = (Span, InlineConfigItem<I>)>,
160 source_map: &SourceMap,
161 mut find_next_item: impl FnMut(BytePos) -> Option<Span>,
162 ) -> Self {
163 let mut cfg = Self::new();
164 let mut disabled_blocks = HashMap::new();
165
166 let mut prev_sp = Span::DUMMY;
167 for (sp, item) in items {
168 if cfg!(debug_assertions) {
169 assert!(sp >= prev_sp, "InlineConfig::new: unsorted items: {sp:?} < {prev_sp:?}");
170 prev_sp = sp;
171 }
172
173 cfg.disable_item(sp, item, source_map, &mut disabled_blocks, &mut find_next_item);
174 }
175
176 for (id, (_, lo, hi)) in disabled_blocks {
177 cfg.disable(id, DisabledRange { lo, hi, block: true });
178 }
179
180 cfg
181 }
182
183 fn new() -> Self {
184 Self { disabled_ranges: HashMap::new() }
185 }
186
187 fn disable_many(&mut self, ids: I, range: DisabledRange) {
188 for id in ids.into_iter() {
189 self.disable(id, range);
190 }
191 }
192
193 fn disable(&mut self, id: I::Item, range: DisabledRange) {
194 self.disabled_ranges.entry(id).or_default().push(range);
195 }
196
197 fn disable_item(
198 &mut self,
199 span: Span,
200 item: InlineConfigItem<I>,
201 source_map: &SourceMap,
202 disabled_blocks: &mut HashMap<I::Item, (usize, BytePos, BytePos)>,
203 find_next_item: &mut dyn FnMut(BytePos) -> Option<Span>,
204 ) {
205 let result = source_map.span_to_source(span).unwrap();
206 let file = result.file;
207 let comment_range = result.data;
208 let src = file.src.as_str();
209
210 #[allow(clippy::collapsible_match)]
211 match item {
212 InlineConfigItem::DisableNextItem(ids) => {
213 if let Some(next_item) = find_next_item(span.hi()) {
214 self.disable_many(
215 ids,
216 DisabledRange { lo: next_item.lo(), hi: next_item.hi(), block: false },
217 );
218 }
219 }
220 InlineConfigItem::DisableLine(ids) => {
221 let start = src[..comment_range.start].rfind('\n').unwrap_or(0);
222 let end = src[comment_range.end..]
223 .find('\n')
224 .map_or(src.len(), |i| comment_range.end + i);
225 self.disable_many(
226 ids,
227 DisabledRange {
228 lo: file.absolute_position(RelativeBytePos::from_usize(start)),
229 hi: file.absolute_position(RelativeBytePos::from_usize(end)),
230 block: false,
231 },
232 );
233 }
234 InlineConfigItem::DisableNextLine(ids) => {
235 if let Some(offset) = src[comment_range.end..].find('\n') {
236 let next_line = comment_range.end + offset + 1;
237 if next_line < src.len() {
238 let end = src[next_line..].find('\n').map_or(src.len(), |i| next_line + i);
239 self.disable_many(
240 ids,
241 DisabledRange {
242 lo: file.absolute_position(RelativeBytePos::from_usize(
243 comment_range.start,
244 )),
245 hi: file.absolute_position(RelativeBytePos::from_usize(end)),
246 block: false,
247 },
248 );
249 }
250 }
251 }
252
253 InlineConfigItem::DisableStart(ids) => {
254 for id in ids.into_iter() {
255 disabled_blocks.entry(id).and_modify(|(depth, _, _)| *depth += 1).or_insert((
256 1,
257 span.lo(),
258 file.absolute_position(RelativeBytePos::from_usize(src.len())),
260 ));
261 }
262 }
263 InlineConfigItem::DisableEnd(ids) => {
264 for id in ids.into_iter() {
265 if let Entry::Occupied(mut entry) = disabled_blocks.entry(id) {
266 let (depth, lo, _) = entry.get_mut();
267 *depth = depth.saturating_sub(1);
268
269 if *depth == 0 {
270 let lo = *lo;
271 let (id, _) = entry.remove_entry();
272
273 self.disable(id, DisabledRange { lo, hi: span.hi(), block: true });
274 }
275 }
276 }
277 }
278 }
279 }
280}
281
282impl InlineConfig<()> {
283 pub fn is_disabled(&self, span: Span) -> bool {
285 if let Some(ranges) = self.disabled_ranges.get(&()) {
286 return ranges.iter().any(|range| range.includes(span));
287 }
288 false
289 }
290
291 pub fn is_disabled_block(&self, span: Span) -> bool {
294 if let Some(ranges) = self.disabled_ranges.get(&()) {
295 return ranges.iter().any(|range| range.block && range.includes(span));
296 }
297 false
298 }
299}
300
301impl<I: ItemIdIterator> InlineConfig<I>
302where
303 I::Item: std::borrow::Borrow<str>,
304{
305 pub fn is_id_disabled(&self, span: Span, id: &str) -> bool {
308 self.is_id_disabled_inner(span, id)
309 || (id != "all" && self.is_id_disabled_inner(span, "all"))
310 }
311
312 fn is_id_disabled_inner(&self, span: Span, id: &str) -> bool {
313 if let Some(ranges) = self.disabled_ranges.get(id)
314 && ranges.iter().any(|range| range.includes(span))
315 {
316 return true;
317 }
318
319 false
320 }
321}
322
323macro_rules! find_next_item {
324 ($self:expr, $x:expr, $span:expr, $walk:ident) => {{
325 let span = $span;
326 if span.hi() < $self.offset {
328 return ControlFlow::Continue(());
329 }
330 if span.lo() > $self.offset {
332 return ControlFlow::Break(span);
333 }
334 $self.$walk($x)
336 }};
337}
338
339#[derive(Debug)]
341struct NextItemFinder {
342 offset: BytePos,
344}
345
346impl NextItemFinder {
347 const fn new(offset: BytePos) -> Self {
348 Self { offset }
349 }
350
351 fn find<'ast>(&mut self, ast: &'ast ast::SourceUnit<'ast>) -> Option<Span> {
353 match self.visit_source_unit(ast) {
354 ControlFlow::Break(span) => Some(span),
355 ControlFlow::Continue(()) => None,
356 }
357 }
358}
359
360impl<'ast> ast::Visit<'ast> for NextItemFinder {
361 type BreakValue = Span;
362
363 fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
364 find_next_item!(self, item, item.span, walk_item)
365 }
366
367 fn visit_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) -> ControlFlow<Self::BreakValue> {
368 find_next_item!(self, stmt, stmt.span, walk_stmt)
369 }
370
371 fn visit_yul_stmt(
372 &mut self,
373 stmt: &'ast ast::yul::Stmt<'ast>,
374 ) -> ControlFlow<Self::BreakValue> {
375 find_next_item!(self, stmt, stmt.span, walk_yul_stmt)
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 impl DisabledRange<usize> {
384 fn to_byte_pos(self) -> DisabledRange<BytePos> {
385 DisabledRange::<BytePos> {
386 lo: BytePos::from_usize(self.lo),
387 hi: BytePos::from_usize(self.hi),
388 block: self.block,
389 }
390 }
391
392 fn includes(&self, range: std::ops::Range<usize>) -> bool {
393 self.to_byte_pos().includes(Span::new(
394 BytePos::from_usize(range.start),
395 BytePos::from_usize(range.end),
396 ))
397 }
398 }
399
400 #[test]
401 fn test_disabled_range_includes() {
402 let strict = DisabledRange { lo: 10, hi: 20, block: false };
403 assert!(strict.includes(10..20));
404 assert!(strict.includes(12..18));
405 assert!(!strict.includes(5..15)); }
407
408 #[test]
409 fn test_inline_config_item_from_str() {
410 assert!(matches!(
411 "disable-next-item".parse::<InlineConfigItem<()>>().unwrap(),
412 InlineConfigItem::DisableNextItem(())
413 ));
414 assert!(matches!(
415 "disable-line".parse::<InlineConfigItem<()>>().unwrap(),
416 InlineConfigItem::DisableLine(())
417 ));
418 assert!(matches!(
419 "disable-start".parse::<InlineConfigItem<()>>().unwrap(),
420 InlineConfigItem::DisableStart(())
421 ));
422 assert!(matches!(
423 "disable-end".parse::<InlineConfigItem<()>>().unwrap(),
424 InlineConfigItem::DisableEnd(())
425 ));
426 assert!("invalid".parse::<InlineConfigItem<()>>().is_err());
427 }
428
429 #[test]
430 fn test_inline_config_item_parse_with_lints() {
431 let lint_ids = vec!["lint1", "lint2"];
432
433 match InlineConfigItem::parse("disable-line", &lint_ids).unwrap() {
435 InlineConfigItem::DisableLine(lints) => assert_eq!(lints, vec!["all"]),
436 _ => panic!("Wrong type"),
437 }
438
439 match InlineConfigItem::parse("disable-start(lint1)", &lint_ids).unwrap() {
441 InlineConfigItem::DisableStart(lints) => assert_eq!(lints, vec!["lint1"]),
442 _ => panic!("Wrong type"),
443 }
444
445 match InlineConfigItem::parse("disable-end(lint1, lint2)", &lint_ids).unwrap() {
447 InlineConfigItem::DisableEnd(lints) => assert_eq!(lints, vec!["lint1", "lint2"]),
448 _ => panic!("Wrong type"),
449 }
450
451 assert!(matches!(
453 InlineConfigItem::parse("disable-line(unknown)", &lint_ids),
454 Err(InvalidInlineConfigItem::Ids(_))
455 ));
456
457 assert!(matches!(
459 InlineConfigItem::parse("disable-line(lint1", &lint_ids),
460 Err(InvalidInlineConfigItem::Syntax(_))
461 ));
462 }
463}