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}
64
65impl IgnoreTracker {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn is_ignored(&self, rule: &str, line: usize) -> bool {
73 self.ignored_lines
74 .get(&line)
75 .map(|rules| rules.contains(rule))
76 .unwrap_or(false)
77 }
78
79 pub fn from_content(content: &str) -> (Self, Vec<IgnoreWarning>) {
81 Self::from_content_with_rules(content, None)
82 }
83
84 pub fn from_content_with_rules(
86 content: &str,
87 valid_rules: Option<&HashSet<String>>,
88 ) -> (Self, Vec<IgnoreWarning>) {
89 let mut tracker = Self::new();
90 let mut warnings = Vec::new();
91 let line_starts = compute_line_starts(content);
92
93 let lines: Vec<&str> = content.lines().collect();
95 let mut parsed_comments: Vec<(usize, Result<ParsedIgnoreComment, IgnoreWarning>)> =
96 Vec::new();
97
98 for (line_idx, line) in lines.iter().enumerate() {
99 let line_number = line_idx + 1; if let Some(result) = parse_ignore_comment(line, line_number) {
101 parsed_comments.push((line_idx, result));
102 }
103 }
104
105 for i in 0..parsed_comments.len() {
108 let (line_idx, ref result) = parsed_comments[i];
109
110 match result {
111 Ok(parsed) if !parsed.is_inline => {
112 let mut target_idx = line_idx + 1;
114 while target_idx < lines.len() {
115 let is_ignore_comment = parsed_comments.iter().any(|(idx, r)| {
117 *idx == target_idx && matches!(r, Ok(p) if !p.is_inline)
118 });
119 if !is_ignore_comment {
120 break;
121 }
122 target_idx += 1;
123 }
124 let actual_target_line = target_idx + 1; if let Some(valid) = valid_rules
128 && !valid.contains(&parsed.rule_name)
129 {
130 warnings.push(IgnoreWarning {
131 line: parsed.comment_line,
132 message: format!(
133 "unknown rule '{}' in nginx-lint:ignore comment",
134 parsed.rule_name
135 ),
136 fixes: Vec::new(),
137 });
138 }
139
140 tracker
141 .ignored_lines
142 .entry(actual_target_line)
143 .or_default()
144 .insert(parsed.rule_name.clone());
145
146 let comment_idx = parsed.comment_line - 1;
148 let num_lines = line_starts.len() - 1;
149 let (fix_start, fix_end) = if comment_idx + 1 < num_lines {
150 (line_starts[comment_idx], line_starts[comment_idx + 1])
152 } else if line_starts[comment_idx] > 0 {
153 (line_starts[comment_idx] - 1, line_starts[comment_idx + 1])
155 } else {
156 (line_starts[comment_idx], line_starts[comment_idx + 1])
157 };
158
159 tracker.directives.push(IgnoreDirective {
160 comment_line: parsed.comment_line,
161 target_line: actual_target_line,
162 rule_name: parsed.rule_name.clone(),
163 used: false,
164 fix_start_offset: fix_start,
165 fix_end_offset: fix_end,
166 fix_replacement: String::new(),
167 });
168 }
169 Ok(parsed) => {
170 if let Some(valid) = valid_rules
172 && !valid.contains(&parsed.rule_name)
173 {
174 warnings.push(IgnoreWarning {
175 line: parsed.comment_line,
176 message: format!(
177 "unknown rule '{}' in nginx-lint:ignore comment",
178 parsed.rule_name
179 ),
180 fixes: Vec::new(),
181 });
182 }
183
184 tracker
185 .ignored_lines
186 .entry(parsed.target_line)
187 .or_default()
188 .insert(parsed.rule_name.clone());
189
190 let comment_idx = parsed.comment_line - 1;
192 let line_start = line_starts[comment_idx];
193 let next_line_start = line_starts[comment_idx + 1];
194 let line_end = if next_line_start > line_start
195 && content.as_bytes().get(next_line_start - 1) == Some(&b'\n')
196 {
197 next_line_start - 1
198 } else {
199 next_line_start
200 };
201 let replacement = parsed.content_before_comment.clone().unwrap_or_default();
202
203 tracker.directives.push(IgnoreDirective {
204 comment_line: parsed.comment_line,
205 target_line: parsed.target_line,
206 rule_name: parsed.rule_name.clone(),
207 used: false,
208 fix_start_offset: line_start,
209 fix_end_offset: line_end,
210 fix_replacement: replacement,
211 });
212 }
213 Err(warning) => {
214 warnings.push(warning.clone());
215 }
216 }
217 }
218
219 (tracker, warnings)
220 }
221
222 fn mark_used(&mut self, rule: &str, line: usize) {
224 for directive in &mut self.directives {
225 if directive.target_line == line && directive.rule_name == rule {
226 directive.used = true;
227 }
228 }
229 }
230
231 pub fn unused_warnings(&self) -> Vec<IgnoreWarning> {
233 self.directives
234 .iter()
235 .filter(|d| !d.used)
236 .map(|d| {
237 let fix =
238 Fix::replace_range(d.fix_start_offset, d.fix_end_offset, &d.fix_replacement);
239
240 IgnoreWarning {
241 line: d.comment_line,
242 message: format!(
243 "unused nginx-lint:ignore comment for rule '{}'",
244 d.rule_name
245 ),
246 fixes: vec![fix],
247 }
248 })
249 .collect()
250 }
251
252 #[cfg(test)]
257 pub fn add_ignore(&mut self, rule: &str, line: usize) {
258 self.ignored_lines
259 .entry(line)
260 .or_default()
261 .insert(rule.to_string());
262 self.directives.push(IgnoreDirective {
263 comment_line: line.saturating_sub(1).max(1),
264 target_line: line,
265 rule_name: rule.to_string(),
266 used: false,
267 fix_start_offset: 0,
268 fix_end_offset: 0,
269 fix_replacement: String::new(),
270 });
271 }
272}
273
274#[derive(Debug)]
276struct ParsedIgnoreComment {
277 rule_name: String,
279 target_line: usize,
281 comment_line: usize,
283 is_inline: bool,
285 content_before_comment: Option<String>,
287}
288
289fn parse_ignore_comment(
300 line: &str,
301 line_number: usize,
302) -> Option<Result<ParsedIgnoreComment, IgnoreWarning>> {
303 const IGNORE_PREFIX: &str = "nginx-lint:ignore";
304
305 let comment_start = line.find('#')?;
307 let comment_part = &line[comment_start..];
308 let comment = comment_part.trim_start_matches('#').trim();
309
310 let rest = comment.strip_prefix(IGNORE_PREFIX)?;
312 let rest = rest.trim();
313
314 let before_comment_trimmed = line[..comment_start].trim();
316 let is_inline = !before_comment_trimmed.is_empty();
317
318 let parts: Vec<&str> = rest.splitn(2, |c: char| c.is_whitespace()).collect();
320
321 if parts.is_empty() || parts[0].is_empty() {
323 return Some(Err(IgnoreWarning {
324 line: line_number,
325 message: "nginx-lint:ignore requires a rule name".to_string(),
326 fixes: Vec::new(),
327 }));
328 }
329
330 let rule_name = parts[0].to_string();
331
332 if parts.len() < 2 || parts[1].trim().is_empty() {
334 return Some(Err(IgnoreWarning {
335 line: line_number,
336 message: format!("nginx-lint:ignore {} requires a reason", rule_name),
337 fixes: Vec::new(),
338 }));
339 }
340
341 let target_line = if is_inline {
343 line_number
344 } else {
345 line_number + 1
346 };
347
348 let content_before = if is_inline {
350 Some(line[..comment_start].trim_end().to_string())
351 } else {
352 None
353 };
354
355 Some(Ok(ParsedIgnoreComment {
356 rule_name,
357 target_line,
358 comment_line: line_number,
359 is_inline,
360 content_before_comment: content_before,
361 }))
362}
363
364#[derive(Debug)]
366pub struct FilterResult {
367 pub errors: Vec<LintError>,
369 pub ignored_count: usize,
371 pub unused_warnings: Vec<IgnoreWarning>,
373}
374
375pub fn filter_errors(errors: Vec<LintError>, tracker: &mut IgnoreTracker) -> FilterResult {
377 let mut remaining = Vec::new();
378 let mut ignored_count = 0;
379
380 for error in errors {
381 if let Some(line) = error.line
382 && tracker.is_ignored(&error.rule, line)
383 {
384 tracker.mark_used(&error.rule, line);
385 ignored_count += 1;
386 continue;
387 }
388 remaining.push(error);
389 }
390
391 let unused_warnings = tracker.unused_warnings();
392
393 FilterResult {
394 errors: remaining,
395 ignored_count,
396 unused_warnings,
397 }
398}
399
400pub fn warnings_to_errors(warnings: Vec<IgnoreWarning>) -> Vec<LintError> {
402 warnings
403 .into_iter()
404 .map(|warning| {
405 let mut error = LintError::new(
406 "invalid-nginx-lint-ignore",
407 "ignore",
408 &warning.message,
409 Severity::Warning,
410 )
411 .with_location(warning.line, 1);
412
413 for fix in warning.fixes {
414 error = error.with_fix(fix);
415 }
416
417 error
418 })
419 .collect()
420}
421
422const CONTEXT_PREFIX: &str = "nginx-lint:context";
424
425pub fn parse_context_comment(content: &str) -> Option<Vec<String>> {
439 for line in content.lines().take(10) {
441 let trimmed = line.trim();
442
443 if trimmed.is_empty() {
445 continue;
446 }
447
448 if !trimmed.starts_with('#') {
450 break;
452 }
453
454 let comment = trimmed.trim_start_matches('#').trim();
455
456 if let Some(rest) = comment.strip_prefix(CONTEXT_PREFIX) {
458 let context_str = rest.trim();
459 if context_str.is_empty() {
460 return None;
461 }
462
463 let context: Vec<String> = context_str
464 .split(',')
465 .map(|s| s.trim().to_string())
466 .filter(|s| !s.is_empty())
467 .collect();
468
469 if context.is_empty() {
470 return None;
471 }
472
473 return Some(context);
474 }
475 }
476
477 None
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_parse_valid_ignore_comment() {
486 let result = parse_ignore_comment(
487 "# nginx-lint:ignore server-tokens-enabled for dev environment",
488 5,
489 );
490 assert!(result.is_some());
491 let parsed = result.unwrap().unwrap();
492 assert_eq!(parsed.rule_name, "server-tokens-enabled");
493 assert_eq!(parsed.target_line, 6); assert_eq!(parsed.comment_line, 5);
495 assert!(!parsed.is_inline);
496 assert!(parsed.content_before_comment.is_none());
497 }
498
499 #[test]
500 fn test_parse_ignore_comment_with_japanese_reason() {
501 let result =
502 parse_ignore_comment("# nginx-lint:ignore server-tokens-enabled 開発環境用", 5);
503 assert!(result.is_some());
504 let parsed = result.unwrap().unwrap();
505 assert_eq!(parsed.rule_name, "server-tokens-enabled");
506 assert_eq!(parsed.target_line, 6);
507 assert_eq!(parsed.comment_line, 5);
508 assert!(!parsed.is_inline);
509 assert!(parsed.content_before_comment.is_none());
510 }
511
512 #[test]
513 fn test_parse_missing_rule_name() {
514 let result = parse_ignore_comment("# nginx-lint:ignore", 5);
515 assert!(result.is_some());
516 let warning = result.unwrap().unwrap_err();
517 assert_eq!(warning.line, 5);
518 assert!(
519 warning
520 .message
521 .contains("nginx-lint:ignore requires a rule name")
522 );
523 }
524
525 #[test]
526 fn test_parse_missing_reason() {
527 let result = parse_ignore_comment("# nginx-lint:ignore server-tokens-enabled", 5);
528 assert!(result.is_some());
529 let warning = result.unwrap().unwrap_err();
530 assert_eq!(warning.line, 5);
531 assert!(
532 warning
533 .message
534 .contains("nginx-lint:ignore server-tokens-enabled requires a reason")
535 );
536 }
537
538 #[test]
539 fn test_parse_not_a_comment() {
540 let result = parse_ignore_comment("server_tokens on;", 5);
541 assert!(result.is_none());
542 }
543
544 #[test]
545 fn test_parse_regular_comment() {
546 let result = parse_ignore_comment("# This is a regular comment", 5);
547 assert!(result.is_none());
548 }
549
550 #[test]
551 fn test_ignore_tracker_is_ignored() {
552 let mut tracker = IgnoreTracker::new();
553 tracker.add_ignore("server-tokens-enabled", 10);
554
555 assert!(tracker.is_ignored("server-tokens-enabled", 10));
556 assert!(!tracker.is_ignored("server-tokens-enabled", 11));
557 assert!(!tracker.is_ignored("other-rule", 10));
558 }
559
560 #[test]
561 fn test_ignore_tracker_from_content() {
562 let content = r#"
563# nginx-lint:ignore server-tokens-enabled dev environment
564server_tokens on;
565"#;
566 let (tracker, warnings) = IgnoreTracker::from_content(content);
567 assert!(warnings.is_empty());
568 assert!(tracker.is_ignored("server-tokens-enabled", 3));
569 assert!(!tracker.is_ignored("server-tokens-enabled", 2));
570 }
571
572 #[test]
573 fn test_ignore_tracker_from_content_with_warnings() {
574 let content = r#"
575# nginx-lint:ignore
576server_tokens on;
577"#;
578 let (_, warnings) = IgnoreTracker::from_content(content);
579 assert_eq!(warnings.len(), 1);
580 assert!(warnings[0].message.contains("requires a rule name"));
581 }
582
583 #[test]
584 fn test_filter_errors() {
585 let mut tracker = IgnoreTracker::new();
586 tracker.add_ignore("server-tokens-enabled", 5);
587
588 let errors = vec![
589 LintError::new(
590 "server-tokens-enabled",
591 "security",
592 "test error",
593 Severity::Warning,
594 )
595 .with_location(5, 1),
596 LintError::new(
597 "server-tokens-enabled",
598 "security",
599 "test error",
600 Severity::Warning,
601 )
602 .with_location(6, 1),
603 LintError::new("other-rule", "security", "test error", Severity::Warning)
604 .with_location(5, 1),
605 ];
606
607 let result = filter_errors(errors, &mut tracker);
608 assert_eq!(result.errors.len(), 2);
609 assert_eq!(result.ignored_count, 1);
610 assert!(
612 result
613 .errors
614 .iter()
615 .all(|e| !(e.rule == "server-tokens-enabled" && e.line == Some(5)))
616 );
617 assert!(result.unused_warnings.is_empty());
619 }
620
621 #[test]
622 fn test_filter_errors_without_line_info() {
623 let mut tracker = IgnoreTracker::new();
624 tracker.add_ignore("some-rule", 5);
625
626 let errors = vec![LintError::new(
627 "some-rule",
628 "test",
629 "error without line",
630 Severity::Warning,
631 )];
632
633 let result = filter_errors(errors, &mut tracker);
634 assert_eq!(result.errors.len(), 1); assert_eq!(result.ignored_count, 0);
636 assert_eq!(result.unused_warnings.len(), 1);
638 }
639
640 #[test]
641 fn test_only_affects_next_line() {
642 let content = r#"
643# nginx-lint:ignore server-tokens-enabled reason
644server_tokens on;
645server_tokens on;
646"#;
647 let (tracker, warnings) = IgnoreTracker::from_content(content);
648 assert!(warnings.is_empty());
649 assert!(tracker.is_ignored("server-tokens-enabled", 3)); assert!(!tracker.is_ignored("server-tokens-enabled", 4)); }
652
653 #[test]
654 fn test_consecutive_ignore_comments() {
655 let content = r#"
657# nginx-lint:ignore server-tokens-enabled reason1
658# nginx-lint:ignore autoindex-enabled reason2
659server_tokens on;
660"#;
661 let (tracker, warnings) = IgnoreTracker::from_content(content);
662 assert!(warnings.is_empty());
663 assert!(tracker.is_ignored("server-tokens-enabled", 4));
665 assert!(tracker.is_ignored("autoindex-enabled", 4));
666 assert!(!tracker.is_ignored("server-tokens-enabled", 2));
668 assert!(!tracker.is_ignored("autoindex-enabled", 3));
669 }
670
671 #[test]
672 fn test_three_consecutive_ignore_comments() {
673 let content = r#"
674# nginx-lint:ignore server-tokens-enabled reason1
675# nginx-lint:ignore autoindex-enabled reason2
676# nginx-lint:ignore gzip-not-enabled reason3
677server_tokens on;
678"#;
679 let (tracker, warnings) = IgnoreTracker::from_content(content);
680 assert!(warnings.is_empty());
681 assert!(tracker.is_ignored("server-tokens-enabled", 5));
683 assert!(tracker.is_ignored("autoindex-enabled", 5));
684 assert!(tracker.is_ignored("gzip-not-enabled", 5));
685 }
686
687 #[test]
688 fn test_warnings_to_errors() {
689 let warnings = vec![IgnoreWarning {
690 line: 5,
691 message: "test warning".to_string(),
692 fixes: Vec::new(),
693 }];
694
695 let errors = warnings_to_errors(warnings);
696 assert_eq!(errors.len(), 1);
697 assert_eq!(errors[0].rule, "invalid-nginx-lint-ignore");
698 assert_eq!(errors[0].category, "ignore");
699 assert_eq!(errors[0].message, "test warning");
700 assert_eq!(errors[0].severity, Severity::Warning);
701 assert_eq!(errors[0].line, Some(5));
702 assert!(errors[0].fixes.is_empty());
703 }
704
705 #[test]
706 fn test_warnings_to_errors_with_fix() {
707 let warnings = vec![IgnoreWarning {
708 line: 5,
709 message: "test warning".to_string(),
710 fixes: vec![Fix::replace_range(10, 50, "")],
711 }];
712
713 let errors = warnings_to_errors(warnings);
714 assert_eq!(errors.len(), 1);
715 assert!(!errors[0].fixes.is_empty());
716 let fix = &errors[0].fixes[0];
717 assert!(fix.is_range_based());
718 assert_eq!(fix.start_offset, Some(10));
719 assert_eq!(fix.end_offset, Some(50));
720 assert_eq!(fix.new_text, "");
721 }
722
723 #[test]
724 fn test_parse_inline_comment() {
725 let result = parse_ignore_comment(
726 "server_tokens on; # nginx-lint:ignore server-tokens-enabled dev environment",
727 5,
728 );
729 assert!(result.is_some());
730 let parsed = result.unwrap().unwrap();
731 assert_eq!(parsed.rule_name, "server-tokens-enabled");
732 assert_eq!(parsed.target_line, 5); assert_eq!(parsed.comment_line, 5);
734 assert!(parsed.is_inline);
735 assert_eq!(
736 parsed.content_before_comment,
737 Some("server_tokens on;".to_string())
738 );
739 }
740
741 #[test]
742 fn test_parse_inline_comment_with_japanese_reason() {
743 let result = parse_ignore_comment(
744 "server_tokens on; # nginx-lint:ignore server-tokens-enabled 開発環境用",
745 5,
746 );
747 assert!(result.is_some());
748 let parsed = result.unwrap().unwrap();
749 assert_eq!(parsed.rule_name, "server-tokens-enabled");
750 assert_eq!(parsed.target_line, 5); assert_eq!(parsed.comment_line, 5);
752 assert!(parsed.is_inline);
753 assert_eq!(
754 parsed.content_before_comment,
755 Some("server_tokens on;".to_string())
756 );
757 }
758
759 #[test]
760 fn test_inline_comment_missing_reason() {
761 let result = parse_ignore_comment(
762 "server_tokens on; # nginx-lint:ignore server-tokens-enabled",
763 5,
764 );
765 assert!(result.is_some());
766 let warning = result.unwrap().unwrap_err();
767 assert_eq!(warning.line, 5);
768 assert!(warning.message.contains("requires a reason"));
769 }
770
771 #[test]
772 fn test_ignore_tracker_inline_comment() {
773 let content = r#"
774server_tokens on; # nginx-lint:ignore server-tokens-enabled dev environment
775"#;
776 let (tracker, warnings) = IgnoreTracker::from_content(content);
777 assert!(warnings.is_empty());
778 assert!(tracker.is_ignored("server-tokens-enabled", 2)); assert!(!tracker.is_ignored("server-tokens-enabled", 3));
780 }
781
782 #[test]
783 fn test_both_comment_styles() {
784 let content = r#"
785# nginx-lint:ignore server-tokens-enabled reason for next line
786server_tokens on;
787autoindex on; # nginx-lint:ignore autoindex-enabled reason for this line
788"#;
789 let (tracker, warnings) = IgnoreTracker::from_content(content);
790 assert!(warnings.is_empty());
791 assert!(tracker.is_ignored("server-tokens-enabled", 3));
793 assert!(tracker.is_ignored("autoindex-enabled", 4));
795 }
796
797 #[test]
798 fn test_unknown_rule_name() {
799 let content = r#"
800# nginx-lint:ignore unknown-rule-name some reason
801server_tokens on;
802"#;
803 let valid_rules: HashSet<String> = ["server-tokens-enabled", "autoindex-enabled"]
804 .iter()
805 .map(|s| s.to_string())
806 .collect();
807 let (_, warnings) = IgnoreTracker::from_content_with_rules(content, Some(&valid_rules));
808 assert_eq!(warnings.len(), 1);
809 assert!(
810 warnings[0]
811 .message
812 .contains("unknown rule 'unknown-rule-name'")
813 );
814 }
815
816 #[test]
817 fn test_unused_ignore_directive() {
818 let content = r#"
819# nginx-lint:ignore server-tokens-enabled reason
820server_tokens off;
821"#;
822 let (mut tracker, _) = IgnoreTracker::from_content(content);
823
824 let errors: Vec<LintError> = vec![];
826 let result = filter_errors(errors, &mut tracker);
827
828 assert_eq!(result.unused_warnings.len(), 1);
830 assert!(
831 result.unused_warnings[0]
832 .message
833 .contains("unused nginx-lint:ignore")
834 );
835 assert!(
836 result.unused_warnings[0]
837 .message
838 .contains("server-tokens-enabled")
839 );
840 }
841
842 #[test]
843 fn test_used_ignore_directive_no_warning() {
844 let content = r#"
845# nginx-lint:ignore server-tokens-enabled reason
846server_tokens on;
847"#;
848 let (mut tracker, _) = IgnoreTracker::from_content(content);
849
850 let errors = vec![
852 LintError::new(
853 "server-tokens-enabled",
854 "security",
855 "test error",
856 Severity::Warning,
857 )
858 .with_location(3, 1),
859 ];
860
861 let result = filter_errors(errors, &mut tracker);
862
863 assert!(result.unused_warnings.is_empty());
865 assert_eq!(result.ignored_count, 1);
866 }
867
868 #[test]
869 fn test_unused_comment_only_line_fix() {
870 let content = "\n# nginx-lint:ignore server-tokens-enabled reason\nserver_tokens off;\n";
871 let (mut tracker, _) = IgnoreTracker::from_content(content);
872
873 let errors: Vec<LintError> = vec![];
874 let result = filter_errors(errors, &mut tracker);
875
876 assert_eq!(result.unused_warnings.len(), 1);
878 let fix = &result.unused_warnings[0].fixes[0];
879 assert!(fix.is_range_based());
880 assert_eq!(fix.new_text, "");
882 let mut fixed = content.to_string();
884 fixed.replace_range(
885 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
886 &fix.new_text,
887 );
888 assert_eq!(fixed, "\nserver_tokens off;\n");
889 }
890
891 #[test]
892 fn test_unused_inline_comment_fix() {
893 let content = "\nserver_tokens off; # nginx-lint:ignore server-tokens-enabled reason\n";
894 let (mut tracker, _) = IgnoreTracker::from_content(content);
895
896 let errors: Vec<LintError> = vec![];
897 let result = filter_errors(errors, &mut tracker);
898
899 assert_eq!(result.unused_warnings.len(), 1);
901 let fix = &result.unused_warnings[0].fixes[0];
902 assert!(fix.is_range_based());
903 assert_eq!(fix.new_text, "server_tokens off;");
904 let mut fixed = content.to_string();
906 fixed.replace_range(
907 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
908 &fix.new_text,
909 );
910 assert_eq!(fixed, "\nserver_tokens off;\n");
911 }
912
913 #[test]
914 fn test_unused_comment_on_first_line_fix() {
915 let content = "# nginx-lint:ignore server-tokens-enabled reason\nserver_tokens off;\n";
916 let (mut tracker, _) = IgnoreTracker::from_content(content);
917
918 let errors: Vec<LintError> = vec![];
919 let result = filter_errors(errors, &mut tracker);
920
921 assert_eq!(result.unused_warnings.len(), 1);
922 let fix = &result.unused_warnings[0].fixes[0];
923 assert!(fix.is_range_based());
924 let mut fixed = content.to_string();
925 fixed.replace_range(
926 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
927 &fix.new_text,
928 );
929 assert_eq!(fixed, "server_tokens off;\n");
930 }
931
932 #[test]
933 fn test_unused_comment_on_last_line_no_trailing_newline_fix() {
934 let content = "server_tokens off;\n# nginx-lint:ignore server-tokens-enabled reason";
935 let (mut tracker, _) = IgnoreTracker::from_content(content);
936
937 let errors: Vec<LintError> = vec![];
938 let result = filter_errors(errors, &mut tracker);
939
940 assert_eq!(result.unused_warnings.len(), 1);
941 let fix = &result.unused_warnings[0].fixes[0];
942 assert!(fix.is_range_based());
943 let mut fixed = content.to_string();
944 fixed.replace_range(
945 fix.start_offset.unwrap()..fix.end_offset.unwrap(),
946 &fix.new_text,
947 );
948 assert_eq!(fixed, "server_tokens off;");
950 }
951
952 #[test]
955 fn test_parse_context_comment_simple() {
956 let content = "# nginx-lint:context http\nserver { listen 80; }";
957 let context = parse_context_comment(content);
958 assert_eq!(context, Some(vec!["http".to_string()]));
959 }
960
961 #[test]
962 fn test_parse_context_comment_multiple() {
963 let content = "# nginx-lint:context http,server\nlocation / { }";
964 let context = parse_context_comment(content);
965 assert_eq!(
966 context,
967 Some(vec!["http".to_string(), "server".to_string()])
968 );
969 }
970
971 #[test]
972 fn test_parse_context_comment_with_spaces() {
973 let content = "# nginx-lint:context http, server\nlocation / { }";
974 let context = parse_context_comment(content);
975 assert_eq!(
976 context,
977 Some(vec!["http".to_string(), "server".to_string()])
978 );
979 }
980
981 #[test]
982 fn test_parse_context_comment_after_empty_lines() {
983 let content = "\n\n# nginx-lint:context http\nserver { }";
984 let context = parse_context_comment(content);
985 assert_eq!(context, Some(vec!["http".to_string()]));
986 }
987
988 #[test]
989 fn test_parse_context_comment_after_other_comments() {
990 let content = "# Some description\n# nginx-lint:context http\nserver { }";
991 let context = parse_context_comment(content);
992 assert_eq!(context, Some(vec!["http".to_string()]));
993 }
994
995 #[test]
996 fn test_parse_context_comment_not_found() {
997 let content = "server { listen 80; }";
998 let context = parse_context_comment(content);
999 assert_eq!(context, None);
1000 }
1001
1002 #[test]
1003 fn test_parse_context_comment_after_directive() {
1004 let content = "server { }\n# nginx-lint:context http";
1006 let context = parse_context_comment(content);
1007 assert_eq!(context, None);
1008 }
1009
1010 #[test]
1011 fn test_parse_context_comment_empty_value() {
1012 let content = "# nginx-lint:context\nserver { }";
1013 let context = parse_context_comment(content);
1014 assert_eq!(context, None);
1015 }
1016}