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 let trimmed = line.trim();
330
331 if let Some(rest) = trimmed.strip_prefix("<!--")
333 && let Some(inner) = rest.strip_suffix("-->")
334 {
335 return Self::parse_directive_content(inner.trim());
336 }
337
338 if let Some(rest) = trimmed.strip_prefix("//") {
340 return Self::parse_directive_content(rest.trim());
341 }
342
343 if let Some(rest) = trimmed.strip_prefix("/*")
345 && let Some(inner) = rest.strip_suffix("*/")
346 {
347 return Self::parse_directive_content(inner.trim());
348 }
349
350 if let Some(rest) = trimmed.strip_prefix('%') {
352 return Self::parse_directive_content(rest.trim());
353 }
354
355 None
356 }
357
358 fn parse_directive_content(
360 content: &str,
361 ) -> Option<(DirectiveKind, Vec<String>, Option<BeginOptions>)> {
362 if let Some(rest) = content.strip_prefix("lang-check-disable-next-line") {
363 let rule_ids = parse_rule_ids(rest);
364 return Some((DirectiveKind::DisableNextLine, rule_ids, None));
365 }
366
367 if let Some(rest) = content.strip_prefix("lang-check-disable") {
368 let rule_ids = parse_rule_ids(rest);
369 return Some((DirectiveKind::Disable, rule_ids, None));
370 }
371
372 if content.starts_with("lang-check-enable") {
373 return Some((DirectiveKind::Enable, Vec::new(), None));
374 }
375
376 if let Some(rest) = content.strip_prefix("lang-check-begin") {
377 let options = parse_begin_options(rest);
378 return Some((DirectiveKind::Begin, Vec::new(), Some(options)));
379 }
380
381 if content.starts_with("lang-check-end") {
382 return Some((DirectiveKind::End, Vec::new(), None));
383 }
384
385 None
386 }
387}
388
389fn parse_rule_ids(rest: &str) -> Vec<String> {
391 rest.split_whitespace()
392 .filter(|s| !s.is_empty())
393 .map(String::from)
394 .collect()
395}
396
397fn parse_begin_options(rest: &str) -> BeginOptions {
407 let mut opts = BeginOptions::default();
408
409 for token in rest.split_whitespace() {
410 if let Some(lang) = token.strip_prefix("lang:") {
411 opts.language = Some(lang.to_string());
412 } else if let Some(dtype) = token.strip_prefix("type:") {
413 opts.doc_type = Some(dtype.to_string());
414 } else if let Some(inner) = token.strip_prefix("check[")
415 && let Some(slice) = inner.strip_suffix(']')
416 && let Some((a_str, b_str)) = slice.split_once(':')
417 && let Ok(b) = b_str.parse::<usize>()
418 {
419 let a = if a_str.is_empty() {
420 0
421 } else if let Ok(v) = a_str.parse::<usize>() {
422 v
423 } else {
424 continue;
425 };
426 opts.line_slice = Some((a, b));
427 } else if let Some(pat) = token.strip_prefix("match:") {
428 let pat = pat.strip_prefix('/').unwrap_or(pat);
430 let pat = pat.strip_suffix('/').unwrap_or(pat);
431 opts.match_pattern = Some(pat.to_string());
432 } else if let Some(pat) = token.strip_prefix("exclude:") {
433 let pat = pat.strip_prefix('/').unwrap_or(pat);
434 let pat = pat.strip_suffix('/').unwrap_or(pat);
435 opts.exclude_pattern = Some(pat.to_string());
436 } else if token == "block" {
437 opts.block = true;
438 } else {
439 opts.rule_ids.push(token.to_string());
440 }
441 }
442
443 opts
444}
445
446fn line_byte_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
448 text.split('\n').scan(0usize, |offset, line| {
449 let start = *offset;
450 *offset += line.len() + 1; Some((start, line))
452 })
453}
454
455fn next_line_start(text: &str, pos: usize) -> usize {
457 text[pos..].find('\n').map_or(text.len(), |nl| pos + nl + 1)
458}
459
460fn line_end_at(text: &str, pos: usize) -> usize {
462 text[pos..].find('\n').map_or(text.len(), |nl| pos + nl)
463}
464
465fn advance_n_lines(text: &str, start: usize, n: usize) -> usize {
467 let mut pos = start;
468 for _ in 0..n {
469 match text[pos..].find('\n') {
470 Some(nl) => pos = pos + nl + 1,
471 None => return text.len(),
472 }
473 }
474 pos
476}
477
478fn line_at(text: &str, pos: usize) -> &str {
480 let start = text[..pos].rfind('\n').map_or(0, |nl| nl + 1);
481 let end = text[pos..].find('\n').map_or(text.len(), |nl| pos + nl);
482 &text[start..end]
483}
484
485#[must_use]
490pub fn resolve_language<'a>(
491 byte_offset: usize,
492 regions: &'a [DirectiveRegion],
493 scope_regions: &'a [crate::scoping::ScopedRegion],
494) -> Option<&'a str> {
495 for region in regions {
497 if region.byte_range.contains(&byte_offset)
498 && let Some(ref lang) = region.options.language
499 {
500 return Some(lang.as_str());
501 }
502 }
503 crate::scoping::ScopeParser::language_at(scope_regions, byte_offset)
505}
506
507fn line_matches_filters(text: &str, byte_pos: usize, opts: &BeginOptions) -> bool {
509 let line = line_at(text, byte_pos);
510
511 if let Some(ref pat) = opts.match_pattern
512 && let Ok(re) = regex::Regex::new(pat)
513 && !re.is_match(line)
514 {
515 return false;
516 }
517
518 if let Some(ref pat) = opts.exclude_pattern
519 && let Ok(re) = regex::Regex::new(pat)
520 && re.is_match(line)
521 {
522 return false;
523 }
524
525 true
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn block_regions_parses_block_option() {
534 let text = "% lang-check-begin block\nsome prose here\n% lang-check-end\nafter";
535 let regions = IgnoreParser::block_regions(text);
536 assert_eq!(regions.len(), 1, "one block region expected");
537 let region = ®ions[0];
538 assert!(text[region.clone()].contains("some prose here"));
539 assert!(!text[region.clone()].contains("after"));
540 }
541
542 #[test]
543 fn block_regions_ignores_plain_begin() {
544 let text = "% lang-check-begin lang:fr\nbonjour\n% lang-check-end";
546 assert!(IgnoreParser::block_regions(text).is_empty());
547 }
548
549 fn make_diag(text: &str, needle: &str, rule_id: &str, unified_id: &str) -> Diagnostic {
550 let start = text.find(needle).unwrap();
551 Diagnostic {
552 start_byte: start as u32,
553 end_byte: (start + needle.len()) as u32,
554 message: "test".to_string(),
555 suggestions: vec![],
556 rule_id: rule_id.to_string(),
557 severity: 2,
558 unified_id: unified_id.to_string(),
559 confidence: 0.9,
560 }
561 }
562
563 #[test]
564 fn parse_html_disable_enable() {
565 let text = "Line one\n<!-- lang-check-disable -->\nBad text here\n<!-- lang-check-enable -->\nGood text";
566 let ranges = IgnoreParser::parse(text);
567 assert_eq!(ranges.len(), 1);
568 assert!(text[ranges[0].byte_range.clone()].contains("Bad text here"));
569 assert!(!text[ranges[0].byte_range.clone()].contains("Good text"));
570 assert!(ranges[0].rule_ids.is_empty());
571 }
572
573 #[test]
574 fn parse_disable_next_line() {
575 let text = "Line one\n<!-- lang-check-disable-next-line -->\nBad line\nGood line";
576 let ranges = IgnoreParser::parse(text);
577 assert_eq!(ranges.len(), 1);
578 assert_eq!(&text[ranges[0].byte_range.clone()], "Bad line");
579 }
580
581 #[test]
582 fn parse_disable_with_rule_id() {
583 let text =
584 "<!-- lang-check-disable spelling.typo -->\nsome text\n<!-- lang-check-enable -->";
585 let ranges = IgnoreParser::parse(text);
586 assert_eq!(ranges.len(), 1);
587 assert_eq!(ranges[0].rule_ids, vec!["spelling.typo"]);
588 }
589
590 #[test]
591 fn parse_disable_multiple_rule_ids() {
592 let text = "<!-- lang-check-disable spelling.typo grammar.article -->\ntext\n<!-- lang-check-enable -->";
593 let ranges = IgnoreParser::parse(text);
594 assert_eq!(ranges.len(), 1);
595 assert_eq!(ranges[0].rule_ids, vec!["spelling.typo", "grammar.article"]);
596 }
597
598 #[test]
599 fn parse_line_comment_format() {
600 let text = "code\n// lang-check-disable\nsome text\n// lang-check-enable\nmore code";
601 let ranges = IgnoreParser::parse(text);
602 assert_eq!(ranges.len(), 1);
603 assert!(text[ranges[0].byte_range.clone()].contains("some text"));
604 }
605
606 #[test]
607 fn parse_block_comment_format() {
608 let text = "/* lang-check-disable-next-line */\nbad line\ngood line";
609 let ranges = IgnoreParser::parse(text);
610 assert_eq!(ranges.len(), 1);
611 assert_eq!(&text[ranges[0].byte_range.clone()], "bad line");
612 }
613
614 #[test]
615 fn parse_latex_comment_format() {
616 let text = "% lang-check-disable\nbad text\n% lang-check-enable\ngood text";
617 let ranges = IgnoreParser::parse(text);
618 assert_eq!(ranges.len(), 1);
619 assert!(text[ranges[0].byte_range.clone()].contains("bad text"));
620 }
621
622 #[test]
623 fn unclosed_disable_extends_to_eof() {
624 let text = "Good text\n<!-- lang-check-disable -->\nBad text\nMore bad text";
625 let ranges = IgnoreParser::parse(text);
626 assert_eq!(ranges.len(), 1);
627 assert_eq!(ranges[0].byte_range.end, text.len());
628 }
629
630 #[test]
631 fn no_directives_no_ranges() {
632 let text = "Just normal text\nwith no directives.";
633 let ranges = IgnoreParser::parse(text);
634 assert!(ranges.is_empty());
635 }
636
637 #[test]
638 fn should_ignore_all_rules() {
639 let text = "Hello\n<!-- lang-check-disable -->\nBad text\n<!-- lang-check-enable -->\nGood";
640 let ranges = IgnoreParser::parse(text);
641 assert_eq!(ranges.len(), 1);
642
643 let d_inside = make_diag(text, "Bad", "some_rule", "spelling.typo");
644 assert!(IgnoreParser::should_ignore(&d_inside, &ranges));
645
646 let d_outside = make_diag(text, "Hello", "some_rule", "spelling.typo");
647 assert!(!IgnoreParser::should_ignore(&d_outside, &ranges));
648 }
649
650 #[test]
651 fn should_ignore_specific_rule_only() {
652 let text =
653 "<!-- lang-check-disable spelling.typo -->\nBad text\n<!-- lang-check-enable -->";
654 let ranges = IgnoreParser::parse(text);
655
656 let d_match = make_diag(text, "Bad", "harper::spelling", "spelling.typo");
657 assert!(IgnoreParser::should_ignore(&d_match, &ranges));
658
659 let d_no_match = make_diag(text, "Bad", "grammar_check", "grammar.article");
660 assert!(!IgnoreParser::should_ignore(&d_no_match, &ranges));
661 }
662
663 #[test]
664 fn disable_next_line_with_rule_id() {
665 let text = "// lang-check-disable-next-line grammar.article\nThe the error\nClean line";
666 let ranges = IgnoreParser::parse(text);
667 assert_eq!(ranges.len(), 1);
668 assert_eq!(&text[ranges[0].byte_range.clone()], "The the error");
669 assert_eq!(ranges[0].rule_ids, vec!["grammar.article"]);
670 }
671
672 #[test]
675 fn parse_begin_end_basic() {
676 let text = "Good\n<!-- lang-check-begin -->\nBad text\n<!-- lang-check-end -->\nGood";
677 let directives = IgnoreParser::parse_directives(text);
678 let resolved = IgnoreParser::resolve_all(text, &directives);
679 assert_eq!(resolved.regions.len(), 1);
680 let region_text = &text[resolved.regions[0].byte_range.clone()];
681 assert!(region_text.contains("Bad text"));
682 assert!(!region_text.contains("Good"));
683 }
684
685 #[test]
686 fn parse_begin_with_rule_ids() {
687 let text = "<!-- lang-check-begin spelling.typo -->\ntext\n<!-- lang-check-end -->";
688 let directives = IgnoreParser::parse_directives(text);
689 let resolved = IgnoreParser::resolve_all(text, &directives);
690 assert_eq!(resolved.regions.len(), 1);
691 assert_eq!(resolved.regions[0].options.rule_ids, vec!["spelling.typo"]);
692 }
693
694 #[test]
695 fn parse_begin_with_lang() {
696 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
697 let directives = IgnoreParser::parse_directives(text);
698 let resolved = IgnoreParser::resolve_all(text, &directives);
699 assert_eq!(resolved.regions.len(), 1);
700 assert_eq!(resolved.regions[0].options.language, Some("fr".to_string()));
701 }
702
703 #[test]
704 fn parse_begin_with_line_count() {
705 let text = "<!-- lang-check-begin check[:2] -->\nLine one\nLine two\nLine three";
706 let directives = IgnoreParser::parse_directives(text);
707 let resolved = IgnoreParser::resolve_all(text, &directives);
708 assert_eq!(resolved.regions.len(), 1);
709 let region_text = &text[resolved.regions[0].byte_range.clone()];
710 assert!(region_text.contains("Line one"));
711 assert!(region_text.contains("Line two"));
712 assert!(!region_text.contains("Line three"));
713 }
714
715 #[test]
716 fn parse_begin_with_match_exclude() {
717 let text =
718 "<!-- lang-check-begin match:/^>/ exclude:/TODO/ -->\ntext\n<!-- lang-check-end -->";
719 let directives = IgnoreParser::parse_directives(text);
720 let resolved = IgnoreParser::resolve_all(text, &directives);
721 assert_eq!(resolved.regions.len(), 1);
722 assert_eq!(
723 resolved.regions[0].options.match_pattern,
724 Some("^>".to_string())
725 );
726 assert_eq!(
727 resolved.regions[0].options.exclude_pattern,
728 Some("TODO".to_string())
729 );
730 }
731
732 #[test]
733 fn parse_begin_multiple_options() {
734 let text =
735 "<!-- lang-check-begin lang:de spelling.typo check[:3] -->\nZeile\nZwei\nDrei\nVier";
736 let directives = IgnoreParser::parse_directives(text);
737 let resolved = IgnoreParser::resolve_all(text, &directives);
738 assert_eq!(resolved.regions.len(), 1);
739 let opts = &resolved.regions[0].options;
740 assert_eq!(opts.language, Some("de".to_string()));
741 assert_eq!(opts.rule_ids, vec!["spelling.typo"]);
742 assert_eq!(opts.line_slice, Some((0, 3)));
743 }
744
745 #[test]
746 fn parse_begin_unclosed_extends_to_eof() {
747 let text = "Good\n<!-- lang-check-begin -->\nBad text\nMore bad text";
748 let directives = IgnoreParser::parse_directives(text);
749 let resolved = IgnoreParser::resolve_all(text, &directives);
750 assert_eq!(resolved.regions.len(), 1);
751 assert_eq!(resolved.regions[0].byte_range.end, text.len());
752 }
753
754 #[test]
755 fn begin_end_suppress_all() {
756 let text = "Good\n<!-- lang-check-begin -->\nBad text\n<!-- lang-check-end -->\nGood";
757 let directives = IgnoreParser::parse_directives(text);
758 let resolved = IgnoreParser::resolve_all(text, &directives);
759
760 let d_inside = make_diag(text, "Bad", "some_rule", "spelling.typo");
761 assert!(IgnoreParser::should_ignore_by_region(
762 &d_inside,
763 text,
764 &resolved.regions
765 ));
766
767 let d_outside = make_diag(text, "Good", "some_rule", "spelling.typo");
768 assert!(!IgnoreParser::should_ignore_by_region(
769 &d_outside,
770 text,
771 &resolved.regions
772 ));
773 }
774
775 #[test]
776 fn begin_end_suppress_specific_rule() {
777 let text = "<!-- lang-check-begin spelling.typo -->\nBad text\n<!-- lang-check-end -->";
778 let directives = IgnoreParser::parse_directives(text);
779 let resolved = IgnoreParser::resolve_all(text, &directives);
780
781 let d_match = make_diag(text, "Bad", "harper::spelling", "spelling.typo");
782 assert!(IgnoreParser::should_ignore_by_region(
783 &d_match,
784 text,
785 &resolved.regions
786 ));
787
788 let d_no_match = make_diag(text, "Bad", "grammar_check", "grammar.article");
789 assert!(!IgnoreParser::should_ignore_by_region(
790 &d_no_match,
791 text,
792 &resolved.regions
793 ));
794 }
795
796 #[test]
797 fn begin_line_count_no_end_needed() {
798 let text = "<!-- lang-check-begin check[:1] -->\nBad line\nGood line";
799 let directives = IgnoreParser::parse_directives(text);
800 let resolved = IgnoreParser::resolve_all(text, &directives);
801 assert_eq!(resolved.regions.len(), 1);
802
803 let d_bad = make_diag(text, "Bad", "r", "spelling.typo");
804 assert!(IgnoreParser::should_ignore_by_region(
805 &d_bad,
806 text,
807 &resolved.regions
808 ));
809
810 let d_good = make_diag(text, "Good", "r", "spelling.typo");
811 assert!(!IgnoreParser::should_ignore_by_region(
812 &d_good,
813 text,
814 &resolved.regions
815 ));
816 }
817
818 #[test]
819 fn begin_end_with_match_filter() {
820 let text = "<!-- lang-check-begin match:/^>/ -->\n> Quoted line\nNormal line\n<!-- lang-check-end -->";
821 let directives = IgnoreParser::parse_directives(text);
822 let resolved = IgnoreParser::resolve_all(text, &directives);
823
824 let d_quoted = make_diag(text, "Quoted", "r", "spelling.typo");
826 assert!(IgnoreParser::should_ignore_by_region(
827 &d_quoted,
828 text,
829 &resolved.regions
830 ));
831
832 let d_normal = make_diag(text, "Normal", "r", "spelling.typo");
834 assert!(!IgnoreParser::should_ignore_by_region(
835 &d_normal,
836 text,
837 &resolved.regions
838 ));
839 }
840
841 #[test]
842 fn begin_end_with_exclude_filter() {
843 let text = "<!-- lang-check-begin exclude:/TODO/ -->\nCheck this\nTODO skip this\n<!-- lang-check-end -->";
844 let directives = IgnoreParser::parse_directives(text);
845 let resolved = IgnoreParser::resolve_all(text, &directives);
846
847 let d_check = make_diag(text, "Check", "r", "spelling.typo");
849 assert!(IgnoreParser::should_ignore_by_region(
850 &d_check,
851 text,
852 &resolved.regions
853 ));
854
855 let d_todo = make_diag(text, "TODO", "r", "spelling.typo");
857 assert!(!IgnoreParser::should_ignore_by_region(
858 &d_todo,
859 text,
860 &resolved.regions
861 ));
862 }
863
864 #[test]
865 fn mixed_disable_and_begin() {
866 let text = "<!-- lang-check-disable -->\nDisabled\n<!-- lang-check-enable -->\n<!-- lang-check-begin -->\nBegin region\n<!-- lang-check-end -->\nClean";
867 let directives = IgnoreParser::parse_directives(text);
868 let resolved = IgnoreParser::resolve_all(text, &directives);
869
870 assert_eq!(resolved.ignore_ranges.len(), 1);
872 assert!(text[resolved.ignore_ranges[0].byte_range.clone()].contains("Disabled"));
873
874 assert_eq!(resolved.regions.len(), 1);
876 assert!(text[resolved.regions[0].byte_range.clone()].contains("Begin region"));
877
878 let d_disabled = make_diag(text, "Disabled", "r", "spelling.typo");
880 assert!(IgnoreParser::should_ignore(
881 &d_disabled,
882 &resolved.ignore_ranges
883 ));
884
885 let d_begin = make_diag(text, "Begin region", "r", "spelling.typo");
886 assert!(IgnoreParser::should_ignore_by_region(
887 &d_begin,
888 text,
889 &resolved.regions
890 ));
891
892 let d_clean = make_diag(text, "Clean", "r", "spelling.typo");
893 assert!(!IgnoreParser::should_ignore(
894 &d_clean,
895 &resolved.ignore_ranges
896 ));
897 assert!(!IgnoreParser::should_ignore_by_region(
898 &d_clean,
899 text,
900 &resolved.regions
901 ));
902 }
903
904 #[test]
905 fn nested_begin_end() {
906 let text = "<!-- lang-check-begin -->\nOuter\n<!-- lang-check-begin spelling.typo -->\nInner\n<!-- lang-check-end -->\nStill outer\n<!-- lang-check-end -->";
907 let directives = IgnoreParser::parse_directives(text);
908 let resolved = IgnoreParser::resolve_all(text, &directives);
909
910 assert_eq!(resolved.regions.len(), 2);
912
913 let inner = resolved
915 .regions
916 .iter()
917 .find(|r| !r.options.rule_ids.is_empty())
918 .unwrap();
919 assert_eq!(inner.options.rule_ids, vec!["spelling.typo"]);
920 assert!(text[inner.byte_range.clone()].contains("Inner"));
921
922 let outer = resolved
924 .regions
925 .iter()
926 .find(|r| r.options.rule_ids.is_empty())
927 .unwrap();
928 assert!(text[outer.byte_range.clone()].contains("Outer"));
929 assert!(text[outer.byte_range.clone()].contains("Still outer"));
930 }
931
932 #[test]
933 fn lang_override_does_not_suppress() {
934 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
936 let directives = IgnoreParser::parse_directives(text);
937 let resolved = IgnoreParser::resolve_all(text, &directives);
938
939 let d = make_diag(text, "Texte", "r", "spelling.typo");
940 assert!(!IgnoreParser::should_ignore_by_region(
941 &d,
942 text,
943 &resolved.regions
944 ));
945 }
946
947 #[test]
948 fn resolve_language_directive_takes_precedence() {
949 let text = "<!-- lang-check-begin lang:fr -->\nTexte\n<!-- lang-check-end -->";
950 let directives = IgnoreParser::parse_directives(text);
951 let resolved = IgnoreParser::resolve_all(text, &directives);
952
953 let texte_offset = text.find("Texte").unwrap();
954
955 assert_eq!(
957 resolve_language(texte_offset, &resolved.regions, &[]),
958 Some("fr")
959 );
960
961 assert_eq!(resolve_language(0, &resolved.regions, &[]), None);
963 }
964}