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