1use std::collections::{HashMap, HashSet};
23
24use crate::linter::{Fix, LintError, Severity, compute_line_starts};
25
26#[derive(Debug, Clone)]
28pub struct IgnoreWarning {
29 pub line: usize,
31 pub message: String,
33 pub fixes: Vec<Fix>,
35}
36
37#[derive(Debug, Clone)]
39struct IgnoreDirective {
40 comment_line: usize,
42 target_line: usize,
44 rule_name: String,
46 used: bool,
48 fix_start_offset: usize,
50 fix_end_offset: usize,
52 fix_replacement: String,
54}
55
56#[derive(Debug, Default)]
58pub struct IgnoreTracker {
59 ignored_lines: HashMap<usize, HashSet<String>>,
61 directives: Vec<IgnoreDirective>,
63 dormant_rules: HashSet<String>,
68}
69
70impl IgnoreTracker {
71 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn is_ignored(&self, rule: &str, line: usize) -> bool {
78 self.ignored_lines
79 .get(&line)
80 .map(|rules| rules.contains(rule))
81 .unwrap_or(false)
82 }
83
84 pub fn from_content(content: &str) -> (Self, Vec<IgnoreWarning>) {
86 Self::from_content_with_rules(content, None)
87 }
88
89 pub fn from_content_with_rules(
91 content: &str,
92 valid_rules: Option<&HashSet<String>>,
93 ) -> (Self, Vec<IgnoreWarning>) {
94 let mut tracker = Self::new();
95 let mut warnings = Vec::new();
96 let line_starts = compute_line_starts(content);
97
98 let lines: Vec<&str> = content.lines().collect();
100 let mut parsed_comments: Vec<(usize, Result<ParsedIgnoreComment, IgnoreWarning>)> =
101 Vec::new();
102
103 for (line_idx, line) in lines.iter().enumerate() {
104 let line_number = line_idx + 1; if let Some(result) = parse_ignore_comment(line, line_number) {
106 parsed_comments.push((line_idx, result));
107 }
108 }
109
110 for i in 0..parsed_comments.len() {
113 let (line_idx, ref result) = parsed_comments[i];
114
115 match result {
116 Ok(parsed) if !parsed.is_inline => {
117 let mut target_idx = line_idx + 1;
119 while target_idx < lines.len() {
120 let is_ignore_comment = parsed_comments.iter().any(|(idx, r)| {
122 *idx == target_idx && matches!(r, Ok(p) if !p.is_inline)
123 });
124 if !is_ignore_comment {
125 break;
126 }
127 target_idx += 1;
128 }
129 let actual_target_line = target_idx + 1; if let Some(valid) = valid_rules
133 && !valid.contains(&parsed.rule_name)
134 {
135 warnings.push(IgnoreWarning {
136 line: parsed.comment_line,
137 message: format!(
138 "unknown rule '{}' in nginx-lint:ignore comment",
139 parsed.rule_name
140 ),
141 fixes: Vec::new(),
142 });
143 }
144
145 tracker
146 .ignored_lines
147 .entry(actual_target_line)
148 .or_default()
149 .insert(parsed.rule_name.clone());
150
151 let comment_idx = parsed.comment_line - 1;
153 let num_lines = line_starts.len() - 1;
154 let (fix_start, fix_end) = if comment_idx + 1 < num_lines {
155 (line_starts[comment_idx], line_starts[comment_idx + 1])
157 } else if line_starts[comment_idx] > 0 {
158 (line_starts[comment_idx] - 1, line_starts[comment_idx + 1])
160 } else {
161 (line_starts[comment_idx], line_starts[comment_idx + 1])
162 };
163
164 tracker.directives.push(IgnoreDirective {
165 comment_line: parsed.comment_line,
166 target_line: actual_target_line,
167 rule_name: parsed.rule_name.clone(),
168 used: false,
169 fix_start_offset: fix_start,
170 fix_end_offset: fix_end,
171 fix_replacement: String::new(),
172 });
173 }
174 Ok(parsed) => {
175 if let Some(valid) = valid_rules
177 && !valid.contains(&parsed.rule_name)
178 {
179 warnings.push(IgnoreWarning {
180 line: parsed.comment_line,
181 message: format!(
182 "unknown rule '{}' in nginx-lint:ignore comment",
183 parsed.rule_name
184 ),
185 fixes: Vec::new(),
186 });
187 }
188
189 tracker
190 .ignored_lines
191 .entry(parsed.target_line)
192 .or_default()
193 .insert(parsed.rule_name.clone());
194
195 let comment_idx = parsed.comment_line - 1;
197 let line_start = line_starts[comment_idx];
198 let next_line_start = line_starts[comment_idx + 1];
199 let line_end = if next_line_start > line_start
200 && content.as_bytes().get(next_line_start - 1) == Some(&b'\n')
201 {
202 next_line_start - 1
203 } else {
204 next_line_start
205 };
206 let replacement = parsed.content_before_comment.clone().unwrap_or_default();
207
208 tracker.directives.push(IgnoreDirective {
209 comment_line: parsed.comment_line,
210 target_line: parsed.target_line,
211 rule_name: parsed.rule_name.clone(),
212 used: false,
213 fix_start_offset: line_start,
214 fix_end_offset: line_end,
215 fix_replacement: replacement,
216 });
217 }
218 Err(warning) => {
219 warnings.push(warning.clone());
220 }
221 }
222 }
223
224 (tracker, warnings)
225 }
226
227 fn mark_used(&mut self, rule: &str, line: usize) {
229 for directive in &mut self.directives {
230 if directive.target_line == line && directive.rule_name == rule {
231 directive.used = true;
232 }
233 }
234 }
235
236 pub fn set_dormant_rules(&mut self, dormant: &HashSet<String>) {
243 self.dormant_rules = dormant.clone();
244 }
245
246 pub fn unused_warnings(&self) -> Vec<IgnoreWarning> {
248 self.directives
249 .iter()
250 .filter(|d| !d.used && !self.dormant_rules.contains(&d.rule_name))
251 .map(|d| {
252 let fix =
253 Fix::replace_range(d.fix_start_offset, d.fix_end_offset, &d.fix_replacement);
254
255 IgnoreWarning {
256 line: d.comment_line,
257 message: format!(
258 "unused nginx-lint:ignore comment for rule '{}'",
259 d.rule_name
260 ),
261 fixes: vec![fix],
262 }
263 })
264 .collect()
265 }
266
267 #[cfg(test)]
272 pub fn add_ignore(&mut self, rule: &str, line: usize) {
273 self.ignored_lines
274 .entry(line)
275 .or_default()
276 .insert(rule.to_string());
277 self.directives.push(IgnoreDirective {
278 comment_line: line.saturating_sub(1).max(1),
279 target_line: line,
280 rule_name: rule.to_string(),
281 used: false,
282 fix_start_offset: 0,
283 fix_end_offset: 0,
284 fix_replacement: String::new(),
285 });
286 }
287}
288
289#[derive(Debug)]
291struct ParsedIgnoreComment {
292 rule_name: String,
294 target_line: usize,
296 comment_line: usize,
298 is_inline: bool,
300 content_before_comment: Option<String>,
302}
303
304fn parse_ignore_comment(
315 line: &str,
316 line_number: usize,
317) -> Option<Result<ParsedIgnoreComment, IgnoreWarning>> {
318 const IGNORE_PREFIX: &str = "nginx-lint:ignore";
319
320 let comment_start = line.find('#')?;
322 let comment_part = &line[comment_start..];
323 let comment = comment_part.trim_start_matches('#').trim();
324
325 let rest = comment.strip_prefix(IGNORE_PREFIX)?;
327 let rest = rest.trim();
328
329 let before_comment_trimmed = line[..comment_start].trim();
331 let is_inline = !before_comment_trimmed.is_empty();
332
333 let parts: Vec<&str> = rest.splitn(2, |c: char| c.is_whitespace()).collect();
335
336 if parts.is_empty() || parts[0].is_empty() {
338 return Some(Err(IgnoreWarning {
339 line: line_number,
340 message: "nginx-lint:ignore requires a rule name".to_string(),
341 fixes: Vec::new(),
342 }));
343 }
344
345 let rule_name = parts[0].to_string();
346
347 if parts.len() < 2 || parts[1].trim().is_empty() {
349 return Some(Err(IgnoreWarning {
350 line: line_number,
351 message: format!("nginx-lint:ignore {} requires a reason", rule_name),
352 fixes: Vec::new(),
353 }));
354 }
355
356 let target_line = if is_inline {
358 line_number
359 } else {
360 line_number + 1
361 };
362
363 let content_before = if is_inline {
365 Some(line[..comment_start].trim_end().to_string())
366 } else {
367 None
368 };
369
370 Some(Ok(ParsedIgnoreComment {
371 rule_name,
372 target_line,
373 comment_line: line_number,
374 is_inline,
375 content_before_comment: content_before,
376 }))
377}
378
379#[derive(Debug)]
381pub struct FilterResult {
382 pub errors: Vec<LintError>,
384 pub ignored_count: usize,
386 pub unused_warnings: Vec<IgnoreWarning>,
388}
389
390pub fn filter_errors(errors: Vec<LintError>, tracker: &mut IgnoreTracker) -> FilterResult {
392 let mut remaining = Vec::new();
393 let mut ignored_count = 0;
394
395 for error in errors {
396 if let Some(line) = error.line
397 && tracker.is_ignored(&error.rule, line)
398 {
399 tracker.mark_used(&error.rule, line);
400 ignored_count += 1;
401 continue;
402 }
403 remaining.push(error);
404 }
405
406 let unused_warnings = tracker.unused_warnings();
407
408 FilterResult {
409 errors: remaining,
410 ignored_count,
411 unused_warnings,
412 }
413}
414
415pub fn warnings_to_errors(warnings: Vec<IgnoreWarning>) -> Vec<LintError> {
417 warnings
418 .into_iter()
419 .map(|warning| {
420 let mut error = LintError::new(
421 "invalid-nginx-lint-ignore",
422 "ignore",
423 &warning.message,
424 Severity::Warning,
425 )
426 .with_location(warning.line, 1);
427
428 for fix in warning.fixes {
429 error = error.with_fix(fix);
430 }
431
432 error
433 })
434 .collect()
435}
436
437const CONTEXT_PREFIX: &str = "nginx-lint:context";
439
440pub fn parse_context_comment(content: &str) -> Option<Vec<String>> {
454 for line in content.lines().take(10) {
456 let trimmed = line.trim();
457
458 if trimmed.is_empty() {
460 continue;
461 }
462
463 if !trimmed.starts_with('#') {
465 break;
467 }
468
469 let comment = trimmed.trim_start_matches('#').trim();
470
471 if let Some(rest) = comment.strip_prefix(CONTEXT_PREFIX) {
473 let context_str = rest.trim();
474 if context_str.is_empty() {
475 return None;
476 }
477
478 let context: Vec<String> = context_str
479 .split(',')
480 .map(|s| s.trim().to_string())
481 .filter(|s| !s.is_empty())
482 .collect();
483
484 if context.is_empty() {
485 return None;
486 }
487
488 return Some(context);
489 }
490 }
491
492 None
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_parse_valid_ignore_comment() {
501 let result = parse_ignore_comment(
502 "# nginx-lint:ignore server-tokens-enabled for dev environment",
503 5,
504 );
505 assert!(result.is_some());
506 let parsed = result.unwrap().unwrap();
507 assert_eq!(parsed.rule_name, "server-tokens-enabled");
508 assert_eq!(parsed.target_line, 6); assert_eq!(parsed.comment_line, 5);
510 assert!(!parsed.is_inline);
511 assert!(parsed.content_before_comment.is_none());
512 }
513
514 #[test]
515 fn test_parse_ignore_comment_with_japanese_reason() {
516 let result =
517 parse_ignore_comment("# nginx-lint:ignore server-tokens-enabled 開発環境用", 5);
518 assert!(result.is_some());
519 let parsed = result.unwrap().unwrap();
520 assert_eq!(parsed.rule_name, "server-tokens-enabled");
521 assert_eq!(parsed.target_line, 6);
522 assert_eq!(parsed.comment_line, 5);
523 assert!(!parsed.is_inline);
524 assert!(parsed.content_before_comment.is_none());
525 }
526
527 #[test]
528 fn test_parse_missing_rule_name() {
529 let result = parse_ignore_comment("# nginx-lint:ignore", 5);
530 assert!(result.is_some());
531 let warning = result.unwrap().unwrap_err();
532 assert_eq!(warning.line, 5);
533 assert!(
534 warning
535 .message
536 .contains("nginx-lint:ignore requires a rule name")
537 );
538 }
539
540 #[test]
541 fn test_parse_missing_reason() {
542 let result = parse_ignore_comment("# nginx-lint:ignore server-tokens-enabled", 5);
543 assert!(result.is_some());
544 let warning = result.unwrap().unwrap_err();
545 assert_eq!(warning.line, 5);
546 assert!(
547 warning
548 .message
549 .contains("nginx-lint:ignore server-tokens-enabled requires a reason")
550 );
551 }
552
553 #[test]
554 fn test_parse_not_a_comment() {
555 let result = parse_ignore_comment("server_tokens on;", 5);
556 assert!(result.is_none());
557 }
558
559 #[test]
560 fn test_parse_regular_comment() {
561 let result = parse_ignore_comment("# This is a regular comment", 5);
562 assert!(result.is_none());
563 }
564
565 #[test]
566 fn test_ignore_tracker_is_ignored() {
567 let mut tracker = IgnoreTracker::new();
568 tracker.add_ignore("server-tokens-enabled", 10);
569
570 assert!(tracker.is_ignored("server-tokens-enabled", 10));
571 assert!(!tracker.is_ignored("server-tokens-enabled", 11));
572 assert!(!tracker.is_ignored("other-rule", 10));
573 }
574
575 #[test]
576 fn test_ignore_tracker_from_content() {
577 let content = r#"
578# nginx-lint:ignore server-tokens-enabled dev environment
579server_tokens on;
580"#;
581 let (tracker, warnings) = IgnoreTracker::from_content(content);
582 assert!(warnings.is_empty());
583 assert!(tracker.is_ignored("server-tokens-enabled", 3));
584 assert!(!tracker.is_ignored("server-tokens-enabled", 2));
585 }
586
587 #[test]
588 fn test_ignore_tracker_from_content_with_warnings() {
589 let content = r#"
590# nginx-lint:ignore
591server_tokens on;
592"#;
593 let (_, warnings) = IgnoreTracker::from_content(content);
594 assert_eq!(warnings.len(), 1);
595 assert!(warnings[0].message.contains("requires a rule name"));
596 }
597
598 #[test]
599 fn test_filter_errors() {
600 let mut tracker = IgnoreTracker::new();
601 tracker.add_ignore("server-tokens-enabled", 5);
602
603 let errors = vec![
604 LintError::new(
605 "server-tokens-enabled",
606 "security",
607 "test error",
608 Severity::Warning,
609 )
610 .with_location(5, 1),
611 LintError::new(
612 "server-tokens-enabled",
613 "security",
614 "test error",
615 Severity::Warning,
616 )
617 .with_location(6, 1),
618 LintError::new("other-rule", "security", "test error", Severity::Warning)
619 .with_location(5, 1),
620 ];
621
622 let result = filter_errors(errors, &mut tracker);
623 assert_eq!(result.errors.len(), 2);
624 assert_eq!(result.ignored_count, 1);
625 assert!(
627 result
628 .errors
629 .iter()
630 .all(|e| !(e.rule == "server-tokens-enabled" && e.line == Some(5)))
631 );
632 assert!(result.unused_warnings.is_empty());
634 }
635
636 #[test]
637 fn test_filter_errors_without_line_info() {
638 let mut tracker = IgnoreTracker::new();
639 tracker.add_ignore("some-rule", 5);
640
641 let errors = vec![LintError::new(
642 "some-rule",
643 "test",
644 "error without line",
645 Severity::Warning,
646 )];
647
648 let result = filter_errors(errors, &mut tracker);
649 assert_eq!(result.errors.len(), 1); assert_eq!(result.ignored_count, 0);
651 assert_eq!(result.unused_warnings.len(), 1);
653 }
654
655 #[test]
656 fn test_only_affects_next_line() {
657 let content = r#"
658# nginx-lint:ignore server-tokens-enabled reason
659server_tokens on;
660server_tokens on;
661"#;
662 let (tracker, warnings) = IgnoreTracker::from_content(content);
663 assert!(warnings.is_empty());
664 assert!(tracker.is_ignored("server-tokens-enabled", 3)); assert!(!tracker.is_ignored("server-tokens-enabled", 4)); }
667
668 #[test]
669 fn test_consecutive_ignore_comments() {
670 let content = r#"
672# nginx-lint:ignore server-tokens-enabled reason1
673# nginx-lint:ignore autoindex-enabled reason2
674server_tokens on;
675"#;
676 let (tracker, warnings) = IgnoreTracker::from_content(content);
677 assert!(warnings.is_empty());
678 assert!(tracker.is_ignored("server-tokens-enabled", 4));
680 assert!(tracker.is_ignored("autoindex-enabled", 4));
681 assert!(!tracker.is_ignored("server-tokens-enabled", 2));
683 assert!(!tracker.is_ignored("autoindex-enabled", 3));
684 }
685
686 #[test]
687 fn test_three_consecutive_ignore_comments() {
688 let content = r#"
689# nginx-lint:ignore server-tokens-enabled reason1
690# nginx-lint:ignore autoindex-enabled reason2
691# nginx-lint:ignore gzip-not-enabled reason3
692server_tokens on;
693"#;
694 let (tracker, warnings) = IgnoreTracker::from_content(content);
695 assert!(warnings.is_empty());
696 assert!(tracker.is_ignored("server-tokens-enabled", 5));
698 assert!(tracker.is_ignored("autoindex-enabled", 5));
699 assert!(tracker.is_ignored("gzip-not-enabled", 5));
700 }
701
702 #[test]
703 fn test_warnings_to_errors() {
704 let warnings = vec![IgnoreWarning {
705 line: 5,
706 message: "test warning".to_string(),
707 fixes: Vec::new(),
708 }];
709
710 let errors = warnings_to_errors(warnings);
711 assert_eq!(errors.len(), 1);
712 assert_eq!(errors[0].rule, "invalid-nginx-lint-ignore");
713 assert_eq!(errors[0].category, "ignore");
714 assert_eq!(errors[0].message, "test warning");
715 assert_eq!(errors[0].severity, Severity::Warning);
716 assert_eq!(errors[0].line, Some(5));
717 assert!(errors[0].fixes.is_empty());
718 }
719
720 #[test]
721 fn test_warnings_to_errors_with_fix() {
722 let warnings = vec![IgnoreWarning {
723 line: 5,
724 message: "test warning".to_string(),
725 fixes: vec![Fix::replace_range(10, 50, "")],
726 }];
727
728 let errors = warnings_to_errors(warnings);
729 assert_eq!(errors.len(), 1);
730 assert!(!errors[0].fixes.is_empty());
731 let fix = &errors[0].fixes[0];
732 assert!(fix.is_range_based());
733 assert_eq!(fix.start_offset, Some(10));
734 assert_eq!(fix.end_offset, Some(50));
735 assert_eq!(fix.new_text, "");
736 }
737
738 #[test]
739 fn test_parse_inline_comment() {
740 let result = parse_ignore_comment(
741 "server_tokens on; # nginx-lint:ignore server-tokens-enabled dev environment",
742 5,
743 );
744 assert!(result.is_some());
745 let parsed = result.unwrap().unwrap();
746 assert_eq!(parsed.rule_name, "server-tokens-enabled");
747 assert_eq!(parsed.target_line, 5); assert_eq!(parsed.comment_line, 5);
749 assert!(parsed.is_inline);
750 assert_eq!(
751 parsed.content_before_comment,
752 Some("server_tokens on;".to_string())
753 );
754 }
755
756 #[test]
757 fn test_parse_inline_comment_with_japanese_reason() {
758 let result = parse_ignore_comment(
759 "server_tokens on; # nginx-lint:ignore server-tokens-enabled 開発環境用",
760 5,
761 );
762 assert!(result.is_some());
763 let parsed = result.unwrap().unwrap();
764 assert_eq!(parsed.rule_name, "server-tokens-enabled");
765 assert_eq!(parsed.target_line, 5); assert_eq!(parsed.comment_line, 5);
767 assert!(parsed.is_inline);
768 assert_eq!(
769 parsed.content_before_comment,
770 Some("server_tokens on;".to_string())
771 );
772 }
773
774 #[test]
775 fn test_inline_comment_missing_reason() {
776 let result = parse_ignore_comment(
777 "server_tokens on; # nginx-lint:ignore server-tokens-enabled",
778 5,
779 );
780 assert!(result.is_some());
781 let warning = result.unwrap().unwrap_err();
782 assert_eq!(warning.line, 5);
783 assert!(warning.message.contains("requires a reason"));
784 }
785
786 #[test]
787 fn test_ignore_tracker_inline_comment() {
788 let content = r#"
789server_tokens on; # nginx-lint:ignore server-tokens-enabled dev environment
790"#;
791 let (tracker, warnings) = IgnoreTracker::from_content(content);
792 assert!(warnings.is_empty());
793 assert!(tracker.is_ignored("server-tokens-enabled", 2)); assert!(!tracker.is_ignored("server-tokens-enabled", 3));
795 }
796
797 #[test]
798 fn test_both_comment_styles() {
799 let content = r#"
800# nginx-lint:ignore server-tokens-enabled reason for next line
801server_tokens on;
802autoindex on; # nginx-lint:ignore autoindex-enabled reason for this line
803"#;
804 let (tracker, warnings) = IgnoreTracker::from_content(content);
805 assert!(warnings.is_empty());
806 assert!(tracker.is_ignored("server-tokens-enabled", 3));
808 assert!(tracker.is_ignored("autoindex-enabled", 4));
810 }
811
812 #[test]
813 fn test_unknown_rule_name() {
814 let content = r#"
815# nginx-lint:ignore unknown-rule-name some reason
816server_tokens on;
817"#;
818 let valid_rules: HashSet<String> = ["server-tokens-enabled", "autoindex-enabled"]
819 .iter()
820 .map(|s| s.to_string())
821 .collect();
822 let (_, warnings) = IgnoreTracker::from_content_with_rules(content, Some(&valid_rules));
823 assert_eq!(warnings.len(), 1);
824 assert!(
825 warnings[0]
826 .message
827 .contains("unknown rule 'unknown-rule-name'")
828 );
829 }
830
831 #[test]
832 fn test_unused_ignore_directive() {
833 let content = r#"
834# nginx-lint:ignore server-tokens-enabled reason
835server_tokens off;
836"#;
837 let (mut tracker, _) = IgnoreTracker::from_content(content);
838
839 let errors: Vec<LintError> = vec![];
841 let result = filter_errors(errors, &mut tracker);
842
843 assert_eq!(result.unused_warnings.len(), 1);
845 assert!(
846 result.unused_warnings[0]
847 .message
848 .contains("unused nginx-lint:ignore")
849 );
850 assert!(
851 result.unused_warnings[0]
852 .message
853 .contains("server-tokens-enabled")
854 );
855 }
856
857 #[test]
858 fn test_dormant_rule_suppresses_unused_warning() {
859 let content = r#"
860# nginx-lint:ignore server-tokens-enabled reason
861server_tokens off;
862"#;
863 let (mut tracker, _) = IgnoreTracker::from_content(content);
864
865 let mut dormant = HashSet::new();
868 dormant.insert("server-tokens-enabled".to_string());
869 tracker.set_dormant_rules(&dormant);
870
871 let result = filter_errors(Vec::<LintError>::new(), &mut tracker);
872
873 assert!(
874 result.unused_warnings.is_empty(),
875 "dormant rule should suppress unused-ignore warning, got: {:?}",
876 result
877 .unused_warnings
878 .iter()
879 .map(|w| &w.message)
880 .collect::<Vec<_>>()
881 );
882 }
883
884 #[test]
885 fn test_dormant_rule_does_not_affect_other_rules() {
886 let content = r#"
887# nginx-lint:ignore server-tokens-enabled reason
888# nginx-lint:ignore autoindex-enabled reason
889server_tokens off;
890"#;
891 let (mut tracker, _) = IgnoreTracker::from_content(content);
892
893 let mut dormant = HashSet::new();
896 dormant.insert("server-tokens-enabled".to_string());
897 tracker.set_dormant_rules(&dormant);
898
899 let result = filter_errors(Vec::<LintError>::new(), &mut tracker);
900
901 assert_eq!(result.unused_warnings.len(), 1);
902 assert!(
903 result.unused_warnings[0]
904 .message
905 .contains("autoindex-enabled")
906 );
907 }
908
909 #[test]
910 fn test_used_ignore_directive_no_warning() {
911 let content = r#"
912# nginx-lint:ignore server-tokens-enabled reason
913server_tokens on;
914"#;
915 let (mut tracker, _) = IgnoreTracker::from_content(content);
916
917 let errors = vec![
919 LintError::new(
920 "server-tokens-enabled",
921 "security",
922 "test error",
923 Severity::Warning,
924 )
925 .with_location(3, 1),
926 ];
927
928 let result = filter_errors(errors, &mut tracker);
929
930 assert!(result.unused_warnings.is_empty());
932 assert_eq!(result.ignored_count, 1);
933 }
934
935 #[test]
936 fn test_unused_comment_only_line_fix() {
937 let content = "\n# nginx-lint:ignore server-tokens-enabled reason\nserver_tokens off;\n";
938 let (mut tracker, _) = IgnoreTracker::from_content(content);
939
940 let errors: Vec<LintError> = vec![];
941 let result = filter_errors(errors, &mut tracker);
942
943 assert_eq!(result.unused_warnings.len(), 1);
945 let fix = &result.unused_warnings[0].fixes[0];
946 assert!(fix.is_range_based());
947 assert_eq!(fix.new_text, "");
949 let mut fixed = content.to_string();
951 fixed.replace_range(
952 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
953 &fix.new_text,
954 );
955 assert_eq!(fixed, "\nserver_tokens off;\n");
956 }
957
958 #[test]
959 fn test_unused_inline_comment_fix() {
960 let content = "\nserver_tokens off; # nginx-lint:ignore server-tokens-enabled reason\n";
961 let (mut tracker, _) = IgnoreTracker::from_content(content);
962
963 let errors: Vec<LintError> = vec![];
964 let result = filter_errors(errors, &mut tracker);
965
966 assert_eq!(result.unused_warnings.len(), 1);
968 let fix = &result.unused_warnings[0].fixes[0];
969 assert!(fix.is_range_based());
970 assert_eq!(fix.new_text, "server_tokens off;");
971 let mut fixed = content.to_string();
973 fixed.replace_range(
974 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
975 &fix.new_text,
976 );
977 assert_eq!(fixed, "\nserver_tokens off;\n");
978 }
979
980 #[test]
981 fn test_unused_comment_on_first_line_fix() {
982 let content = "# nginx-lint:ignore server-tokens-enabled reason\nserver_tokens off;\n";
983 let (mut tracker, _) = IgnoreTracker::from_content(content);
984
985 let errors: Vec<LintError> = vec![];
986 let result = filter_errors(errors, &mut tracker);
987
988 assert_eq!(result.unused_warnings.len(), 1);
989 let fix = &result.unused_warnings[0].fixes[0];
990 assert!(fix.is_range_based());
991 let mut fixed = content.to_string();
992 fixed.replace_range(
993 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
994 &fix.new_text,
995 );
996 assert_eq!(fixed, "server_tokens off;\n");
997 }
998
999 #[test]
1000 fn test_unused_comment_on_last_line_no_trailing_newline_fix() {
1001 let content = "server_tokens off;\n# nginx-lint:ignore server-tokens-enabled reason";
1002 let (mut tracker, _) = IgnoreTracker::from_content(content);
1003
1004 let errors: Vec<LintError> = vec![];
1005 let result = filter_errors(errors, &mut tracker);
1006
1007 assert_eq!(result.unused_warnings.len(), 1);
1008 let fix = &result.unused_warnings[0].fixes[0];
1009 assert!(fix.is_range_based());
1010 let mut fixed = content.to_string();
1011 fixed.replace_range(
1012 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
1013 &fix.new_text,
1014 );
1015 assert_eq!(fixed, "server_tokens off;");
1017 }
1018
1019 #[test]
1022 fn test_parse_context_comment_simple() {
1023 let content = "# nginx-lint:context http\nserver { listen 80; }";
1024 let context = parse_context_comment(content);
1025 assert_eq!(context, Some(vec!["http".to_string()]));
1026 }
1027
1028 #[test]
1029 fn test_parse_context_comment_multiple() {
1030 let content = "# nginx-lint:context http,server\nlocation / { }";
1031 let context = parse_context_comment(content);
1032 assert_eq!(
1033 context,
1034 Some(vec!["http".to_string(), "server".to_string()])
1035 );
1036 }
1037
1038 #[test]
1039 fn test_parse_context_comment_with_spaces() {
1040 let content = "# nginx-lint:context http, server\nlocation / { }";
1041 let context = parse_context_comment(content);
1042 assert_eq!(
1043 context,
1044 Some(vec!["http".to_string(), "server".to_string()])
1045 );
1046 }
1047
1048 #[test]
1049 fn test_parse_context_comment_after_empty_lines() {
1050 let content = "\n\n# nginx-lint:context http\nserver { }";
1051 let context = parse_context_comment(content);
1052 assert_eq!(context, Some(vec!["http".to_string()]));
1053 }
1054
1055 #[test]
1056 fn test_parse_context_comment_after_other_comments() {
1057 let content = "# Some description\n# nginx-lint:context http\nserver { }";
1058 let context = parse_context_comment(content);
1059 assert_eq!(context, Some(vec!["http".to_string()]));
1060 }
1061
1062 #[test]
1063 fn test_parse_context_comment_not_found() {
1064 let content = "server { listen 80; }";
1065 let context = parse_context_comment(content);
1066 assert_eq!(context, None);
1067 }
1068
1069 #[test]
1070 fn test_parse_context_comment_after_directive() {
1071 let content = "server { }\n# nginx-lint:context http";
1073 let context = parse_context_comment(content);
1074 assert_eq!(context, None);
1075 }
1076
1077 #[test]
1078 fn test_parse_context_comment_empty_value() {
1079 let content = "# nginx-lint:context\nserver { }";
1080 let context = parse_context_comment(content);
1081 assert_eq!(context, None);
1082 }
1083}