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