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