1use std::{
2 borrow::Cow,
3 collections::{HashMap, VecDeque},
4 hash::{Hash, Hasher},
5 sync::OnceLock,
6};
7
8use anyhow::Result;
9use bon::bon;
10use either::Either;
11use markdown::mdast::{MdxFlowExpression, Node};
12use regex::Regex;
13
14use crate::{
15 app_error::{MultiError, ParseError, ResultBoth},
16 context::Context,
17 location::{AdjustedOffset, AdjustedPoint, DenormalizedLocation, MaybeEndedLineRange},
18 parser::{CommentString, ParseResult},
19 utils::mdast::{MaybePosition, VariantName},
20};
21
22#[derive(Debug, PartialEq, Eq)]
23struct HashableMdxNode<'node> {
24 inner: &'node MdxFlowExpression,
25}
26
27impl<'node> HashableMdxNode<'node> {
28 fn new(inner: &'node MdxFlowExpression) -> Self {
29 Self { inner }
30 }
31}
32
33impl<'node> Hash for HashableMdxNode<'node> {
34 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
35 if let Some(pos) = &self.inner.position {
36 pos.start.line.hash(state);
37 pos.start.column.hash(state);
38 pos.end.line.hash(state);
39 pos.end.column.hash(state);
40 }
41 self.inner.value.hash(state);
42 }
43}
44
45fn collect_comment_pairs(root: &Node) -> Option<HashMap<HashableMdxNode<'_>, Option<&Node>>> {
48 let mut comment_q = None::<VecDeque<_>>;
49 let mut pairs = None::<HashMap<_, _>>;
50
51 fn traverse<'node>(
52 node: &'node Node,
53 comment_q: &mut Option<VecDeque<&'node MdxFlowExpression>>,
54 pairs: &mut Option<HashMap<HashableMdxNode<'node>, Option<&'node Node>>>,
55 ) {
56 match node {
57 Node::MdxFlowExpression(expr) if expr.value.is_comment() => {
58 comment_q.get_or_insert_with(VecDeque::new).push_back(expr);
59 }
60 _ => {
61 while let Some(comment) = comment_q.as_mut().and_then(|p| p.pop_front()) {
62 pairs
63 .get_or_insert_with(HashMap::new)
64 .insert(HashableMdxNode::new(comment), Some(node));
65 }
66 }
67 }
68
69 if let Some(children) = node.children() {
70 for child in children {
71 traverse(child, comment_q, pairs);
72 }
73 }
74 }
75
76 traverse(root, &mut comment_q, &mut pairs);
77
78 while let Some(comment) = comment_q.as_mut().and_then(|p| p.pop_front()) {
79 pairs
80 .get_or_insert_with(HashMap::new)
81 .insert(HashableMdxNode::new(comment), None);
82 }
83
84 pairs
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub(crate) enum RuleKey<'s> {
89 All,
90 Rule(Cow<'s, str>),
91}
92
93impl<'s> Hash for RuleKey<'s> {
94 fn hash<H: Hasher>(&self, state: &mut H) {
95 match self {
96 RuleKey::All => 0.hash(state),
97 RuleKey::Rule(rule) => rule.hash(state),
98 }
99 }
100}
101
102impl<'s> From<&'s str> for RuleKey<'s> {
103 fn from(rule: &'s str) -> Self {
104 RuleKey::Rule(Cow::Borrowed(rule))
105 }
106}
107
108impl<'s> From<String> for RuleKey<'s> {
109 fn from(rule: String) -> Self {
110 RuleKey::Rule(Cow::Owned(rule))
111 }
112}
113
114impl AsRef<str> for RuleKey<'_> {
115 fn as_ref(&self) -> &str {
116 match self {
117 RuleKey::All => "All rules",
118 RuleKey::Rule(rule) => rule,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124struct LintTimeConfigureAttr<'comment> {
125 rule_name: &'comment str,
126 attributes: Option<Cow<'comment, str>>,
127 next_line_only: bool,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131struct LintTimeConfigureInfo<'comment> {
132 attributes: LintTimeConfigureAttr<'comment>,
133 covered_range: MaybeEndedLineRange,
134}
135
136impl<'comment>
137 TryFrom<(
138 &str,
139 &'comment str,
140 Option<&'comment str>,
141 Option<&'comment str>,
142 )> for LintTimeConfigureAttr<'comment>
143{
144 type Error = ParseError;
145
146 fn try_from(
147 value: (
148 &str,
149 &'comment str,
150 Option<&'comment str>,
151 Option<&'comment str>,
152 ),
153 ) -> Result<Self, Self::Error> {
154 match value {
155 (_, "configure", Some(rule), Some(attributes)) => Ok(LintTimeConfigureAttr {
156 rule_name: rule,
157 attributes: Some(Cow::Borrowed(attributes)),
158 next_line_only: false,
159 }),
160 (_, "configure", Some(rule), None) => Ok(LintTimeConfigureAttr {
161 rule_name: rule,
162 attributes: None,
163 next_line_only: false,
164 }),
165 (_, "configure-next-line", Some(rule), Some(attributes)) => Ok(LintTimeConfigureAttr {
166 rule_name: rule,
167 attributes: Some(Cow::Borrowed(attributes)),
168 next_line_only: true,
169 }),
170 (_, "configure-next-line", Some(rule), None) => Ok(LintTimeConfigureAttr {
171 rule_name: rule,
172 attributes: None,
173 next_line_only: true,
174 }),
175 (orig, ..) => Err(ParseError::ConfigurationCommentMissingRule(
176 orig.to_string(),
177 )),
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183enum RuleToggle {
184 EnableAll,
185 EnableRule { rule: String },
186 DisableAll { next_line_only: bool },
187 DisableRule { rule: String, next_line_only: bool },
188}
189
190impl From<(&str, Option<&str>)> for RuleToggle {
191 fn from(value: (&str, Option<&str>)) -> Self {
192 match value {
193 ("enable", Some(rule)) => RuleToggle::EnableRule {
194 rule: rule.to_string(),
195 },
196 ("enable", _) => RuleToggle::EnableAll,
197 ("disable", Some(rule)) => RuleToggle::DisableRule {
198 rule: rule.to_string(),
199 next_line_only: false,
200 },
201 ("disable", None) => RuleToggle::DisableAll {
202 next_line_only: false,
203 },
204 ("disable-next-line", Some(rule)) => RuleToggle::DisableRule {
205 rule: rule.to_string(),
206 next_line_only: true,
207 },
208 ("disable-next-line", None) => RuleToggle::DisableAll {
209 next_line_only: true,
210 },
211 _ => unreachable!("Only valid toggle arguments sent from call site (hardcoded)"),
212 }
213 }
214}
215
216trait NextLineOnly {
217 fn next_line_only(&self) -> bool;
218}
219
220impl NextLineOnly for RuleToggle {
221 fn next_line_only(&self) -> bool {
222 match self {
223 RuleToggle::DisableAll { next_line_only } => *next_line_only,
224 RuleToggle::DisableRule { next_line_only, .. } => *next_line_only,
225 _ => false,
226 }
227 }
228}
229
230impl NextLineOnly for LintTimeConfigureAttr<'_> {
231 fn next_line_only(&self) -> bool {
232 self.next_line_only
233 }
234}
235
236enum ConfigurationComment<'comment> {
237 Configure(LintTimeConfigureAttr<'comment>),
238 EnableDisable(RuleToggle),
239}
240
241static CONFIG_COMMENT_REGEX: OnceLock<Regex> = OnceLock::new();
242
243#[bon]
244impl<'comment> ConfigurationComment<'comment> {
245 fn parse(value: &'comment str) -> Option<Self> {
246 let comment_string = value.as_comment()?;
247
248 let regex = CONFIG_COMMENT_REGEX.get_or_init(||
250 Regex::new(r"^supa-mdx-lint-(enable|disable|disable-next-line|configure|configure-next-line)(?:\s+(\S+)(?:\s+(.+))?)?$").expect("Hardcoded regex should not fail")
251 );
252
253 if let Some(captures) = regex.captures(comment_string) {
254 if let Some(action) = captures.get(1) {
255 match action.as_str() {
256 toggle @ ("enable" | "disable" | "disable-next-line") => {
257 let rule_toggle =
258 RuleToggle::from((toggle, captures.get(2).map(|m| m.as_str())));
259 return Some(ConfigurationComment::EnableDisable(rule_toggle));
260 }
261 configuration @ ("configure" | "configure-next-line") => {
262 return LintTimeConfigureAttr::try_from((
263 comment_string,
264 configuration,
265 captures.get(2).map(|m| m.as_str()),
266 captures.get(3).map(|m| m.as_str()),
267 ))
268 .ok()
269 .map(ConfigurationComment::Configure);
270 }
271 _ => {}
272 }
273 }
274 }
275
276 None
277 }
278
279 #[builder]
280 fn get_covered_range(
281 curr: impl MaybePosition + VariantName,
282 next: Option<impl MaybePosition + VariantName>,
283 next_line_only: bool,
284 parsed: &ParseResult,
285 ) -> Result<MaybeEndedLineRange, ParseError> {
286 let Some(pos) = curr.position() else {
287 return Err(ParseError::MissingPosition(curr.variant_name()));
288 };
289
290 let start_offset = AdjustedOffset::from_unist(&pos.start, parsed.content_start_offset());
291 let start_line = AdjustedPoint::from_adjusted_offset(&start_offset, parsed.rope());
292
293 if !next_line_only {
294 return Ok(MaybeEndedLineRange::new(start_line.row, None));
295 }
296
297 let next = next
298 .map(|next| {
299 let Some(next_pos) = next.position() else {
300 return Err(ParseError::MissingPosition(next.variant_name()));
301 };
302
303 let end_offset =
304 AdjustedOffset::from_unist(&next_pos.start, parsed.content_start_offset());
305 let end_row =
306 AdjustedPoint::from_adjusted_offset(&end_offset, parsed.rope()).row + 1;
307 let last_row = parsed.rope().line_len() - 1;
308 if end_row > last_row {
309 Ok(None)
310 } else {
311 Ok(Some(end_row))
312 }
313 })
314 .transpose()?
315 .flatten();
316
317 Ok(MaybeEndedLineRange::new(start_line.row, next))
318 }
319}
320
321#[allow(clippy::type_complexity)]
322#[derive(Debug, Default)]
323pub(crate) struct ConfigurationCommentCollection<'comment>(
324 Vec<
325 Result<
326 Either<LintTimeConfigureInfo<'comment>, (RuleToggle, MaybeEndedLineRange)>,
327 ParseError,
328 >,
329 >,
330);
331
332impl<'ast> ConfigurationCommentCollection<'ast> {
333 pub(crate) fn from_parse_result(parsed: &'ast ParseResult) -> Self {
334 let ast = parsed.ast();
335 let Some(comment_pairs) = collect_comment_pairs(ast) else {
336 return Self::default();
337 };
338 let comment_pairs = comment_pairs
339 .into_iter()
340 .filter_map(|(comment, next_node)| {
341 if let Some(config_comment) = ConfigurationComment::parse(&comment.inner.value) {
342 match config_comment {
343 ConfigurationComment::Configure(info) => {
344 match ConfigurationComment::get_covered_range()
345 .curr(comment.inner)
346 .maybe_next(next_node)
347 .next_line_only(info.next_line_only())
348 .parsed(parsed)
349 .call()
350 {
351 Ok(range) => Some(Ok(Either::Left(LintTimeConfigureInfo {
352 attributes: info,
353 covered_range: range,
354 }))),
355 Err(err) => Some(Err(err)),
356 }
357 }
358 ConfigurationComment::EnableDisable(info) => {
359 match ConfigurationComment::get_covered_range()
360 .curr(comment.inner)
361 .maybe_next(next_node)
362 .next_line_only(info.next_line_only())
363 .parsed(parsed)
364 .call()
365 {
366 Ok(range) => Some(Ok(Either::Right((info, range)))),
367 Err(err) => Some(Err(err)),
368 }
369 }
370 }
371 } else {
372 None
373 }
374 })
375 .collect();
376 Self(comment_pairs)
377 }
378
379 pub(crate) fn into_parts(
380 self,
381 ) -> ResultBoth<(LintTimeRuleConfigs<'ast>, LintDisables<'ast>), MultiError> {
382 let mut configs = LintTimeRuleConfigs::default();
383 let mut disables_builder = LintDisablesBuilder::default();
384 let mut errors = None::<MultiError>;
385 for res in self.0.into_iter() {
386 match res {
387 Ok(Either::Left(info)) => {
388 let attributes = info.attributes.attributes.clone();
389 configs
390 .entry(info.attributes.rule_name.into())
391 .or_default()
392 .push((
393 attributes.unwrap_or_default().into_owned(),
394 info.covered_range.clone(),
395 ));
396 }
397 Ok(Either::Right((info, range))) => match info {
398 RuleToggle::EnableAll => {
399 disables_builder.add_toggle(RuleKey::All, Switch::On, range.clone())
400 }
401 RuleToggle::EnableRule { rule } => {
402 disables_builder.add_toggle(rule.into(), Switch::On, range.clone())
403 }
404 RuleToggle::DisableAll { .. } => {
405 disables_builder.add_toggle(RuleKey::All, Switch::Off, range.clone())
406 }
407 RuleToggle::DisableRule { rule, .. } => {
408 disables_builder.add_toggle(rule.into(), Switch::Off, range.clone())
409 }
410 },
411 Err(err) => {
412 errors
413 .get_or_insert_with(MultiError::default)
414 .add_err(err.into());
415 }
416 }
417 }
418
419 let (disables, build_err) = disables_builder.build().split();
420 if let Some(build_err) = build_err {
421 errors
422 .get_or_insert_with(MultiError::default)
423 .add_err(Box::new(build_err));
424 }
425
426 ResultBoth::new((configs, disables), errors)
427 }
428}
429
430#[derive(Debug, Default)]
431pub(crate) struct LintTimeRuleConfigs<'key>(
432 HashMap<RuleKey<'key>, Vec<(String, MaybeEndedLineRange)>>,
433);
434
435impl<'key> std::ops::Deref for LintTimeRuleConfigs<'key> {
436 type Target = HashMap<RuleKey<'key>, Vec<(String, MaybeEndedLineRange)>>;
437
438 fn deref(&self) -> &Self::Target {
439 &self.0
440 }
441}
442
443impl std::ops::DerefMut for LintTimeRuleConfigs<'_> {
444 fn deref_mut(&mut self) -> &mut Self::Target {
445 &mut self.0
446 }
447}
448
449#[derive(Debug)]
450enum Switch {
451 On,
452 Off,
453}
454
455#[derive(Debug, Default)]
456struct LintDisablesBuilder<'key>(HashMap<RuleKey<'key>, Vec<(Switch, MaybeEndedLineRange)>>);
457
458#[derive(Debug, Default)]
459pub struct LintDisables<'key>(HashMap<RuleKey<'key>, Vec<MaybeEndedLineRange>>);
460
461#[derive(Debug)]
462enum MergeRangesResult {
463 Merged,
464 NotMerged((Switch, MaybeEndedLineRange)),
465}
466
467fn maybe_merge_ranges(
469 rule: &str,
470 last: &mut MaybeEndedLineRange,
471 curr: (Switch, MaybeEndedLineRange),
472) -> Result<MergeRangesResult, ParseError> {
473 match curr.0 {
474 Switch::On => {
475 if last.is_open_ended() {
476 last.end = Some(curr.1.start);
477 Ok(MergeRangesResult::Merged)
478 } else {
479 Err(ParseError::UnmatchedConfigurationPair(
480 format!("{rule} enabled without a matching disable comment"),
481 curr.1.start,
482 ))
483 }
484 }
485 Switch::Off => {
486 if last.overlaps_strict(&curr.1) {
487 last.end = last.end.max(curr.1.end);
488 Err(ParseError::UnmatchedConfigurationPair(format!("{rule} disabled twice in succession for overlapping ranges. This is probably not what you want and can cause the effective range to be different from expected."), curr.1.start))
489 } else {
490 Ok(MergeRangesResult::NotMerged(curr))
491 }
492 }
493 }
494}
495
496impl<'key> LintDisablesBuilder<'key> {
497 fn add_toggle(&mut self, rule_key: RuleKey<'key>, switch: Switch, range: MaybeEndedLineRange) {
498 self.0.entry(rule_key).or_default().push((switch, range));
499 }
500
501 fn build(self) -> ResultBoth<LintDisables<'key>, MultiError> {
502 let mut disables = HashMap::new();
503 let mut errors = None::<MultiError>;
504
505 for (rule_key, mut toggles) in self.0 {
506 toggles.sort_by_key(|(_, range)| range.clone());
507
508 let mut disabled_ranges = Vec::<(Switch, MaybeEndedLineRange)>::new();
509 for toggle in toggles {
510 match disabled_ranges.last_mut() {
511 Some(last) => {
512 match maybe_merge_ranges(rule_key.as_ref(), &mut last.1, toggle) {
513 Ok(MergeRangesResult::Merged) => {}
514 Ok(MergeRangesResult::NotMerged(toggle)) => {
515 disabled_ranges.push(toggle)
516 }
517 Err(err) => {
518 errors
519 .get_or_insert_with(MultiError::default)
520 .add_err(Box::new(err));
521 }
522 }
523 }
524 None if matches!(toggle.0, Switch::Off) => {
525 disabled_ranges.push(toggle);
526 }
527 None if matches!(toggle.0, Switch::On) => {
528 errors
529 .get_or_insert_with(MultiError::default)
530 .add_err(Box::new(ParseError::UnmatchedConfigurationPair(
531 format!(
532 "{} enabled with corresponding disable statement. This is a no-op",
533 rule_key.as_ref()
534 ),
535 toggle.1.start,
536 )));
537 }
538 _ => unreachable!(
539 "Compiler does not seem to know that all the toggle variations are covered"
540 ),
541 }
542 }
543 disables.insert(
544 rule_key,
545 disabled_ranges
546 .into_iter()
547 .map(|(_, range)| range)
548 .collect(),
549 );
550 }
551
552 ResultBoth::new(LintDisables(disables), errors)
553 }
554}
555
556impl<'key> LintDisables<'key> {
557 pub(crate) fn disabled_for_location(
558 &self,
559 rule_name: &str,
560 location: &DenormalizedLocation,
561 ctx: &Context,
562 ) -> bool {
563 let all_key = RuleKey::All;
564 let specific_key = RuleKey::from(rule_name);
565
566 if let Some(disabled_ranges) = self.0.get(&all_key) {
567 if disabled_ranges
568 .iter()
569 .any(|range| range.overlaps_lines(&location.offset_range, ctx.rope()))
570 {
571 return true;
572 }
573 } else if let Some(disabled_ranges) = self.0.get(&specific_key) {
574 if disabled_ranges
575 .iter()
576 .any(|range| range.overlaps_lines(&location.offset_range, ctx.rope()))
577 {
578 return true;
579 }
580 }
581
582 false
583 }
584}
585
586#[cfg(test)]
587mod test {
588
589 use markdown::mdast::Paragraph;
590
591 use crate::parse;
592
593 use super::*;
594
595 #[test]
596 fn test_collect_comment_pairs() {
597 let markdown = r#"
598{/* Comment 1 */}
599{/* Comment 2 */}
600Paragraph 1
601
602A list:
603- Item 1
604 {/* Comment 3 */}
605- Item 2
606"#;
607
608 let parse_result = parse(markdown).unwrap();
609 let mut comment_pairs = collect_comment_pairs(parse_result.ast())
610 .unwrap()
611 .into_iter()
612 .collect::<Vec<_>>();
613 comment_pairs.sort_by_key(|(comment, _)| &comment.inner.value);
614
615 let first = comment_pairs.first().unwrap();
616 assert_eq!(first.0.inner.value, "/* Comment 1 */");
617 match first.1 {
618 Some(Node::Paragraph(Paragraph { children, .. })) => match children.get(0).unwrap() {
619 Node::Text(text) => {
620 assert_eq!(text.value, "Paragraph 1");
621 }
622 _ => {
623 panic!("Expected a text node");
624 }
625 },
626 _ => {
627 panic!("Expected a paragraph");
628 }
629 }
630
631 let second = comment_pairs.get(1).unwrap();
632 assert_eq!(second.0.inner.value, "/* Comment 2 */");
633 match second.1 {
634 Some(Node::Paragraph(Paragraph { children, .. })) => match children.get(0).unwrap() {
635 Node::Text(text) => {
636 assert_eq!(text.value, "Paragraph 1");
637 }
638 _ => {
639 panic!("Expected a text node");
640 }
641 },
642 _ => {
643 panic!("Expected a paragraph");
644 }
645 }
646
647 let third = comment_pairs.get(2).unwrap();
648 assert_eq!(third.0.inner.value, "/* Comment 3 */");
649 match third.1 {
650 Some(Node::ListItem(list_item)) => match list_item.children.get(0).unwrap() {
651 Node::Paragraph(Paragraph { children, .. }) => match children.get(0).unwrap() {
652 Node::Text(text) => {
653 assert_eq!(text.value, "Item 2");
654 }
655 _ => {
656 panic!("Expected a text node");
657 }
658 },
659 _ => {
660 panic!("Expected a paragraph");
661 }
662 },
663 _ => {
664 panic!("Expected a list item");
665 }
666 }
667 }
668
669 #[test]
670 fn rule_toggle_parse_enable_all() {
671 let value = "/* supa-mdx-lint-enable */";
672 assert!(matches!(
673 ConfigurationComment::parse(value),
674 Some(ConfigurationComment::EnableDisable(RuleToggle::EnableAll))
675 ));
676 }
677
678 #[test]
679 fn rule_toggle_parse_enable_specific_rule() {
680 let value = "/* supa-mdx-lint-enable specific-rule */";
681 assert!(matches!(
682 ConfigurationComment::parse(value),
683 Some(ConfigurationComment::EnableDisable(RuleToggle::EnableRule { rule }))
684 if rule == "specific-rule"
685 ));
686 }
687
688 #[test]
689 fn rule_toggle_parse_disable_all() {
690 let value = "/* supa-mdx-lint-disable */";
691 assert!(matches!(
692 ConfigurationComment::parse(value),
693 Some(ConfigurationComment::EnableDisable(RuleToggle::DisableAll { next_line_only }))
694 if !next_line_only
695 ));
696 }
697
698 #[test]
699 fn rule_toggle_parse_disable_specific_rule() {
700 let value = "/* supa-mdx-lint-disable specific-rule */";
701 assert!(matches!(
702 ConfigurationComment::parse(value),
703 Some(ConfigurationComment::EnableDisable(RuleToggle::DisableRule { rule, next_line_only }))
704 if rule == "specific-rule" && !next_line_only
705 ));
706 }
707
708 #[test]
709 fn rule_toggle_parse_disable_next_line_all() {
710 let value = "/* supa-mdx-lint-disable-next-line */";
711 assert!(matches!(
712 ConfigurationComment::parse(value),
713 Some(ConfigurationComment::EnableDisable(RuleToggle::DisableAll { next_line_only }))
714 if next_line_only
715 ));
716 }
717
718 #[test]
719 fn rule_toggle_parse_disable_next_line_specific_rule() {
720 let value = "/* supa-mdx-lint-disable-next-line specific-rule */";
721 assert!(matches!(
722 ConfigurationComment::parse(value),
723 Some(ConfigurationComment::EnableDisable(RuleToggle::DisableRule { rule, next_line_only }))
724 if rule == "specific-rule" && next_line_only
725 ));
726 }
727
728 #[test]
729 fn rule_toggle_parse_invalid_format() {
730 let value = "supa-mdx-lint-enable";
731 assert!(ConfigurationComment::parse(value).is_none());
732 }
733
734 #[test]
735 fn rule_toggle_parse_invalid_command() {
736 let value = "/* supa-mdx-lint-invalid */";
737 assert!(ConfigurationComment::parse(value).is_none());
738 }
739
740 #[test]
741 fn rule_toggle_parse_ignores_whitespace() {
742 let value = " /* supa-mdx-lint-enable rule-name */";
743 assert!(matches!(
744 ConfigurationComment::parse(value),
745 Some(ConfigurationComment::EnableDisable(RuleToggle::EnableRule { rule }))
746 if rule == "rule-name"
747 ));
748 }
749
750 #[test]
751 fn test_collect_lint_disables_basic() {
752 let input = r#"{/* supa-mdx-lint-disable foo */}
753Some content
754{/* supa-mdx-lint-enable foo */}"#;
755
756 let parse_result = parse(input).unwrap();
757 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
758 .into_parts()
759 .unwrap();
760
761 assert_eq!(disables.0.len(), 1);
762 assert_eq!(disables.0[&"foo".into()][0].start, 0);
763 assert_eq!(disables.0[&"foo".into()][0].end, Some(2));
764 }
765
766 #[test]
767 fn test_collect_lint_disables_multiple_rules() {
768 let input = r#"{/* supa-mdx-lint-disable foo */}
769Content
770{/* supa-mdx-lint-disable bar */}
771More content
772{/* supa-mdx-lint-enable foo */}
773{/* supa-mdx-lint-enable bar */}"#;
774
775 let parse_result = parse(input).unwrap();
776 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
777 .into_parts()
778 .unwrap();
779
780 assert_eq!(disables.0.len(), 2);
781 assert_eq!(disables.0[&"bar".into()][0].start, 2);
782 assert_eq!(disables.0[&"bar".into()][0].end, Some(5));
783 }
784
785 #[test]
786 fn test_collect_lint_disables_next_line() {
787 let input = r#"{/* supa-mdx-lint-disable-next-line foo */}
788This line is ignored
789This line is not ignored"#;
790
791 let parse_result = parse(input).unwrap();
792 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
793 .into_parts()
794 .unwrap();
795
796 assert_eq!(disables.0.len(), 1);
797 assert_eq!(disables.0[&"foo".into()][0].end, Some(2));
798 }
799
800 #[test]
801 fn test_collect_lint_disables_disable_all() {
802 let input = r#"{/* supa-mdx-lint-disable */}
803Everything here is ignored
804Still ignored
805{/* supa-mdx-lint-enable */}"#;
806
807 let parse_result = parse(input).unwrap();
808 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
809 .into_parts()
810 .unwrap();
811
812 assert_eq!(disables.0.len(), 1);
813 assert_eq!(disables.0[&RuleKey::All][0].start, 0);
814 assert_eq!(disables.0[&RuleKey::All][0].end, Some(3));
815 }
816
817 #[test]
818 fn test_collect_lint_never_reenabled() {
819 let input = r#"{/* supa-mdx-lint-disable foo */}
820Never reenabled"#;
821
822 let parse_result = parse(input).unwrap();
823 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
824 .into_parts()
825 .unwrap();
826
827 assert_eq!(disables.0.len(), 1);
828 }
829
830 #[test]
831 fn test_collect_lint_disables_invalid_enable() {
832 let input = r#"{/* supa-mdx-lint-enable foo */}
833This should error because there was no disable"#;
834
835 let parse_result = parse(input).unwrap();
836 let result = ConfigurationCommentCollection::from_parse_result(&parse_result).into_parts();
837
838 assert!(result.has_err());
839 }
840
841 #[test]
842 fn test_collect_lint_disables_skip_blank_lines() {
843 let input = r#"{/* supa-mdx-lint-disable-next-line foo */}
844
845This line is ignored
846This line is not ignored"#;
847
848 let parse_result = parse(input).unwrap();
849 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
850 .into_parts()
851 .unwrap();
852
853 assert_eq!(disables.0.len(), 1);
854 assert_eq!(disables.0[&"foo".into()][0].end, Some(3));
855 }
856
857 #[test]
858 fn test_collect_lint_disables_skip_intervening_comments() {
859 let input = r#"{/* supa-mdx-lint-disable-next-line foo */}
860
861{/* some other comment */}
862{/* supa-mdx-lint-disable-next-line bar */}
863
864This line is ignored by both foo and bar
865This line is not ignored
866"#;
867
868 let parse_result = parse(input).unwrap();
869 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
870 .into_parts()
871 .unwrap();
872
873 assert_eq!(disables.0.len(), 2);
874 assert_eq!(disables.0[&"foo".into()][0].start, 0);
875 assert_eq!(disables.0[&"foo".into()][0].end, Some(6));
876 assert_eq!(disables.0[&"bar".into()][0].start, 3);
877 assert_eq!(disables.0[&"bar".into()][0].end, Some(6));
878 }
879
880 #[test]
881 fn test_collect_lint_disables_with_frontmatter() {
882 let input = r#"---
883title: Some frontmatter
884description: Testing with frontmatter
885---
886
887{/* supa-mdx-lint-disable-next-line foo */}
888This line should be ignored by foo
889Regular content
890
891{/* supa-mdx-lint-disable bar */}
892These lines should be ignored by bar
893More content
894{/* supa-mdx-lint-enable bar */}
895
896This line should not be ignored
897"#;
898
899 let parse_result = parse(input).unwrap();
900 let (_, disables) = ConfigurationCommentCollection::from_parse_result(&parse_result)
901 .into_parts()
902 .unwrap();
903
904 assert_eq!(disables.0.len(), 2);
905
906 assert_eq!(disables.0[&"foo".into()].len(), 1);
908 assert_eq!(disables.0[&"foo".into()][0].start, 5);
909 assert_eq!(disables.0[&"foo".into()][0].end, Some(7));
910
911 assert_eq!(disables.0[&"bar".into()].len(), 1);
913 assert_eq!(disables.0[&"bar".into()][0].start, 9);
914 assert_eq!(disables.0[&"bar".into()][0].end, Some(12));
915 }
916}