1use std::ops::Range;
2
3use crate::checker::Diagnostic;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum DirectiveKind {
8 Disable,
10 Enable,
12 DisableNextLine,
14 Begin,
16 End,
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct BeginOptions {
23 pub rule_ids: Vec<String>,
25 pub language: Option<String>,
27 pub doc_type: Option<String>,
29 pub line_slice: Option<(usize, usize)>,
32 pub match_pattern: Option<String>,
34 pub exclude_pattern: Option<String>,
36 pub block: bool,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct IgnoreDirective {
46 pub line_start: usize,
48 pub line_end: usize,
50 pub kind: DirectiveKind,
52 pub rule_ids: Vec<String>,
55 pub options: Option<BeginOptions>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct IgnoreRange {
62 pub byte_range: Range<usize>,
64 pub rule_ids: Vec<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct DirectiveRegion {
71 pub directive_range: Option<Range<usize>>,
73 pub byte_range: Range<usize>,
75 pub options: BeginOptions,
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct ResolvedDirectives {
82 pub ignore_ranges: Vec<IgnoreRange>,
84 pub regions: Vec<DirectiveRegion>,
86}
87
88pub struct IgnoreParser;
91
92impl IgnoreParser {
93 #[must_use]
95 pub fn parse_directives(text: &str) -> Vec<IgnoreDirective> {
96 let mut directives = Vec::new();
97 let mut fences = crate::text_util::FenceTracker::new();
103
104 for (line_start, line) in line_byte_offsets(text) {
105 if fences.consume(line) {
106 continue;
107 }
108 let line_end = line_start + line.len();
109
110 if let Some((kind, rule_ids, options)) = Self::extract_directive(line) {
111 directives.push(IgnoreDirective {
112 line_start,
113 line_end,
114 kind,
115 rule_ids,
116 options,
117 });
118 }
119 }
120
121 directives
122 }
123
124 #[must_use]
126 pub fn resolve(text: &str, directives: &[IgnoreDirective]) -> Vec<IgnoreRange> {
127 let mut ranges = Vec::new();
128
129 let mut open_disables: Vec<&IgnoreDirective> = Vec::new();
131
132 for directive in directives {
133 match &directive.kind {
134 DirectiveKind::Disable => {
135 open_disables.push(directive);
136 }
137 DirectiveKind::Enable => {
138 if let Some(disable) = open_disables.pop() {
139 let start = next_line_start(text, disable.line_end);
142 ranges.push(IgnoreRange {
143 byte_range: start..directive.line_start,
144 rule_ids: disable.rule_ids.clone(),
145 });
146 }
147 }
148 DirectiveKind::DisableNextLine => {
149 let start = next_line_start(text, directive.line_end);
151 if start < text.len() {
152 let end = line_end_at(text, start);
153 ranges.push(IgnoreRange {
154 byte_range: start..end,
155 rule_ids: directive.rule_ids.clone(),
156 });
157 }
158 }
159 DirectiveKind::Begin | DirectiveKind::End => {}
161 }
162 }
163
164 for disable in open_disables {
166 let start = next_line_start(text, disable.line_end);
167 if start < text.len() {
168 ranges.push(IgnoreRange {
169 byte_range: start..text.len(),
170 rule_ids: disable.rule_ids.clone(),
171 });
172 }
173 }
174
175 ranges
176 }
177
178 #[must_use]
180 pub fn should_ignore(diagnostic: &Diagnostic, ranges: &[IgnoreRange]) -> bool {
181 let d_start = diagnostic.start_byte as usize;
182
183 for range in ranges {
184 if range.byte_range.contains(&d_start) {
185 if range.rule_ids.is_empty() {
187 return true;
188 }
189 if range
191 .rule_ids
192 .iter()
193 .any(|r| r == &diagnostic.unified_id || r == &diagnostic.rule_id)
194 {
195 return true;
196 }
197 }
198 }
199
200 false
201 }
202
203 #[must_use]
205 pub fn parse(text: &str) -> Vec<IgnoreRange> {
206 let directives = Self::parse_directives(text);
207 Self::resolve(text, &directives)
208 }
209
210 #[must_use]
212 pub fn resolve_all(text: &str, directives: &[IgnoreDirective]) -> ResolvedDirectives {
213 let ignore_ranges = Self::resolve(text, directives);
214 let regions = Self::resolve_regions(text, directives);
215 ResolvedDirectives {
216 ignore_ranges,
217 regions,
218 }
219 }
220
221 #[must_use]
225 pub fn block_regions(text: &str) -> Vec<Range<usize>> {
226 let directives = Self::parse_directives(text);
227 Self::resolve_regions(text, &directives)
228 .into_iter()
229 .filter(|region| region.options.block)
230 .map(|region| region.byte_range)
231 .collect()
232 }
233
234 fn resolve_regions(text: &str, directives: &[IgnoreDirective]) -> Vec<DirectiveRegion> {
235 let mut regions = Vec::new();
236 let mut open_begins: Vec<&IgnoreDirective> = Vec::new();
237
238 for directive in directives {
239 match &directive.kind {
240 DirectiveKind::Begin => {
241 let opts = directive.options.clone().unwrap_or_default();
242
243 if let Some((a, b)) = opts.line_slice {
244 let first_line = next_line_start(text, directive.line_end);
246 let start = advance_n_lines(text, first_line, a);
247 let end = advance_n_lines(text, first_line, b);
248 if start < text.len() {
249 regions.push(DirectiveRegion {
250 directive_range: Some(directive.line_start..directive.line_end),
251 byte_range: start..end,
252 options: opts,
253 });
254 }
255 } else {
256 open_begins.push(directive);
257 }
258 }
259 DirectiveKind::End => {
260 if let Some(begin) = open_begins.pop() {
261 let opts = begin.options.clone().unwrap_or_default();
262 let start = next_line_start(text, begin.line_end);
263 let end = directive.line_start;
264 if start < end {
265 regions.push(DirectiveRegion {
266 directive_range: Some(begin.line_start..begin.line_end),
267 byte_range: start..end,
268 options: opts,
269 });
270 }
271 }
272 }
273 _ => {}
274 }
275 }
276
277 for begin in open_begins {
279 let opts = begin.options.clone().unwrap_or_default();
280 let start = next_line_start(text, begin.line_end);
281 if start < text.len() {
282 regions.push(DirectiveRegion {
283 directive_range: Some(begin.line_start..begin.line_end),
284 byte_range: start..text.len(),
285 options: opts,
286 });
287 }
288 }
289
290 regions
291 }
292
293 #[must_use]
298 pub fn should_ignore_by_region(
299 diagnostic: &Diagnostic,
300 text: &str,
301 regions: &[DirectiveRegion],
302 ) -> bool {
303 let d_start = diagnostic.start_byte as usize;
304
305 for region in regions {
306 if !region.byte_range.contains(&d_start) {
307 continue;
308 }
309
310 if region.options.rule_ids.is_empty()
312 && region.options.language.is_some()
313 && region.options.match_pattern.is_none()
314 && region.options.exclude_pattern.is_none()
315 {
316 continue;
317 }
318
319 if !line_matches_filters(text, d_start, ®ion.options) {
321 continue;
322 }
323
324 if region.options.rule_ids.is_empty() {
326 return true;
327 }
328 if region
329 .options
330 .rule_ids
331 .iter()
332 .any(|r| r == &diagnostic.unified_id || r == &diagnostic.rule_id)
333 {
334 return true;
335 }
336 }
337
338 false
339 }
340
341 fn extract_directive(line: &str) -> Option<(DirectiveKind, Vec<String>, Option<BeginOptions>)> {
343 crate::text_util::in_comment(line, Self::parse_directive_content)
344 }
345
346 fn parse_directive_content(
348 content: &str,
349 ) -> Option<(DirectiveKind, Vec<String>, Option<BeginOptions>)> {
350 if let Some(rest) = content.strip_prefix("lang-check-disable-next-line") {
351 let rule_ids = parse_rule_ids(rest);
352 return Some((DirectiveKind::DisableNextLine, rule_ids, None));
353 }
354
355 if let Some(rest) = content.strip_prefix("lang-check-disable") {
356 let rule_ids = parse_rule_ids(rest);
357 return Some((DirectiveKind::Disable, rule_ids, None));
358 }
359
360 if content.starts_with("lang-check-enable") {
361 return Some((DirectiveKind::Enable, Vec::new(), None));
362 }
363
364 if let Some(rest) = content.strip_prefix("lang-check-begin") {
365 let options = parse_begin_options(rest);
366 return Some((DirectiveKind::Begin, Vec::new(), Some(options)));
367 }
368
369 if content.starts_with("lang-check-end") {
370 return Some((DirectiveKind::End, Vec::new(), None));
371 }
372
373 None
374 }
375}
376
377fn parse_rule_ids(rest: &str) -> Vec<String> {
379 rest.split_whitespace()
380 .filter(|s| !s.is_empty())
381 .map(String::from)
382 .collect()
383}
384
385fn parse_begin_options(rest: &str) -> BeginOptions {
395 let mut opts = BeginOptions::default();
396
397 for token in rest.split_whitespace() {
398 if let Some(lang) = token.strip_prefix("lang:") {
399 opts.language = Some(lang.to_string());
400 } else if let Some(dtype) = token.strip_prefix("type:") {
401 opts.doc_type = Some(dtype.to_string());
402 } else if let Some(inner) = token.strip_prefix("check[")
403 && let Some(slice) = inner.strip_suffix(']')
404 && let Some((a_str, b_str)) = slice.split_once(':')
405 && let Ok(b) = b_str.parse::<usize>()
406 {
407 let a = if a_str.is_empty() {
408 0
409 } else if let Ok(v) = a_str.parse::<usize>() {
410 v
411 } else {
412 continue;
413 };
414 opts.line_slice = Some((a, b));
415 } else if let Some(pat) = token.strip_prefix("match:") {
416 let pat = pat.strip_prefix('/').unwrap_or(pat);
418 let pat = pat.strip_suffix('/').unwrap_or(pat);
419 opts.match_pattern = Some(pat.to_string());
420 } else if let Some(pat) = token.strip_prefix("exclude:") {
421 let pat = pat.strip_prefix('/').unwrap_or(pat);
422 let pat = pat.strip_suffix('/').unwrap_or(pat);
423 opts.exclude_pattern = Some(pat.to_string());
424 } else if token == "block" {
425 opts.block = true;
426 } else {
427 opts.rule_ids.push(token.to_string());
428 }
429 }
430
431 opts
432}
433
434fn line_byte_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
436 text.split('\n').scan(0usize, |offset, line| {
437 let start = *offset;
438 *offset += line.len() + 1; Some((start, line))
440 })
441}
442
443fn next_line_start(text: &str, pos: usize) -> usize {
445 text[pos..].find('\n').map_or(text.len(), |nl| pos + nl + 1)
446}
447
448fn line_end_at(text: &str, pos: usize) -> usize {
450 text[pos..].find('\n').map_or(text.len(), |nl| pos + nl)
451}
452
453fn advance_n_lines(text: &str, start: usize, n: usize) -> usize {
455 let mut pos = start;
456 for _ in 0..n {
457 match text[pos..].find('\n') {
458 Some(nl) => pos = pos + nl + 1,
459 None => return text.len(),
460 }
461 }
462 pos
464}
465
466fn line_at(text: &str, pos: usize) -> &str {
468 let start = text[..pos].rfind('\n').map_or(0, |nl| nl + 1);
469 let end = text[pos..].find('\n').map_or(text.len(), |nl| pos + nl);
470 &text[start..end]
471}
472
473#[must_use]
478pub fn resolve_language<'a>(
479 byte_offset: usize,
480 regions: &'a [DirectiveRegion],
481 scope_regions: &'a [crate::scoping::ScopedRegion],
482) -> Option<&'a str> {
483 for region in regions {
485 if region.byte_range.contains(&byte_offset)
486 && let Some(ref lang) = region.options.language
487 {
488 return Some(lang.as_str());
489 }
490 }
491 crate::scoping::ScopeParser::language_at(scope_regions, byte_offset)
493}
494
495fn line_matches_filters(text: &str, byte_pos: usize, opts: &BeginOptions) -> bool {
497 let line = line_at(text, byte_pos);
498
499 if let Some(ref pat) = opts.match_pattern
500 && let Ok(re) = regex::Regex::new(pat)
501 && !re.is_match(line)
502 {
503 return false;
504 }
505
506 if let Some(ref pat) = opts.exclude_pattern
507 && let Ok(re) = regex::Regex::new(pat)
508 && re.is_match(line)
509 {
510 return false;
511 }
512
513 true
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn block_regions_parses_block_option() {
522 let text = "% lang-check-begin block\nsome prose here\n% lang-check-end\nafter";
523 let regions = IgnoreParser::block_regions(text);
524 assert_eq!(regions.len(), 1, "one block region expected");
525 let region = ®ions[0];
526 assert!(text[region.clone()].contains("some prose here"));
527 assert!(!text[region.clone()].contains("after"));
528 }
529
530 #[test]
531 fn block_regions_ignores_plain_begin() {
532 let text = "% lang-check-begin lang:fr\nbonjour\n% lang-check-end";
534 assert!(IgnoreParser::block_regions(text).is_empty());
535 }
536
537 fn make_diag(text: &str, needle: &str, rule_id: &str, unified_id: &str) -> Diagnostic {
538 let start = text.find(needle).unwrap();
539 Diagnostic {
540 start_byte: start as u32,
541 end_byte: (start + needle.len()) as u32,
542 message: "test".to_string(),
543 suggestions: vec![],
544 rule_id: rule_id.to_string(),
545 severity: 2,
546 unified_id: unified_id.to_string(),
547 confidence: 0.9,
548 language: String::new(),
549 pack_installable: false,
550 }
551 }
552
553 #[test]
554 fn parse_html_disable_enable() {
555 let text = "Line one\n<!-- lang-check-disable -->\nBad text here\n<!-- lang-check-enable -->\nGood text";
556 let ranges = IgnoreParser::parse(text);
557 assert_eq!(ranges.len(), 1);
558 assert!(text[ranges[0].byte_range.clone()].contains("Bad text here"));
559 assert!(!text[ranges[0].byte_range.clone()].contains("Good text"));
560 assert!(ranges[0].rule_ids.is_empty());
561 }
562
563 #[test]
564 fn parse_disable_next_line() {
565 let text = "Line one\n<!-- lang-check-disable-next-line -->\nBad line\nGood line";
566 let ranges = IgnoreParser::parse(text);
567 assert_eq!(ranges.len(), 1);
568 assert_eq!(&text[ranges[0].byte_range.clone()], "Bad line");
569 }
570
571 #[test]
572 fn parse_disable_with_rule_id() {
573 let text =
574 "<!-- lang-check-disable spelling.typo -->\nsome text\n<!-- lang-check-enable -->";
575 let ranges = IgnoreParser::parse(text);
576 assert_eq!(ranges.len(), 1);
577 assert_eq!(ranges[0].rule_ids, vec!["spelling.typo"]);
578 }
579
580 #[test]
581 fn parse_disable_multiple_rule_ids() {
582 let text = "<!-- lang-check-disable spelling.typo grammar.article -->\ntext\n<!-- lang-check-enable -->";
583 let ranges = IgnoreParser::parse(text);
584 assert_eq!(ranges.len(), 1);
585 assert_eq!(ranges[0].rule_ids, vec!["spelling.typo", "grammar.article"]);
586 }
587
588 #[test]
589 fn parse_line_comment_format() {
590 let text = "code\n// lang-check-disable\nsome text\n// lang-check-enable\nmore code";
591 let ranges = IgnoreParser::parse(text);
592 assert_eq!(ranges.len(), 1);
593 assert!(text[ranges[0].byte_range.clone()].contains("some text"));
594 }
595
596 #[test]
597 fn parse_block_comment_format() {
598 let text = "/* lang-check-disable-next-line */\nbad line\ngood line";
599 let ranges = IgnoreParser::parse(text);
600 assert_eq!(ranges.len(), 1);
601 assert_eq!(&text[ranges[0].byte_range.clone()], "bad line");
602 }
603
604 #[test]
605 fn parse_latex_comment_format() {
606 let text = "% lang-check-disable\nbad text\n% lang-check-enable\ngood text";
607 let ranges = IgnoreParser::parse(text);
608 assert_eq!(ranges.len(), 1);
609 assert!(text[ranges[0].byte_range.clone()].contains("bad text"));
610 }
611
612 #[test]
613 fn unclosed_disable_extends_to_eof() {
614 let text = "Good text\n<!-- lang-check-disable -->\nBad text\nMore bad text";
615 let ranges = IgnoreParser::parse(text);
616 assert_eq!(ranges.len(), 1);
617 assert_eq!(ranges[0].byte_range.end, text.len());
618 }
619
620 #[test]
621 fn no_directives_no_ranges() {
622 let text = "Just normal text\nwith no directives.";
623 let ranges = IgnoreParser::parse(text);
624 assert!(ranges.is_empty());
625 }
626
627 #[test]
628 fn should_ignore_all_rules() {
629 let text = "Hello\n<!-- lang-check-disable -->\nBad text\n<!-- lang-check-enable -->\nGood";
630 let ranges = IgnoreParser::parse(text);
631 assert_eq!(ranges.len(), 1);
632
633 let d_inside = make_diag(text, "Bad", "some_rule", "spelling.typo");
634 assert!(IgnoreParser::should_ignore(&d_inside, &ranges));
635
636 let d_outside = make_diag(text, "Hello", "some_rule", "spelling.typo");
637 assert!(!IgnoreParser::should_ignore(&d_outside, &ranges));
638 }
639
640 #[test]
641 fn should_ignore_specific_rule_only() {
642 let text =
643 "<!-- lang-check-disable spelling.typo -->\nBad text\n<!-- lang-check-enable -->";
644 let ranges = IgnoreParser::parse(text);
645
646 let d_match = make_diag(text, "Bad", "harper::spelling", "spelling.typo");
647 assert!(IgnoreParser::should_ignore(&d_match, &ranges));
648
649 let d_no_match = make_diag(text, "Bad", "grammar_check", "grammar.article");
650 assert!(!IgnoreParser::should_ignore(&d_no_match, &ranges));
651 }
652
653 #[test]
654 fn disable_next_line_with_rule_id() {
655 let text = "// lang-check-disable-next-line grammar.article\nThe the error\nClean line";
656 let ranges = IgnoreParser::parse(text);
657 assert_eq!(ranges.len(), 1);
658 assert_eq!(&text[ranges[0].byte_range.clone()], "The the error");
659 assert_eq!(ranges[0].rule_ids, vec!["grammar.article"]);
660 }
661
662 #[test]
665 fn parse_begin_end_basic() {
666 let text = "Good\n<!-- lang-check-begin -->\nBad text\n<!-- lang-check-end -->\nGood";
667 let directives = IgnoreParser::parse_directives(text);
668 let resolved = IgnoreParser::resolve_all(text, &directives);
669 assert_eq!(resolved.regions.len(), 1);
670 let region_text = &text[resolved.regions[0].byte_range.clone()];
671 assert!(region_text.contains("Bad text"));
672 assert!(!region_text.contains("Good"));
673 }
674
675 #[test]
676 fn parse_begin_with_rule_ids() {
677 let text = "<!-- lang-check-begin spelling.typo -->\ntext\n<!-- lang-check-end -->";
678 let directives = IgnoreParser::parse_directives(text);
679 let resolved = IgnoreParser::resolve_all(text, &directives);
680 assert_eq!(resolved.regions.len(), 1);
681 assert_eq!(resolved.regions[0].options.rule_ids, vec!["spelling.typo"]);
682 }
683
684 #[test]
685 fn parse_begin_with_lang() {
686 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
687 let directives = IgnoreParser::parse_directives(text);
688 let resolved = IgnoreParser::resolve_all(text, &directives);
689 assert_eq!(resolved.regions.len(), 1);
690 assert_eq!(resolved.regions[0].options.language, Some("fr".to_string()));
691 }
692
693 #[test]
694 fn parse_begin_with_line_count() {
695 let text = "<!-- lang-check-begin check[:2] -->\nLine one\nLine two\nLine three";
696 let directives = IgnoreParser::parse_directives(text);
697 let resolved = IgnoreParser::resolve_all(text, &directives);
698 assert_eq!(resolved.regions.len(), 1);
699 let region_text = &text[resolved.regions[0].byte_range.clone()];
700 assert!(region_text.contains("Line one"));
701 assert!(region_text.contains("Line two"));
702 assert!(!region_text.contains("Line three"));
703 }
704
705 #[test]
706 fn parse_begin_with_match_exclude() {
707 let text =
708 "<!-- lang-check-begin match:/^>/ exclude:/TODO/ -->\ntext\n<!-- lang-check-end -->";
709 let directives = IgnoreParser::parse_directives(text);
710 let resolved = IgnoreParser::resolve_all(text, &directives);
711 assert_eq!(resolved.regions.len(), 1);
712 assert_eq!(
713 resolved.regions[0].options.match_pattern,
714 Some("^>".to_string())
715 );
716 assert_eq!(
717 resolved.regions[0].options.exclude_pattern,
718 Some("TODO".to_string())
719 );
720 }
721
722 #[test]
723 fn parse_begin_multiple_options() {
724 let text =
725 "<!-- lang-check-begin lang:de spelling.typo check[:3] -->\nZeile\nZwei\nDrei\nVier";
726 let directives = IgnoreParser::parse_directives(text);
727 let resolved = IgnoreParser::resolve_all(text, &directives);
728 assert_eq!(resolved.regions.len(), 1);
729 let opts = &resolved.regions[0].options;
730 assert_eq!(opts.language, Some("de".to_string()));
731 assert_eq!(opts.rule_ids, vec!["spelling.typo"]);
732 assert_eq!(opts.line_slice, Some((0, 3)));
733 }
734
735 #[test]
736 fn parse_begin_unclosed_extends_to_eof() {
737 let text = "Good\n<!-- lang-check-begin -->\nBad text\nMore bad text";
738 let directives = IgnoreParser::parse_directives(text);
739 let resolved = IgnoreParser::resolve_all(text, &directives);
740 assert_eq!(resolved.regions.len(), 1);
741 assert_eq!(resolved.regions[0].byte_range.end, text.len());
742 }
743
744 #[test]
745 fn begin_end_suppress_all() {
746 let text = "Good\n<!-- lang-check-begin -->\nBad text\n<!-- lang-check-end -->\nGood";
747 let directives = IgnoreParser::parse_directives(text);
748 let resolved = IgnoreParser::resolve_all(text, &directives);
749
750 let d_inside = make_diag(text, "Bad", "some_rule", "spelling.typo");
751 assert!(IgnoreParser::should_ignore_by_region(
752 &d_inside,
753 text,
754 &resolved.regions
755 ));
756
757 let d_outside = make_diag(text, "Good", "some_rule", "spelling.typo");
758 assert!(!IgnoreParser::should_ignore_by_region(
759 &d_outside,
760 text,
761 &resolved.regions
762 ));
763 }
764
765 #[test]
766 fn begin_end_suppress_specific_rule() {
767 let text = "<!-- lang-check-begin spelling.typo -->\nBad text\n<!-- lang-check-end -->";
768 let directives = IgnoreParser::parse_directives(text);
769 let resolved = IgnoreParser::resolve_all(text, &directives);
770
771 let d_match = make_diag(text, "Bad", "harper::spelling", "spelling.typo");
772 assert!(IgnoreParser::should_ignore_by_region(
773 &d_match,
774 text,
775 &resolved.regions
776 ));
777
778 let d_no_match = make_diag(text, "Bad", "grammar_check", "grammar.article");
779 assert!(!IgnoreParser::should_ignore_by_region(
780 &d_no_match,
781 text,
782 &resolved.regions
783 ));
784 }
785
786 #[test]
787 fn begin_line_count_no_end_needed() {
788 let text = "<!-- lang-check-begin check[:1] -->\nBad line\nGood line";
789 let directives = IgnoreParser::parse_directives(text);
790 let resolved = IgnoreParser::resolve_all(text, &directives);
791 assert_eq!(resolved.regions.len(), 1);
792
793 let d_bad = make_diag(text, "Bad", "r", "spelling.typo");
794 assert!(IgnoreParser::should_ignore_by_region(
795 &d_bad,
796 text,
797 &resolved.regions
798 ));
799
800 let d_good = make_diag(text, "Good", "r", "spelling.typo");
801 assert!(!IgnoreParser::should_ignore_by_region(
802 &d_good,
803 text,
804 &resolved.regions
805 ));
806 }
807
808 #[test]
809 fn begin_end_with_match_filter() {
810 let text = "<!-- lang-check-begin match:/^>/ -->\n> Quoted line\nNormal line\n<!-- lang-check-end -->";
811 let directives = IgnoreParser::parse_directives(text);
812 let resolved = IgnoreParser::resolve_all(text, &directives);
813
814 let d_quoted = make_diag(text, "Quoted", "r", "spelling.typo");
816 assert!(IgnoreParser::should_ignore_by_region(
817 &d_quoted,
818 text,
819 &resolved.regions
820 ));
821
822 let d_normal = make_diag(text, "Normal", "r", "spelling.typo");
824 assert!(!IgnoreParser::should_ignore_by_region(
825 &d_normal,
826 text,
827 &resolved.regions
828 ));
829 }
830
831 #[test]
832 fn begin_end_with_exclude_filter() {
833 let text = "<!-- lang-check-begin exclude:/TODO/ -->\nCheck this\nTODO skip this\n<!-- lang-check-end -->";
834 let directives = IgnoreParser::parse_directives(text);
835 let resolved = IgnoreParser::resolve_all(text, &directives);
836
837 let d_check = make_diag(text, "Check", "r", "spelling.typo");
839 assert!(IgnoreParser::should_ignore_by_region(
840 &d_check,
841 text,
842 &resolved.regions
843 ));
844
845 let d_todo = make_diag(text, "TODO", "r", "spelling.typo");
847 assert!(!IgnoreParser::should_ignore_by_region(
848 &d_todo,
849 text,
850 &resolved.regions
851 ));
852 }
853
854 #[test]
855 fn mixed_disable_and_begin() {
856 let text = "<!-- lang-check-disable -->\nDisabled\n<!-- lang-check-enable -->\n<!-- lang-check-begin -->\nBegin region\n<!-- lang-check-end -->\nClean";
857 let directives = IgnoreParser::parse_directives(text);
858 let resolved = IgnoreParser::resolve_all(text, &directives);
859
860 assert_eq!(resolved.ignore_ranges.len(), 1);
862 assert!(text[resolved.ignore_ranges[0].byte_range.clone()].contains("Disabled"));
863
864 assert_eq!(resolved.regions.len(), 1);
866 assert!(text[resolved.regions[0].byte_range.clone()].contains("Begin region"));
867
868 let d_disabled = make_diag(text, "Disabled", "r", "spelling.typo");
870 assert!(IgnoreParser::should_ignore(
871 &d_disabled,
872 &resolved.ignore_ranges
873 ));
874
875 let d_begin = make_diag(text, "Begin region", "r", "spelling.typo");
876 assert!(IgnoreParser::should_ignore_by_region(
877 &d_begin,
878 text,
879 &resolved.regions
880 ));
881
882 let d_clean = make_diag(text, "Clean", "r", "spelling.typo");
883 assert!(!IgnoreParser::should_ignore(
884 &d_clean,
885 &resolved.ignore_ranges
886 ));
887 assert!(!IgnoreParser::should_ignore_by_region(
888 &d_clean,
889 text,
890 &resolved.regions
891 ));
892 }
893
894 #[test]
895 fn nested_begin_end() {
896 let text = "<!-- lang-check-begin -->\nOuter\n<!-- lang-check-begin spelling.typo -->\nInner\n<!-- lang-check-end -->\nStill outer\n<!-- lang-check-end -->";
897 let directives = IgnoreParser::parse_directives(text);
898 let resolved = IgnoreParser::resolve_all(text, &directives);
899
900 assert_eq!(resolved.regions.len(), 2);
902
903 let inner = resolved
905 .regions
906 .iter()
907 .find(|r| !r.options.rule_ids.is_empty())
908 .unwrap();
909 assert_eq!(inner.options.rule_ids, vec!["spelling.typo"]);
910 assert!(text[inner.byte_range.clone()].contains("Inner"));
911
912 let outer = resolved
914 .regions
915 .iter()
916 .find(|r| r.options.rule_ids.is_empty())
917 .unwrap();
918 assert!(text[outer.byte_range.clone()].contains("Outer"));
919 assert!(text[outer.byte_range.clone()].contains("Still outer"));
920 }
921
922 #[test]
923 fn lang_override_does_not_suppress() {
924 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
926 let directives = IgnoreParser::parse_directives(text);
927 let resolved = IgnoreParser::resolve_all(text, &directives);
928
929 let d = make_diag(text, "Texte", "r", "spelling.typo");
930 assert!(!IgnoreParser::should_ignore_by_region(
931 &d,
932 text,
933 &resolved.regions
934 ));
935 }
936
937 #[test]
938 fn resolve_language_directive_takes_precedence() {
939 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
940 let directives = IgnoreParser::parse_directives(text);
941 let resolved = IgnoreParser::resolve_all(text, &directives);
942
943 let texte_offset = text.find("Texte").unwrap();
944
945 assert_eq!(
947 resolve_language(texte_offset, &resolved.regions, &[]),
948 Some("fr")
949 );
950
951 assert_eq!(resolve_language(0, &resolved.regions, &[]), None);
953 }
954}