1#![allow(dead_code)]
7#![warn(clippy::all)]
8#![allow(unused_imports)]
9#![allow(clippy::field_reassign_with_default)]
10#![allow(clippy::ptr_arg)]
11#![allow(clippy::derivable_impls)]
12#![allow(clippy::should_implement_trait)]
13#![allow(clippy::collapsible_if)]
14#![allow(clippy::single_match)]
15#![allow(clippy::needless_ifs)]
16#![allow(clippy::len_without_is_empty)]
17#![allow(clippy::new_without_default)]
18#![allow(clippy::inherent_to_string_shadow_display)]
19#![allow(clippy::type_complexity)]
20#![allow(clippy::manual_strip)]
21#![allow(clippy::bool_comparison)]
22#![allow(clippy::if_same_then_else)]
23#![allow(clippy::implicit_saturating_sub)]
24#![allow(clippy::int_plus_one)]
25#![allow(clippy::manual_map)]
26#![allow(clippy::needless_bool)]
27#![allow(clippy::clone_on_copy)]
28#![allow(clippy::manual_find)]
29#![allow(clippy::for_kv_map)]
30#![allow(clippy::enum_variant_names)]
31#![allow(clippy::manual_range_contains)]
32#![allow(clippy::to_string_in_format_args)]
33
34pub mod autofix;
35pub mod framework;
36pub mod ide_integration;
37pub mod plugin;
38pub mod rules;
39
40pub use framework::{
41 AutoFix, LintConfig, LintContext, LintDiagnostic, LintEngine, LintId, LintRegistry, LintRule,
42 LintSuppression, Severity,
43};
44pub use rules::{
45 DeadCodeRule, DeprecatedApiRule, DeprecatedTacticRule, LongProofRule, MissingDocRule,
46 MissingDocstringRule, NamingConventionRule, RedundantAssumptionRule, RedundantPatternRule,
47 SimplifiableExprRule, StyleRule, UnreachableCodeRule, UnusedHypothesisRule, UnusedImportRule,
48 UnusedVariableRule,
49};
50
51use std::fmt;
52
53#[derive(Clone, Debug)]
59pub struct LintPass {
60 pub name: String,
62 pub lint_ids: Vec<LintId>,
64 pub enabled: bool,
66 pub can_fix: bool,
68}
69
70impl LintPass {
71 pub fn new(name: &str) -> Self {
73 Self {
74 name: name.to_string(),
75 lint_ids: Vec::new(),
76 enabled: true,
77 can_fix: false,
78 }
79 }
80
81 pub fn with_lint(mut self, id: &str) -> Self {
83 self.lint_ids.push(LintId::new(id));
84 self
85 }
86
87 pub fn with_fixes(mut self) -> Self {
89 self.can_fix = true;
90 self
91 }
92
93 pub fn disabled(mut self) -> Self {
95 self.enabled = false;
96 self
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Hash)]
106pub enum LintCategory {
107 Correctness,
109 Style,
111 Performance,
113 Complexity,
115 Deprecation,
117 Documentation,
119 Naming,
121 Redundancy,
123 Security,
125 Custom(String),
127}
128
129impl fmt::Display for LintCategory {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 LintCategory::Correctness => write!(f, "correctness"),
133 LintCategory::Style => write!(f, "style"),
134 LintCategory::Performance => write!(f, "performance"),
135 LintCategory::Complexity => write!(f, "complexity"),
136 LintCategory::Deprecation => write!(f, "deprecation"),
137 LintCategory::Documentation => write!(f, "documentation"),
138 LintCategory::Naming => write!(f, "naming"),
139 LintCategory::Redundancy => write!(f, "redundancy"),
140 LintCategory::Security => write!(f, "security"),
141 LintCategory::Custom(ref name) => write!(f, "custom:{}", name),
142 }
143 }
144}
145
146#[derive(Clone, Debug)]
152pub struct LintMetadata {
153 pub id: LintId,
155 pub category: LintCategory,
157 pub summary: String,
159 pub explanation: String,
161 pub severity: Severity,
163 pub fixable: bool,
165 pub references: Vec<String>,
167}
168
169impl LintMetadata {
170 pub fn new(id: &str, category: LintCategory, summary: &str, severity: Severity) -> Self {
172 Self {
173 id: LintId::new(id),
174 category,
175 summary: summary.to_string(),
176 explanation: String::new(),
177 severity,
178 fixable: false,
179 references: Vec::new(),
180 }
181 }
182
183 pub fn with_explanation(mut self, text: &str) -> Self {
185 self.explanation = text.to_string();
186 self
187 }
188
189 pub fn fixable(mut self) -> Self {
191 self.fixable = true;
192 self
193 }
194
195 pub fn with_reference(mut self, url: &str) -> Self {
197 self.references.push(url.to_string());
198 self
199 }
200}
201
202#[derive(Clone, Debug, Default)]
208pub struct LintStats {
209 pub total_diagnostics: u64,
211 pub errors: u64,
213 pub warnings: u64,
215 pub infos: u64,
217 pub hints: u64,
219 pub decls_checked: u64,
221 pub fixes_applied: u64,
223 pub suppressions_honored: u64,
225}
226
227impl LintStats {
228 pub fn new() -> Self {
230 Self::default()
231 }
232
233 pub fn has_errors(&self) -> bool {
235 self.errors > 0
236 }
237
238 pub fn is_clean(&self) -> bool {
240 self.errors == 0 && self.warnings == 0
241 }
242
243 pub fn record(&mut self, sev: Severity) {
245 self.total_diagnostics += 1;
246 match sev {
247 Severity::Error => self.errors += 1,
248 Severity::Warning => self.warnings += 1,
249 Severity::Info => self.infos += 1,
250 Severity::Hint => self.hints += 1,
251 }
252 }
253}
254
255impl fmt::Display for LintStats {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(
258 f,
259 "LintStats {{ total: {}, errors: {}, warnings: {}, infos: {}, hints: {} }}",
260 self.total_diagnostics, self.errors, self.warnings, self.infos, self.hints
261 )
262 }
263}
264
265#[derive(Clone, Debug, PartialEq)]
271pub struct LintSuppressAnnotation {
272 pub ids: Vec<LintId>,
274 pub is_file_level: bool,
276 pub line: usize,
278}
279
280impl LintSuppressAnnotation {
281 pub fn single(id: &str, line: usize) -> Self {
283 Self {
284 ids: vec![LintId::new(id)],
285 is_file_level: false,
286 line,
287 }
288 }
289
290 pub fn file_level(ids: Vec<&str>) -> Self {
292 Self {
293 ids: ids.into_iter().map(LintId::new).collect(),
294 is_file_level: true,
295 line: 0,
296 }
297 }
298
299 pub fn suppresses(&self, id: &LintId) -> bool {
301 self.ids.contains(id)
302 }
303}
304
305#[derive(Clone, Debug)]
311pub struct LintReport {
312 pub filename: String,
314 pub diagnostics: Vec<LintDiagnostic>,
316 pub stats: LintStats,
318 pub fixes_applied: bool,
320}
321
322impl LintReport {
323 pub fn empty(filename: &str) -> Self {
325 Self {
326 filename: filename.to_string(),
327 diagnostics: Vec::new(),
328 stats: LintStats::new(),
329 fixes_applied: false,
330 }
331 }
332
333 pub fn add_diagnostic(&mut self, diag: LintDiagnostic) {
335 self.stats.record(diag.severity);
336 self.diagnostics.push(diag);
337 }
338
339 pub fn at_severity(&self, sev: Severity) -> Vec<&LintDiagnostic> {
341 self.diagnostics
342 .iter()
343 .filter(|d| d.severity <= sev)
344 .collect()
345 }
346
347 pub fn is_clean(&self) -> bool {
349 self.stats.is_clean()
350 }
351}
352
353impl fmt::Display for LintReport {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 write!(
356 f,
357 "LintReport[{}] {{ {} diags, {} }}",
358 self.filename,
359 self.diagnostics.len(),
360 self.stats
361 )
362 }
363}
364
365#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_lint_pass_new() {
375 let pass = LintPass::new("style");
376 assert_eq!(pass.name, "style");
377 assert!(pass.enabled);
378 assert!(!pass.can_fix);
379 }
380
381 #[test]
382 fn test_lint_pass_with_lint() {
383 let pass = LintPass::new("unused").with_lint("unused_variable");
384 assert_eq!(pass.lint_ids.len(), 1);
385 assert_eq!(pass.lint_ids[0].as_str(), "unused_variable");
386 }
387
388 #[test]
389 fn test_lint_pass_disabled() {
390 let pass = LintPass::new("x").disabled();
391 assert!(!pass.enabled);
392 }
393
394 #[test]
395 fn test_lint_pass_with_fixes() {
396 let pass = LintPass::new("x").with_fixes();
397 assert!(pass.can_fix);
398 }
399
400 #[test]
401 fn test_lint_category_display() {
402 assert_eq!(format!("{}", LintCategory::Correctness), "correctness");
403 assert_eq!(format!("{}", LintCategory::Style), "style");
404 assert_eq!(format!("{}", LintCategory::Naming), "naming");
405 }
406
407 #[test]
408 fn test_lint_metadata_new() {
409 let meta = LintMetadata::new(
410 "unused_variable",
411 LintCategory::Redundancy,
412 "Unused variable",
413 Severity::Warning,
414 );
415 assert_eq!(meta.id.as_str(), "unused_variable");
416 assert_eq!(meta.severity, Severity::Warning);
417 assert!(!meta.fixable);
418 }
419
420 #[test]
421 fn test_lint_metadata_fixable() {
422 let meta = LintMetadata::new("x", LintCategory::Style, "X", Severity::Hint).fixable();
423 assert!(meta.fixable);
424 }
425
426 #[test]
427 fn test_lint_metadata_with_explanation() {
428 let meta = LintMetadata::new("x", LintCategory::Style, "X", Severity::Hint)
429 .with_explanation("More details here.");
430 assert!(!meta.explanation.is_empty());
431 }
432
433 #[test]
434 fn test_lint_stats_default() {
435 let s = LintStats::new();
436 assert!(!s.has_errors());
437 assert!(s.is_clean());
438 assert_eq!(s.total_diagnostics, 0);
439 }
440
441 #[test]
442 fn test_lint_stats_record_error() {
443 let mut s = LintStats::new();
444 s.record(Severity::Error);
445 assert!(s.has_errors());
446 assert!(!s.is_clean());
447 assert_eq!(s.errors, 1);
448 }
449
450 #[test]
451 fn test_lint_stats_record_warning() {
452 let mut s = LintStats::new();
453 s.record(Severity::Warning);
454 assert!(!s.has_errors());
455 assert!(!s.is_clean());
456 assert_eq!(s.warnings, 1);
457 }
458
459 #[test]
460 fn test_lint_stats_display() {
461 let mut s = LintStats::new();
462 s.record(Severity::Error);
463 s.record(Severity::Warning);
464 let text = format!("{}", s);
465 assert!(text.contains("total: 2"));
466 assert!(text.contains("errors: 1"));
467 }
468
469 #[test]
470 fn test_lint_suppress_annotation_single() {
471 let ann = LintSuppressAnnotation::single("unused_variable", 5);
472 assert_eq!(ann.ids.len(), 1);
473 assert_eq!(ann.line, 5);
474 assert!(!ann.is_file_level);
475 }
476
477 #[test]
478 fn test_lint_suppress_annotation_file_level() {
479 let ann = LintSuppressAnnotation::file_level(vec!["dead_code", "unused_import"]);
480 assert_eq!(ann.ids.len(), 2);
481 assert!(ann.is_file_level);
482 }
483
484 #[test]
485 fn test_lint_suppress_annotation_suppresses() {
486 let ann = LintSuppressAnnotation::single("unused_variable", 0);
487 assert!(ann.suppresses(&LintId::new("unused_variable")));
488 assert!(!ann.suppresses(&LintId::new("dead_code")));
489 }
490
491 #[test]
492 fn test_lint_report_empty() {
493 let r = LintReport::empty("foo.ox");
494 assert_eq!(r.filename, "foo.ox");
495 assert!(r.is_clean());
496 assert!(r.diagnostics.is_empty());
497 }
498
499 #[test]
500 fn test_lint_report_display() {
501 let r = LintReport::empty("bar.ox");
502 let s = format!("{}", r);
503 assert!(s.contains("bar.ox"));
504 }
505
506 #[test]
507 fn test_lint_id_matches_pattern_wildcard() {
508 let id = LintId::new("unused_variable");
509 assert!(id.matches_pattern("*"));
510 assert!(id.matches_pattern("unused_*"));
511 assert!(!id.matches_pattern("dead_*"));
512 }
513
514 #[test]
515 fn test_lint_stats_hints_and_infos() {
516 let mut s = LintStats::new();
517 s.record(Severity::Info);
518 s.record(Severity::Hint);
519 assert_eq!(s.infos, 1);
520 assert_eq!(s.hints, 1);
521 assert!(s.is_clean());
522 }
523
524 #[test]
525 fn test_lint_metadata_with_reference() {
526 let meta = LintMetadata::new("x", LintCategory::Correctness, "x", Severity::Error)
527 .with_reference("https://oxilean.org/lint/x");
528 assert_eq!(meta.references.len(), 1);
529 }
530}
531
532#[derive(Clone, Debug, Default)]
538pub struct LintRuleSet {
539 pub name: String,
541 pub ids: Vec<LintId>,
543}
544
545impl LintRuleSet {
546 pub fn new(name: &str) -> Self {
548 Self {
549 name: name.to_string(),
550 ids: Vec::new(),
551 }
552 }
553
554 pub fn add(&mut self, id: &str) {
556 self.ids.push(LintId::new(id));
557 }
558
559 pub fn len(&self) -> usize {
561 self.ids.len()
562 }
563
564 pub fn is_empty(&self) -> bool {
566 self.ids.is_empty()
567 }
568
569 pub fn contains(&self, id: &LintId) -> bool {
571 self.ids.contains(id)
572 }
573}
574
575impl std::fmt::Display for LintRuleSet {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 write!(f, "LintRuleSet[{}]({} rules)", self.name, self.ids.len())
578 }
579}
580
581#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
587pub enum LintLevel {
588 Allow,
590 Warn,
592 Deny,
594}
595
596impl std::fmt::Display for LintLevel {
597 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598 match self {
599 LintLevel::Allow => write!(f, "allow"),
600 LintLevel::Warn => write!(f, "warn"),
601 LintLevel::Deny => write!(f, "deny"),
602 }
603 }
604}
605
606#[cfg(test)]
611mod extra_tests {
612 use super::*;
613
614 #[test]
615 fn test_lint_rule_set_new() {
616 let s = LintRuleSet::new("default");
617 assert_eq!(s.name, "default");
618 assert!(s.is_empty());
619 }
620
621 #[test]
622 fn test_lint_rule_set_add() {
623 let mut s = LintRuleSet::new("style");
624 s.add("unused_variable");
625 assert_eq!(s.len(), 1);
626 assert!(s.contains(&LintId::new("unused_variable")));
627 }
628
629 #[test]
630 fn test_lint_rule_set_contains_false() {
631 let s = LintRuleSet::new("x");
632 assert!(!s.contains(&LintId::new("nonexistent")));
633 }
634
635 #[test]
636 fn test_lint_rule_set_display() {
637 let mut s = LintRuleSet::new("perf");
638 s.add("redundant_clone");
639 let txt = format!("{}", s);
640 assert!(txt.contains("perf"));
641 assert!(txt.contains("1 rules"));
642 }
643
644 #[test]
645 fn test_lint_level_ordering() {
646 assert!(LintLevel::Deny > LintLevel::Warn);
647 assert!(LintLevel::Warn > LintLevel::Allow);
648 }
649
650 #[test]
651 fn test_lint_level_display() {
652 assert_eq!(format!("{}", LintLevel::Allow), "allow");
653 assert_eq!(format!("{}", LintLevel::Warn), "warn");
654 assert_eq!(format!("{}", LintLevel::Deny), "deny");
655 }
656
657 #[test]
658 fn test_lint_pass_multiple_lints() {
659 let pass = LintPass::new("all")
660 .with_lint("unused_variable")
661 .with_lint("dead_code")
662 .with_lint("unused_import");
663 assert_eq!(pass.lint_ids.len(), 3);
664 }
665
666 #[test]
667 fn test_lint_report_add_and_severity() {
668 use framework::Severity;
669 let r = LintReport::empty("test.ox");
670 assert!(r.is_clean());
673 let _ = r.at_severity(Severity::Error);
674 }
675
676 #[test]
677 fn test_lint_stats_multiple_records() {
678 use framework::Severity;
679 let mut s = LintStats::new();
680 s.record(Severity::Error);
681 s.record(Severity::Warning);
682 s.record(Severity::Info);
683 s.record(Severity::Hint);
684 assert_eq!(s.total_diagnostics, 4);
685 assert_eq!(s.errors, 1);
686 assert_eq!(s.warnings, 1);
687 assert_eq!(s.infos, 1);
688 assert_eq!(s.hints, 1);
689 }
690}
691
692#[derive(Clone, Debug, Default)]
698pub struct LintResult {
699 pub diagnostics: Vec<LintDiagnostic>,
701}
702
703impl LintResult {
704 pub fn new() -> Self {
706 Self::default()
707 }
708
709 pub fn add(&mut self, diag: LintDiagnostic) {
711 self.diagnostics.push(diag);
712 }
713
714 pub fn has_diagnostics(&self) -> bool {
716 !self.diagnostics.is_empty()
717 }
718
719 pub fn len(&self) -> usize {
721 self.diagnostics.len()
722 }
723
724 pub fn is_empty(&self) -> bool {
726 self.diagnostics.is_empty()
727 }
728
729 pub fn is_clean(&self) -> bool {
731 self.diagnostics.is_empty()
732 }
733
734 pub fn at_severity(&self, sev: Severity) -> Vec<&LintDiagnostic> {
736 self.diagnostics
737 .iter()
738 .filter(|d| d.severity <= sev)
739 .collect()
740 }
741
742 pub fn merge(&mut self, other: LintResult) {
744 self.diagnostics.extend(other.diagnostics);
745 }
746}
747
748impl std::fmt::Display for LintResult {
749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750 write!(f, "LintResult({} diagnostics)", self.diagnostics.len())
751 }
752}
753
754pub struct LintConfigBuilder {
760 config: LintConfig,
761}
762
763impl LintConfigBuilder {
764 pub fn new() -> Self {
766 Self {
767 config: LintConfig::default(),
768 }
769 }
770
771 pub fn allow(mut self, id: &str) -> Self {
773 self.config.enabled.insert(LintId::new(id));
774 self
775 }
776
777 pub fn deny(mut self, id: &str) -> Self {
779 self.config.disabled.insert(LintId::new(id));
780 self
781 }
782
783 pub fn build(self) -> LintConfig {
785 self.config
786 }
787}
788
789impl Default for LintConfigBuilder {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795#[cfg(test)]
800mod lint_result_tests {
801 use super::*;
802 use framework::{LintId, Severity};
803
804 fn mk_diag(sev: Severity) -> LintDiagnostic {
805 use framework::SourceRange;
806 LintDiagnostic::new(
807 LintId::new("test"),
808 sev,
809 "test message",
810 SourceRange::default(),
811 )
812 }
813
814 #[test]
815 fn test_lint_result_empty() {
816 let r = LintResult::new();
817 assert!(r.is_clean());
818 assert_eq!(r.len(), 0);
819 }
820
821 #[test]
822 fn test_lint_result_add() {
823 let mut r = LintResult::new();
824 r.add(mk_diag(Severity::Warning));
825 assert!(r.has_diagnostics());
826 assert_eq!(r.len(), 1);
827 }
828
829 #[test]
830 fn test_lint_result_at_severity() {
831 let mut r = LintResult::new();
832 r.add(mk_diag(Severity::Warning));
833 r.add(mk_diag(Severity::Error));
834 let errors = r.at_severity(Severity::Error);
835 assert_eq!(errors.len(), 1);
836 }
837
838 #[test]
839 fn test_lint_result_merge() {
840 let mut r1 = LintResult::new();
841 let mut r2 = LintResult::new();
842 r1.add(mk_diag(Severity::Warning));
843 r2.add(mk_diag(Severity::Error));
844 r1.merge(r2);
845 assert_eq!(r1.len(), 2);
846 }
847
848 #[test]
849 fn test_lint_result_display() {
850 let r = LintResult::new();
851 let s = format!("{}", r);
852 assert!(s.contains("LintResult"));
853 }
854
855 #[test]
856 fn test_lint_config_builder() {
857 let cfg = LintConfigBuilder::new()
858 .allow("dead_code")
859 .deny("unused_variable")
860 .build();
861 assert!(cfg.is_allowed(&LintId::new("dead_code")));
862 assert!(cfg.is_denied(&LintId::new("unused_variable")));
863 }
864
865 #[test]
866 fn test_lint_category_all_variants() {
867 let cats = vec![
868 LintCategory::Correctness,
869 LintCategory::Style,
870 LintCategory::Performance,
871 LintCategory::Complexity,
872 LintCategory::Deprecation,
873 LintCategory::Documentation,
874 LintCategory::Naming,
875 LintCategory::Redundancy,
876 ];
877 for cat in cats {
878 let s = format!("{}", cat);
879 assert!(!s.is_empty());
880 }
881 }
882
883 #[test]
884 fn test_lint_suppress_annotation_suppresses_false() {
885 let ann = LintSuppressAnnotation::single("unused_variable", 0);
886 assert!(!ann.suppresses(&LintId::new("dead_code")));
887 }
888
889 #[test]
890 fn test_lint_rule_set_add_multiple() {
891 let mut s = LintRuleSet::new("default");
892 for name in ["a", "b", "c", "d"] {
893 s.add(name);
894 }
895 assert_eq!(s.len(), 4);
896 }
897}
898
899#[derive(Clone, Debug, Default)]
903pub struct LintFilter {
904 include_patterns: Vec<String>,
906 exclude_patterns: Vec<String>,
908 min_severity: Option<Severity>,
910}
911
912impl LintFilter {
913 pub fn new() -> Self {
915 Self::default()
916 }
917
918 pub fn include(mut self, pattern: &str) -> Self {
920 self.include_patterns.push(pattern.to_string());
921 self
922 }
923
924 pub fn exclude(mut self, pattern: &str) -> Self {
926 self.exclude_patterns.push(pattern.to_string());
927 self
928 }
929
930 pub fn min_severity(mut self, sev: Severity) -> Self {
932 self.min_severity = Some(sev);
933 self
934 }
935
936 pub fn accepts(&self, diag: &LintDiagnostic) -> bool {
938 if let Some(min) = &self.min_severity {
940 if diag.severity > *min {
941 return false;
942 }
943 }
944
945 if !self.include_patterns.is_empty() {
947 let matched = self
948 .include_patterns
949 .iter()
950 .any(|p| diag.lint_id.matches_pattern(p));
951 if !matched {
952 return false;
953 }
954 }
955
956 let excluded = self
958 .exclude_patterns
959 .iter()
960 .any(|p| diag.lint_id.matches_pattern(p));
961 !excluded
962 }
963
964 pub fn apply<'a>(&self, diags: &'a [LintDiagnostic]) -> Vec<&'a LintDiagnostic> {
966 diags.iter().filter(|d| self.accepts(d)).collect()
967 }
968}
969
970#[derive(Clone, Copy, Debug, PartialEq, Eq)]
974pub enum LintOutputFormat {
975 Text,
977 Json,
979 GitHubActions,
981 Count,
983}
984
985impl std::fmt::Display for LintOutputFormat {
986 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
987 match self {
988 LintOutputFormat::Text => write!(f, "text"),
989 LintOutputFormat::Json => write!(f, "json"),
990 LintOutputFormat::GitHubActions => write!(f, "github-actions"),
991 LintOutputFormat::Count => write!(f, "count"),
992 }
993 }
994}
995
996impl LintOutputFormat {
997 pub fn parse(s: &str) -> Option<Self> {
999 match s {
1000 "text" => Some(LintOutputFormat::Text),
1001 "json" => Some(LintOutputFormat::Json),
1002 "github-actions" => Some(LintOutputFormat::GitHubActions),
1003 "count" => Some(LintOutputFormat::Count),
1004 _ => None,
1005 }
1006 }
1007}
1008
1009#[derive(Clone, Debug)]
1013pub struct LintProfile {
1014 pub name: String,
1016 pub rule_sets: Vec<LintRuleSet>,
1018 pub overrides: Vec<(LintId, LintLevel)>,
1020}
1021
1022impl LintProfile {
1023 pub fn new(name: &str) -> Self {
1025 Self {
1026 name: name.to_string(),
1027 rule_sets: Vec::new(),
1028 overrides: Vec::new(),
1029 }
1030 }
1031
1032 pub fn with_rule_set(mut self, rs: LintRuleSet) -> Self {
1034 self.rule_sets.push(rs);
1035 self
1036 }
1037
1038 pub fn with_override(mut self, id: &str, level: LintLevel) -> Self {
1040 self.overrides.push((LintId::new(id), level));
1041 self
1042 }
1043
1044 pub fn all_ids(&self) -> Vec<&LintId> {
1046 self.rule_sets.iter().flat_map(|rs| rs.ids.iter()).collect()
1047 }
1048
1049 pub fn effective_level(&self, id: &LintId) -> Option<LintLevel> {
1051 self.overrides
1052 .iter()
1053 .rev()
1054 .find_map(|(k, v)| if k == id { Some(*v) } else { None })
1055 }
1056}
1057
1058#[cfg(test)]
1059mod lint_profile_tests {
1060 use super::*;
1061
1062 fn mk_diag_with_id(id: &str, sev: Severity) -> LintDiagnostic {
1063 use framework::{LintId, SourceRange};
1064 LintDiagnostic::new(LintId::new(id), sev, "msg", SourceRange::default())
1065 }
1066
1067 #[test]
1068 fn test_lint_filter_no_constraints() {
1069 let filter = LintFilter::new();
1070 let diag = mk_diag_with_id("unused_variable", Severity::Warning);
1071 assert!(filter.accepts(&diag));
1072 }
1073
1074 #[test]
1075 fn test_lint_filter_min_severity_ok() {
1076 let filter = LintFilter::new().min_severity(Severity::Warning);
1077 let warn = mk_diag_with_id("x", Severity::Warning);
1078 let info = mk_diag_with_id("x", Severity::Info);
1079 assert!(filter.accepts(&warn));
1080 assert!(!filter.accepts(&info));
1081 }
1082
1083 #[test]
1084 fn test_lint_filter_include_pattern() {
1085 let filter = LintFilter::new().include("unused_*");
1086 let pass = mk_diag_with_id("unused_variable", Severity::Warning);
1087 let fail = mk_diag_with_id("dead_code", Severity::Warning);
1088 assert!(filter.accepts(&pass));
1089 assert!(!filter.accepts(&fail));
1090 }
1091
1092 #[test]
1093 fn test_lint_filter_exclude_pattern() {
1094 let filter = LintFilter::new().exclude("dead_*");
1095 let pass = mk_diag_with_id("unused_variable", Severity::Warning);
1096 let fail = mk_diag_with_id("dead_code", Severity::Warning);
1097 assert!(filter.accepts(&pass));
1098 assert!(!filter.accepts(&fail));
1099 }
1100
1101 #[test]
1102 fn test_lint_filter_apply() {
1103 let filter = LintFilter::new().min_severity(Severity::Error);
1104 let diags = vec![
1105 mk_diag_with_id("a", Severity::Error),
1106 mk_diag_with_id("b", Severity::Warning),
1107 mk_diag_with_id("c", Severity::Info),
1108 ];
1109 let accepted = filter.apply(&diags);
1110 assert_eq!(accepted.len(), 1);
1111 }
1112
1113 #[test]
1114 fn test_lint_output_format_display() {
1115 assert_eq!(format!("{}", LintOutputFormat::Text), "text");
1116 assert_eq!(format!("{}", LintOutputFormat::Json), "json");
1117 assert_eq!(format!("{}", LintOutputFormat::Count), "count");
1118 }
1119
1120 #[test]
1121 fn test_lint_output_format_from_str() {
1122 assert_eq!(
1123 LintOutputFormat::parse("json"),
1124 Some(LintOutputFormat::Json)
1125 );
1126 assert_eq!(LintOutputFormat::parse("unknown"), None);
1127 }
1128
1129 #[test]
1130 fn test_lint_profile_basic() {
1131 let profile = LintProfile::new("strict");
1132 assert_eq!(profile.name, "strict");
1133 assert!(profile.rule_sets.is_empty());
1134 }
1135
1136 #[test]
1137 fn test_lint_profile_with_rule_set() {
1138 let mut rs = LintRuleSet::new("style");
1139 rs.add("unused_variable");
1140 rs.add("dead_code");
1141 let profile = LintProfile::new("standard").with_rule_set(rs);
1142 assert_eq!(profile.all_ids().len(), 2);
1143 }
1144
1145 #[test]
1146 fn test_lint_profile_overrides() {
1147 let profile = LintProfile::new("strict").with_override("dead_code", LintLevel::Deny);
1148 let id = LintId::new("dead_code");
1149 assert_eq!(profile.effective_level(&id), Some(LintLevel::Deny));
1150 let id2 = LintId::new("nonexistent");
1151 assert_eq!(profile.effective_level(&id2), None);
1152 }
1153
1154 #[test]
1155 fn test_lint_stats_is_clean_after_info_only() {
1156 let mut s = LintStats::new();
1157 s.record(Severity::Info);
1158 assert!(s.is_clean());
1159 }
1160
1161 #[test]
1162 fn test_lint_filter_both_include_and_exclude() {
1163 let filter = LintFilter::new()
1165 .include("unused_*")
1166 .exclude("unused_import");
1167 let pass = mk_diag_with_id("unused_variable", Severity::Hint);
1168 let excluded = mk_diag_with_id("unused_import", Severity::Hint);
1169 assert!(filter.accepts(&pass));
1170 assert!(!filter.accepts(&excluded));
1171 }
1172}
1173
1174#[derive(Debug, Default)]
1180#[allow(dead_code)]
1181pub struct LintDatabase {
1182 entries: std::collections::HashMap<String, LintEntry>,
1183}
1184
1185#[derive(Clone, Debug)]
1187#[allow(dead_code)]
1188pub struct LintEntry {
1189 pub id: LintId,
1190 pub description: String,
1191 pub default_level: Severity,
1192 pub tags: Vec<String>,
1193 pub has_autofix: bool,
1194}
1195
1196impl LintEntry {
1197 #[allow(dead_code)]
1198 pub fn new(id: &str, description: &str, default_level: Severity) -> Self {
1199 Self {
1200 id: LintId::new(id),
1201 description: description.to_string(),
1202 default_level,
1203 tags: Vec::new(),
1204 has_autofix: false,
1205 }
1206 }
1207
1208 #[allow(dead_code)]
1209 pub fn with_tag(mut self, tag: &str) -> Self {
1210 self.tags.push(tag.to_string());
1211 self
1212 }
1213
1214 #[allow(dead_code)]
1215 pub fn with_autofix(mut self) -> Self {
1216 self.has_autofix = true;
1217 self
1218 }
1219}
1220
1221impl LintDatabase {
1222 #[allow(dead_code)]
1223 pub fn new() -> Self {
1224 Self {
1225 entries: std::collections::HashMap::new(),
1226 }
1227 }
1228
1229 #[allow(dead_code)]
1231 pub fn register(&mut self, entry: LintEntry) {
1232 self.entries.insert(entry.id.as_str().to_string(), entry);
1233 }
1234
1235 #[allow(dead_code)]
1237 pub fn get(&self, id: &str) -> Option<&LintEntry> {
1238 self.entries.get(id)
1239 }
1240
1241 #[allow(dead_code)]
1243 pub fn all_ids(&self) -> Vec<&str> {
1244 let mut ids: Vec<&str> = self.entries.keys().map(|s| s.as_str()).collect();
1245 ids.sort();
1246 ids
1247 }
1248
1249 #[allow(dead_code)]
1251 pub fn by_tag(&self, tag: &str) -> Vec<&LintEntry> {
1252 self.entries
1253 .values()
1254 .filter(|e| e.tags.iter().any(|t| t == tag))
1255 .collect()
1256 }
1257
1258 #[allow(dead_code)]
1260 pub fn with_autofix(&self) -> Vec<&LintEntry> {
1261 self.entries.values().filter(|e| e.has_autofix).collect()
1262 }
1263
1264 #[allow(dead_code)]
1266 pub fn len(&self) -> usize {
1267 self.entries.len()
1268 }
1269
1270 #[allow(dead_code)]
1271 pub fn is_empty(&self) -> bool {
1272 self.entries.is_empty()
1273 }
1274}
1275
1276#[derive(Clone, Debug)]
1282#[allow(dead_code)]
1283pub struct LintRunOptions {
1284 pub include_info: bool,
1286 pub include_hints: bool,
1288 pub max_diagnostics: Option<usize>,
1290 pub auto_apply_fixes: bool,
1292 pub fail_fast: bool,
1294}
1295
1296impl LintRunOptions {
1297 #[allow(dead_code)]
1298 pub fn default_opts() -> Self {
1299 Self {
1300 include_info: true,
1301 include_hints: false,
1302 max_diagnostics: None,
1303 auto_apply_fixes: false,
1304 fail_fast: false,
1305 }
1306 }
1307
1308 #[allow(dead_code)]
1309 pub fn strict() -> Self {
1310 Self {
1311 include_info: true,
1312 include_hints: true,
1313 max_diagnostics: None,
1314 auto_apply_fixes: false,
1315 fail_fast: true,
1316 }
1317 }
1318}
1319
1320impl Default for LintRunOptions {
1321 fn default() -> Self {
1322 Self::default_opts()
1323 }
1324}
1325
1326#[allow(dead_code)]
1332pub struct LintSummaryReport {
1333 pub total_diagnostics: usize,
1334 pub by_severity: std::collections::HashMap<String, usize>,
1335 pub by_category: std::collections::HashMap<String, usize>,
1336 pub files_with_issues: usize,
1337 pub auto_fixes_available: usize,
1338}
1339
1340impl LintSummaryReport {
1341 #[allow(dead_code)]
1342 pub fn new() -> Self {
1343 Self {
1344 total_diagnostics: 0,
1345 by_severity: std::collections::HashMap::new(),
1346 by_category: std::collections::HashMap::new(),
1347 files_with_issues: 0,
1348 auto_fixes_available: 0,
1349 }
1350 }
1351
1352 #[allow(dead_code)]
1354 pub fn add(&mut self, diag: &LintDiagnostic) {
1355 self.total_diagnostics += 1;
1356 let sev_key = format!("{:?}", diag.severity).to_lowercase();
1357 *self.by_severity.entry(sev_key).or_insert(0) += 1;
1358 if diag.fix.is_some() {
1359 self.auto_fixes_available += 1;
1360 }
1361 }
1362
1363 #[allow(dead_code)]
1365 pub fn is_clean(&self) -> bool {
1366 let errors = self.by_severity.get("error").copied().unwrap_or(0);
1367 let warnings = self.by_severity.get("warning").copied().unwrap_or(0);
1368 errors == 0 && warnings == 0
1369 }
1370}
1371
1372impl Default for LintSummaryReport {
1373 fn default() -> Self {
1374 Self::new()
1375 }
1376}
1377
1378#[allow(dead_code)]
1384pub struct LintIgnoreList {
1385 ignored: std::collections::HashSet<String>,
1386}
1387
1388impl LintIgnoreList {
1389 #[allow(dead_code)]
1390 pub fn new() -> Self {
1391 Self {
1392 ignored: std::collections::HashSet::new(),
1393 }
1394 }
1395
1396 #[allow(dead_code)]
1398 pub fn ignore(&mut self, id: &str) {
1399 self.ignored.insert(id.to_string());
1400 }
1401
1402 #[allow(dead_code)]
1404 pub fn is_ignored(&self, id: &str) -> bool {
1405 self.ignored.contains(id)
1406 }
1407
1408 #[allow(dead_code)]
1410 pub fn filter<'a>(&self, diags: &'a [LintDiagnostic]) -> Vec<&'a LintDiagnostic> {
1411 diags
1412 .iter()
1413 .filter(|d| !self.is_ignored(d.lint_id.as_str()))
1414 .collect()
1415 }
1416
1417 #[allow(dead_code)]
1419 pub fn len(&self) -> usize {
1420 self.ignored.len()
1421 }
1422}
1423
1424impl Default for LintIgnoreList {
1425 fn default() -> Self {
1426 Self::new()
1427 }
1428}
1429
1430#[allow(dead_code)]
1436pub struct LintFormatter {
1437 pub format: LintOutputFormat,
1438}
1439
1440impl LintFormatter {
1441 #[allow(dead_code)]
1442 pub fn new(format: LintOutputFormat) -> Self {
1443 Self { format }
1444 }
1445
1446 #[allow(dead_code)]
1448 pub fn format_one(&self, diag: &LintDiagnostic) -> String {
1449 let file = diag.range.file.as_deref().unwrap_or("unknown");
1450 let offset = diag.range.start;
1451 match self.format {
1452 LintOutputFormat::Text => {
1453 format!(
1454 "[{:?}] {} at {}:{}: {}",
1455 diag.severity,
1456 diag.lint_id.as_str(),
1457 file,
1458 offset,
1459 diag.message
1460 )
1461 }
1462 LintOutputFormat::GitHubActions => {
1463 let level = match diag.severity {
1464 Severity::Error => "error",
1465 Severity::Warning => "warning",
1466 Severity::Hint | Severity::Info => "notice",
1467 };
1468 format!(
1469 "::{} file={},line={}::{}",
1470 level, file, offset, diag.message
1471 )
1472 }
1473 LintOutputFormat::Json => {
1474 format!(
1475 "{{\"id\":\"{}\",\"severity\":\"{:?}\",\"file\":\"{}\",\"line\":{},\"message\":\"{}\"}}",
1476 diag.lint_id.as_str(),
1477 diag.severity,
1478 file,
1479 offset,
1480 diag.message.replace('"', "\\\"")
1481 )
1482 }
1483 LintOutputFormat::Count => {
1484 format!(
1485 "{}:{}:{:?}:{} - {}",
1486 file,
1487 offset,
1488 diag.severity,
1489 diag.lint_id.as_str(),
1490 diag.message
1491 )
1492 }
1493 }
1494 }
1495
1496 #[allow(dead_code)]
1498 pub fn format_all(&self, diags: &[LintDiagnostic]) -> String {
1499 diags
1500 .iter()
1501 .map(|d| self.format_one(d))
1502 .collect::<Vec<_>>()
1503 .join("\n")
1504 }
1505}
1506
1507#[allow(dead_code)]
1513pub struct LintTrend {
1514 snapshots: Vec<(String, usize)>,
1515}
1516
1517impl LintTrend {
1518 #[allow(dead_code)]
1519 pub fn new() -> Self {
1520 Self {
1521 snapshots: Vec::new(),
1522 }
1523 }
1524
1525 #[allow(dead_code)]
1527 pub fn record(&mut self, label: &str, count: usize) {
1528 self.snapshots.push((label.to_string(), count));
1529 }
1530
1531 #[allow(dead_code)]
1533 pub fn is_improving(&self) -> bool {
1534 if self.snapshots.len() < 2 {
1535 return false;
1536 }
1537 let prev = self.snapshots[self.snapshots.len() - 2].1;
1538 let latest = self.snapshots[self.snapshots.len() - 1].1;
1539 latest < prev
1540 }
1541
1542 #[allow(dead_code)]
1544 pub fn latest_count(&self) -> usize {
1545 self.snapshots.last().map(|(_, c)| *c).unwrap_or(0)
1546 }
1547
1548 #[allow(dead_code)]
1550 pub fn snapshot_count(&self) -> usize {
1551 self.snapshots.len()
1552 }
1553}
1554
1555impl Default for LintTrend {
1556 fn default() -> Self {
1557 Self::new()
1558 }
1559}
1560
1561#[allow(dead_code)]
1567pub struct LintBaseline {
1568 known: std::collections::HashSet<String>,
1570}
1571
1572impl LintBaseline {
1573 #[allow(dead_code)]
1574 pub fn new() -> Self {
1575 Self {
1576 known: std::collections::HashSet::new(),
1577 }
1578 }
1579
1580 #[allow(dead_code)]
1582 pub fn add(&mut self, diag: &LintDiagnostic) {
1583 let file = diag.range.file.as_deref().unwrap_or("unknown");
1584 let key = format!("{}:{}:{}", diag.lint_id.as_str(), file, diag.range.start);
1585 self.known.insert(key);
1586 }
1587
1588 #[allow(dead_code)]
1590 pub fn is_known(&self, diag: &LintDiagnostic) -> bool {
1591 let file = diag.range.file.as_deref().unwrap_or("unknown");
1592 let key = format!("{}:{}:{}", diag.lint_id.as_str(), file, diag.range.start);
1593 self.known.contains(&key)
1594 }
1595
1596 #[allow(dead_code)]
1598 pub fn new_diagnostics<'a>(&self, diags: &'a [LintDiagnostic]) -> Vec<&'a LintDiagnostic> {
1599 diags.iter().filter(|d| !self.is_known(d)).collect()
1600 }
1601
1602 #[allow(dead_code)]
1604 pub fn size(&self) -> usize {
1605 self.known.len()
1606 }
1607}
1608
1609impl Default for LintBaseline {
1610 fn default() -> Self {
1611 Self::new()
1612 }
1613}
1614
1615#[allow(dead_code)]
1621pub struct LintRuleGroup {
1622 pub name: String,
1623 pub description: String,
1624 pub rules: Vec<String>,
1625}
1626
1627impl LintRuleGroup {
1628 #[allow(dead_code)]
1629 pub fn new(name: &str, description: &str) -> Self {
1630 Self {
1631 name: name.to_string(),
1632 description: description.to_string(),
1633 rules: Vec::new(),
1634 }
1635 }
1636
1637 #[allow(dead_code)]
1638 pub fn add_rule(&mut self, rule: &str) {
1639 self.rules.push(rule.to_string());
1640 }
1641
1642 #[allow(dead_code)]
1643 pub fn rule_count(&self) -> usize {
1644 self.rules.len()
1645 }
1646
1647 #[allow(dead_code)]
1648 pub fn contains(&self, rule: &str) -> bool {
1649 self.rules.iter().any(|r| r == rule)
1650 }
1651}
1652
1653#[allow(dead_code)]
1659pub struct LintAggregator {
1660 diagnostics: Vec<LintDiagnostic>,
1661}
1662
1663impl LintAggregator {
1664 #[allow(dead_code)]
1665 pub fn new() -> Self {
1666 Self {
1667 diagnostics: Vec::new(),
1668 }
1669 }
1670
1671 #[allow(dead_code)]
1673 pub fn add(&mut self, diag: LintDiagnostic) {
1674 self.diagnostics.push(diag);
1675 }
1676
1677 #[allow(dead_code)]
1679 pub fn add_all(&mut self, diags: Vec<LintDiagnostic>) {
1680 self.diagnostics.extend(diags);
1681 }
1682
1683 #[allow(dead_code)]
1685 pub fn into_diagnostics(self) -> Vec<LintDiagnostic> {
1686 self.diagnostics
1687 }
1688
1689 #[allow(dead_code)]
1691 pub fn count(&self) -> usize {
1692 self.diagnostics.len()
1693 }
1694
1695 #[allow(dead_code)]
1697 pub fn count_by_severity(&self, severity: Severity) -> usize {
1698 self.diagnostics
1699 .iter()
1700 .filter(|d| d.severity == severity)
1701 .count()
1702 }
1703}
1704
1705impl Default for LintAggregator {
1706 fn default() -> Self {
1707 Self::new()
1708 }
1709}
1710
1711#[allow(dead_code)]
1717pub struct LintEventLog {
1718 events: Vec<LintEvent>,
1719 counter: u64,
1720}
1721
1722#[allow(dead_code)]
1724#[derive(Clone, Debug)]
1725pub struct LintEvent {
1726 pub id: u64,
1727 pub kind: LintEventKind,
1728 pub message: String,
1729}
1730
1731#[allow(dead_code)]
1733#[derive(Clone, Debug)]
1734pub enum LintEventKind {
1735 RuleStarted,
1736 RuleFinished,
1737 DiagnosticEmitted,
1738 FixApplied,
1739 FixSkipped,
1740 PassEnabled,
1741 PassDisabled,
1742}
1743
1744impl LintEventLog {
1745 #[allow(dead_code)]
1746 pub fn new() -> Self {
1747 Self {
1748 events: Vec::new(),
1749 counter: 0,
1750 }
1751 }
1752
1753 #[allow(dead_code)]
1754 pub fn log(&mut self, kind: LintEventKind, message: &str) -> u64 {
1755 self.counter += 1;
1756 let id = self.counter;
1757 self.events.push(LintEvent {
1758 id,
1759 kind,
1760 message: message.to_string(),
1761 });
1762 id
1763 }
1764
1765 #[allow(dead_code)]
1766 pub fn total(&self) -> usize {
1767 self.events.len()
1768 }
1769
1770 #[allow(dead_code)]
1771 pub fn events(&self) -> &[LintEvent] {
1772 &self.events
1773 }
1774}
1775
1776impl Default for LintEventLog {
1777 fn default() -> Self {
1778 Self::new()
1779 }
1780}
1781
1782#[allow(dead_code)]
1788pub struct LintDiff {
1789 pub added: Vec<String>,
1790 pub removed: Vec<String>,
1791}
1792
1793impl LintDiff {
1794 #[allow(dead_code)]
1796 pub fn compute(before: &[String], after: &[String]) -> Self {
1797 let before_set: std::collections::HashSet<&String> = before.iter().collect();
1798 let after_set: std::collections::HashSet<&String> = after.iter().collect();
1799 let added = after_set
1800 .difference(&before_set)
1801 .map(|s| s.to_string())
1802 .collect();
1803 let removed = before_set
1804 .difference(&after_set)
1805 .map(|s| s.to_string())
1806 .collect();
1807 Self { added, removed }
1808 }
1809
1810 #[allow(dead_code)]
1812 pub fn is_empty(&self) -> bool {
1813 self.added.is_empty() && self.removed.is_empty()
1814 }
1815}
1816
1817#[cfg(test)]
1822mod lib_extended_tests {
1823 use super::*;
1824 fn mk_diag(id: &str, severity: Severity) -> LintDiagnostic {
1825 LintDiagnostic::new(
1826 LintId::new(id),
1827 severity,
1828 "test",
1829 framework::SourceRange::with_file(0, 0, "test.ox".to_string()),
1830 )
1831 }
1832
1833 #[test]
1836 fn lint_database_register_and_get() {
1837 let mut db = LintDatabase::new();
1838 let entry = LintEntry::new("unused_import", "Remove unused imports", Severity::Warning)
1839 .with_tag("style")
1840 .with_autofix();
1841 db.register(entry);
1842 assert!(!db.is_empty());
1843 let found = db.get("unused_import").expect("key should exist");
1844 assert!(found.has_autofix);
1845 assert!(found.tags.contains(&"style".to_string()));
1846 }
1847
1848 #[test]
1849 fn lint_database_by_tag() {
1850 let mut db = LintDatabase::new();
1851 db.register(LintEntry::new("a", "a", Severity::Info).with_tag("security"));
1852 db.register(LintEntry::new("b", "b", Severity::Info).with_tag("style"));
1853 db.register(LintEntry::new("c", "c", Severity::Info).with_tag("security"));
1854 let sec = db.by_tag("security");
1855 assert_eq!(sec.len(), 2);
1856 }
1857
1858 #[test]
1859 fn lint_database_with_autofix() {
1860 let mut db = LintDatabase::new();
1861 db.register(LintEntry::new("fixable", "fixable", Severity::Warning).with_autofix());
1862 db.register(LintEntry::new("not_fixable", "no fix", Severity::Warning));
1863 let fixable = db.with_autofix();
1864 assert_eq!(fixable.len(), 1);
1865 }
1866
1867 #[test]
1870 fn lint_run_options_default() {
1871 let opts = LintRunOptions::default_opts();
1872 assert!(opts.include_info);
1873 assert!(!opts.include_hints);
1874 assert!(!opts.auto_apply_fixes);
1875 assert!(!opts.fail_fast);
1876 }
1877
1878 #[test]
1879 fn lint_run_options_strict() {
1880 let opts = LintRunOptions::strict();
1881 assert!(opts.include_hints);
1882 assert!(opts.fail_fast);
1883 }
1884
1885 #[test]
1888 fn lint_category_display() {
1889 assert_eq!(format!("{}", LintCategory::Style), "style");
1890 assert_eq!(format!("{}", LintCategory::Security), "security");
1891 assert_eq!(
1892 format!("{}", LintCategory::Custom("my_cat".to_string())),
1893 "custom:my_cat"
1894 );
1895 }
1896
1897 #[test]
1900 fn lint_summary_report_add() {
1901 let mut report = LintSummaryReport::new();
1902 let diag = mk_diag("test", Severity::Warning);
1903 report.add(&diag);
1904 assert_eq!(report.total_diagnostics, 1);
1905 assert!(!report.is_clean()); }
1907
1908 #[test]
1909 fn lint_summary_report_clean_with_info_only() {
1910 let mut report = LintSummaryReport::new();
1911 report.add(&mk_diag("test", Severity::Info));
1912 assert!(report.is_clean());
1913 }
1914
1915 #[test]
1918 fn lint_ignore_list_filters() {
1919 let mut ignore = LintIgnoreList::new();
1920 ignore.ignore("dead_code");
1921 ignore.ignore("unused_import");
1922 let diags = vec![
1923 mk_diag("dead_code", Severity::Warning),
1924 mk_diag("naming_convention", Severity::Warning),
1925 ];
1926 let filtered = ignore.filter(&diags);
1927 assert_eq!(filtered.len(), 1);
1928 assert_eq!(filtered[0].lint_id.as_str(), "naming_convention");
1929 }
1930
1931 #[test]
1932 fn lint_ignore_list_is_ignored() {
1933 let mut ignore = LintIgnoreList::new();
1934 ignore.ignore("foo");
1935 assert!(ignore.is_ignored("foo"));
1936 assert!(!ignore.is_ignored("bar"));
1937 assert_eq!(ignore.len(), 1);
1938 }
1939
1940 #[test]
1943 fn lint_output_format_display() {
1944 assert_eq!(format!("{}", LintOutputFormat::Text), "text");
1945 assert_eq!(format!("{}", LintOutputFormat::Json), "json");
1946 assert_eq!(
1947 format!("{}", LintOutputFormat::GitHubActions),
1948 "github-actions"
1949 );
1950 assert_eq!(format!("{}", LintOutputFormat::Count), "count");
1951 }
1952
1953 #[test]
1956 fn lint_formatter_text() {
1957 let formatter = LintFormatter::new(LintOutputFormat::Text);
1958 let diag = mk_diag("unused_import", Severity::Warning);
1959 let output = formatter.format_one(&diag);
1960 assert!(output.contains("unused_import"));
1961 assert!(output.contains("test.ox"));
1962 }
1963
1964 #[test]
1965 fn lint_formatter_github() {
1966 let formatter = LintFormatter::new(LintOutputFormat::GitHubActions);
1967 let diag = mk_diag("unused_import", Severity::Warning);
1968 let output = formatter.format_one(&diag);
1969 assert!(output.starts_with("::warning"));
1970 }
1971
1972 #[test]
1973 fn lint_formatter_json() {
1974 let formatter = LintFormatter::new(LintOutputFormat::Json);
1975 let diag = mk_diag("foo", Severity::Error);
1976 let output = formatter.format_one(&diag);
1977 assert!(output.starts_with('{'));
1978 assert!(output.contains("\"id\":\"foo\""));
1979 }
1980
1981 #[test]
1982 fn lint_formatter_compact() {
1983 let formatter = LintFormatter::new(LintOutputFormat::Count);
1984 let diag = mk_diag("bar", Severity::Info);
1985 let output = formatter.format_one(&diag);
1986 assert!(output.contains("bar"));
1987 }
1988
1989 #[test]
1990 fn lint_formatter_format_all() {
1991 let formatter = LintFormatter::new(LintOutputFormat::Count);
1992 let diags = vec![
1993 mk_diag("a", Severity::Warning),
1994 mk_diag("b", Severity::Info),
1995 ];
1996 let output = formatter.format_all(&diags);
1997 assert!(output.contains('\n'));
1998 }
1999
2000 #[test]
2003 fn lint_trend_improving() {
2004 let mut trend = LintTrend::new();
2005 trend.record("v1", 10);
2006 trend.record("v2", 5);
2007 assert!(trend.is_improving());
2008 assert_eq!(trend.latest_count(), 5);
2009 assert_eq!(trend.snapshot_count(), 2);
2010 }
2011
2012 #[test]
2013 fn lint_trend_not_improving() {
2014 let mut trend = LintTrend::new();
2015 trend.record("v1", 3);
2016 trend.record("v2", 7);
2017 assert!(!trend.is_improving());
2018 }
2019
2020 #[test]
2023 fn lint_baseline_filters_known() {
2024 let diag = mk_diag("dead_code", Severity::Warning);
2025 let mut baseline = LintBaseline::new();
2026 baseline.add(&diag);
2027 assert!(baseline.is_known(&diag));
2028
2029 let new_diag = mk_diag("new_lint", Severity::Warning);
2030 assert!(!baseline.is_known(&new_diag));
2031
2032 let all = vec![diag, new_diag];
2033 let new_only = baseline.new_diagnostics(&all);
2034 assert_eq!(new_only.len(), 1);
2035 assert_eq!(new_only[0].lint_id.as_str(), "new_lint");
2036 }
2037
2038 #[test]
2041 fn lint_rule_group_contains() {
2042 let mut group = LintRuleGroup::new("style", "Style rules");
2043 group.add_rule("naming_convention");
2044 group.add_rule("unused_import");
2045 assert!(group.contains("naming_convention"));
2046 assert!(!group.contains("dead_code"));
2047 assert_eq!(group.rule_count(), 2);
2048 }
2049
2050 #[test]
2053 fn lint_aggregator_collects() {
2054 let mut agg = LintAggregator::new();
2055 agg.add(mk_diag("a", Severity::Warning));
2056 agg.add(mk_diag("b", Severity::Error));
2057 agg.add_all(vec![mk_diag("c", Severity::Info)]);
2058 assert_eq!(agg.count(), 3);
2059 assert_eq!(agg.count_by_severity(Severity::Warning), 1);
2060 assert_eq!(agg.count_by_severity(Severity::Error), 1);
2061 }
2062
2063 #[test]
2064 fn lint_aggregator_into_diagnostics() {
2065 let mut agg = LintAggregator::new();
2066 agg.add(mk_diag("x", Severity::Info));
2067 let diags = agg.into_diagnostics();
2068 assert_eq!(diags.len(), 1);
2069 }
2070
2071 #[test]
2074 fn lint_event_log_basic() {
2075 let mut log = LintEventLog::new();
2076 let id = log.log(LintEventKind::RuleStarted, "checking naming_convention");
2077 assert_eq!(log.total(), 1);
2078 assert_eq!(log.events()[0].id, id);
2079 }
2080
2081 #[test]
2084 fn lint_diff_no_change() {
2085 let fingerprints = vec!["a".to_string(), "b".to_string()];
2086 let diff = LintDiff::compute(&fingerprints, &fingerprints);
2087 assert!(diff.is_empty());
2088 }
2089
2090 #[test]
2091 fn lint_diff_new_and_removed() {
2092 let before = vec!["a".to_string(), "b".to_string()];
2093 let after = vec!["b".to_string(), "c".to_string()];
2094 let diff = LintDiff::compute(&before, &after);
2095 assert!(!diff.is_empty());
2096 assert!(diff.added.contains(&"c".to_string()));
2097 assert!(diff.removed.contains(&"a".to_string()));
2098 }
2099}
2100
2101#[allow(dead_code)]
2107pub struct LintRuleMetadata {
2108 pub id: LintId,
2109 pub name: String,
2110 pub category: LintCategory,
2111 pub default_level: Severity,
2112 pub description: String,
2113 pub rationale: String,
2114 pub examples: Vec<LintExample>,
2115 pub since_version: String,
2116 pub deprecated: bool,
2117}
2118
2119#[allow(dead_code)]
2121pub struct LintExample {
2122 pub title: String,
2123 pub bad: String,
2124 pub good: String,
2125}
2126
2127impl LintRuleMetadata {
2128 #[allow(dead_code)]
2129 pub fn new(id: &str, name: &str, category: LintCategory, default_level: Severity) -> Self {
2130 Self {
2131 id: LintId::new(id),
2132 name: name.to_string(),
2133 category,
2134 default_level,
2135 description: String::new(),
2136 rationale: String::new(),
2137 examples: Vec::new(),
2138 since_version: "0.1.1".to_string(),
2139 deprecated: false,
2140 }
2141 }
2142
2143 #[allow(dead_code)]
2144 pub fn with_description(mut self, desc: &str) -> Self {
2145 self.description = desc.to_string();
2146 self
2147 }
2148
2149 #[allow(dead_code)]
2150 pub fn with_rationale(mut self, rationale: &str) -> Self {
2151 self.rationale = rationale.to_string();
2152 self
2153 }
2154
2155 #[allow(dead_code)]
2156 pub fn with_example(mut self, title: &str, bad: &str, good: &str) -> Self {
2157 self.examples.push(LintExample {
2158 title: title.to_string(),
2159 bad: bad.to_string(),
2160 good: good.to_string(),
2161 });
2162 self
2163 }
2164
2165 #[allow(dead_code)]
2166 pub fn mark_deprecated(mut self) -> Self {
2167 self.deprecated = true;
2168 self
2169 }
2170}
2171
2172#[allow(dead_code)]
2178pub struct LintPriorityQueue {
2179 items: Vec<(u8, LintDiagnostic)>,
2180}
2181
2182impl LintPriorityQueue {
2183 #[allow(dead_code)]
2184 pub fn new() -> Self {
2185 Self { items: Vec::new() }
2186 }
2187
2188 #[allow(dead_code)]
2189 fn severity_to_priority(s: Severity) -> u8 {
2190 match s {
2191 Severity::Error => 4,
2192 Severity::Warning => 3,
2193 Severity::Hint => 2,
2194 Severity::Info => 1,
2195 }
2196 }
2197
2198 #[allow(dead_code)]
2200 pub fn push(&mut self, diag: LintDiagnostic) {
2201 let priority = Self::severity_to_priority(diag.severity);
2202 self.items.push((priority, diag));
2203 self.items.sort_by(|a, b| b.0.cmp(&a.0));
2205 }
2206
2207 #[allow(dead_code)]
2209 pub fn pop(&mut self) -> Option<LintDiagnostic> {
2210 if self.items.is_empty() {
2211 None
2212 } else {
2213 Some(self.items.remove(0).1)
2214 }
2215 }
2216
2217 #[allow(dead_code)]
2218 pub fn len(&self) -> usize {
2219 self.items.len()
2220 }
2221
2222 #[allow(dead_code)]
2223 pub fn is_empty(&self) -> bool {
2224 self.items.is_empty()
2225 }
2226}
2227
2228impl Default for LintPriorityQueue {
2229 fn default() -> Self {
2230 Self::new()
2231 }
2232}
2233
2234#[allow(dead_code)]
2240pub struct LintBudget {
2241 pub max_total: usize,
2242 pub max_per_file: usize,
2243 total_used: usize,
2244 per_file_used: std::collections::HashMap<String, usize>,
2245}
2246
2247impl LintBudget {
2248 #[allow(dead_code)]
2249 pub fn new(max_total: usize, max_per_file: usize) -> Self {
2250 Self {
2251 max_total,
2252 max_per_file,
2253 total_used: 0,
2254 per_file_used: std::collections::HashMap::new(),
2255 }
2256 }
2257
2258 #[allow(dead_code)]
2260 pub fn try_spend(&mut self, file: &str) -> bool {
2261 if self.total_used >= self.max_total {
2262 return false;
2263 }
2264 let per_file = self.per_file_used.entry(file.to_string()).or_insert(0);
2265 if *per_file >= self.max_per_file {
2266 return false;
2267 }
2268 *per_file += 1;
2269 self.total_used += 1;
2270 true
2271 }
2272
2273 #[allow(dead_code)]
2274 pub fn remaining_total(&self) -> usize {
2275 self.max_total.saturating_sub(self.total_used)
2276 }
2277}
2278
2279#[allow(dead_code)]
2285pub struct LintCooldown {
2286 pub window: usize,
2287 seen: std::collections::HashMap<String, usize>,
2288 current_tick: usize,
2289}
2290
2291impl LintCooldown {
2292 #[allow(dead_code)]
2293 pub fn new(window: usize) -> Self {
2294 Self {
2295 window,
2296 seen: std::collections::HashMap::new(),
2297 current_tick: 0,
2298 }
2299 }
2300
2301 #[allow(dead_code)]
2303 pub fn tick(&mut self) {
2304 self.current_tick += 1;
2305 }
2306
2307 #[allow(dead_code)]
2309 pub fn should_emit(&mut self, fingerprint: &str) -> bool {
2310 match self.seen.get(fingerprint).copied() {
2311 None => {
2312 self.seen.insert(fingerprint.to_string(), self.current_tick);
2313 true
2314 }
2315 Some(last) if self.current_tick.saturating_sub(last) >= self.window => {
2316 self.seen.insert(fingerprint.to_string(), self.current_tick);
2317 true
2318 }
2319 _ => false,
2320 }
2321 }
2322}
2323
2324#[cfg(test)]
2329mod lib_final_tests {
2330 use super::*;
2331
2332 fn mk_diag(id: &str, severity: Severity) -> LintDiagnostic {
2333 LintDiagnostic::new(
2334 LintId::new(id),
2335 severity,
2336 "test",
2337 framework::SourceRange::new(0, 0),
2338 )
2339 }
2340
2341 #[test]
2344 fn lint_rule_metadata_basic() {
2345 let meta = LintRuleMetadata::new(
2346 "unused_import",
2347 "Unused Import",
2348 LintCategory::Style,
2349 Severity::Warning,
2350 )
2351 .with_description("Detects unused imports.")
2352 .with_rationale("Unused imports add noise.")
2353 .with_example("simple", "import Unused", "-- no import");
2354 assert_eq!(meta.id.as_str().to_string(), "unused_import");
2355 assert_eq!(meta.examples.len(), 1);
2356 assert!(!meta.deprecated);
2357 }
2358
2359 #[test]
2360 fn lint_rule_metadata_deprecated() {
2361 let meta =
2362 LintRuleMetadata::new("old_lint", "Old Lint", LintCategory::Style, Severity::Info)
2363 .mark_deprecated();
2364 assert!(meta.deprecated);
2365 }
2366
2367 #[test]
2370 fn lint_priority_queue_orders_by_severity() {
2371 let mut pq = LintPriorityQueue::new();
2372 pq.push(mk_diag("info_lint", Severity::Info));
2373 pq.push(mk_diag("error_lint", Severity::Error));
2374 pq.push(mk_diag("warning_lint", Severity::Warning));
2375 let first = pq.pop().expect("queue should not be empty");
2377 assert_eq!(first.severity, Severity::Error);
2378 let second = pq.pop().expect("queue should not be empty");
2379 assert_eq!(second.severity, Severity::Warning);
2380 }
2381
2382 #[test]
2383 fn lint_priority_queue_empty() {
2384 let mut pq = LintPriorityQueue::new();
2385 assert!(pq.is_empty());
2386 assert!(pq.pop().is_none());
2387 }
2388
2389 #[test]
2392 fn lint_budget_total_limit() {
2393 let mut budget = LintBudget::new(2, 10);
2394 assert!(budget.try_spend("a.ox"));
2395 assert!(budget.try_spend("b.ox"));
2396 assert!(!budget.try_spend("c.ox")); assert_eq!(budget.remaining_total(), 0);
2398 }
2399
2400 #[test]
2401 fn lint_budget_per_file_limit() {
2402 let mut budget = LintBudget::new(100, 2);
2403 assert!(budget.try_spend("a.ox"));
2404 assert!(budget.try_spend("a.ox"));
2405 assert!(!budget.try_spend("a.ox")); }
2407
2408 #[test]
2411 fn lint_cooldown_emits_once_then_suppresses() {
2412 let mut cd = LintCooldown::new(3);
2413 assert!(cd.should_emit("lint:a.ox:1"));
2414 assert!(!cd.should_emit("lint:a.ox:1"));
2416 cd.tick();
2418 cd.tick();
2419 cd.tick();
2420 assert!(cd.should_emit("lint:a.ox:1"));
2421 }
2422
2423 #[test]
2424 fn lint_cooldown_different_fingerprints() {
2425 let mut cd = LintCooldown::new(5);
2426 assert!(cd.should_emit("fp1"));
2427 assert!(cd.should_emit("fp2")); }
2429}
2430
2431#[allow(dead_code)]
2437pub struct LintSessionContext {
2438 pub session_id: String,
2439 pub files_processed: usize,
2440 pub total_diagnostics: usize,
2441 pub elapsed_ms: u64,
2442}
2443
2444impl LintSessionContext {
2445 #[allow(dead_code)]
2446 pub fn new(session_id: &str) -> Self {
2447 Self {
2448 session_id: session_id.to_string(),
2449 files_processed: 0,
2450 total_diagnostics: 0,
2451 elapsed_ms: 0,
2452 }
2453 }
2454
2455 #[allow(dead_code)]
2456 pub fn record_file(&mut self, diagnostic_count: usize, elapsed_ms: u64) {
2457 self.files_processed += 1;
2458 self.total_diagnostics += diagnostic_count;
2459 self.elapsed_ms += elapsed_ms;
2460 }
2461
2462 #[allow(dead_code)]
2463 pub fn average_diagnostics_per_file(&self) -> f64 {
2464 if self.files_processed == 0 {
2465 return 0.0;
2466 }
2467 self.total_diagnostics as f64 / self.files_processed as f64
2468 }
2469}
2470
2471#[allow(dead_code)]
2477pub struct LintConfigValidator;
2478
2479impl LintConfigValidator {
2480 #[allow(dead_code)]
2483 pub fn validate(config: &LintConfig) -> Vec<String> {
2484 let mut errors = Vec::new();
2485 for id in config.enabled.iter() {
2487 if config.disabled.contains(id) {
2488 errors.push(format!(
2489 "Rule `{}` appears in both enabled and disabled lists",
2490 id.as_str().to_string()
2491 ));
2492 }
2493 }
2494 errors
2495 }
2496}
2497
2498#[cfg(test)]
2499mod lint_session_tests {
2500 use super::*;
2501
2502 #[test]
2503 fn lint_session_context_average() {
2504 let mut ctx = LintSessionContext::new("sess-1");
2505 ctx.record_file(10, 50);
2506 ctx.record_file(20, 100);
2507 assert_eq!(ctx.files_processed, 2);
2508 assert!((ctx.average_diagnostics_per_file() - 15.0).abs() < 1e-9);
2509 assert_eq!(ctx.elapsed_ms, 150);
2510 }
2511
2512 #[test]
2513 fn lint_config_builder_builds() {
2514 let config = LintConfigBuilder::new()
2515 .allow("unused_import")
2516 .deny("dead_code")
2517 .build();
2518 assert_eq!(config.enabled.len(), 1);
2519 assert_eq!(config.disabled.len(), 1);
2520 }
2521
2522 #[test]
2523 fn lint_config_validator_no_conflict() {
2524 let config = LintConfigBuilder::new()
2525 .allow("lint_a")
2526 .allow("lint_b")
2527 .build();
2528 let errors = LintConfigValidator::validate(&config);
2529 assert!(errors.is_empty());
2530 }
2531
2532 #[test]
2533 fn lint_config_validator_with_conflict() {
2534 let config = LintConfigBuilder::new()
2535 .allow("conflict_lint")
2536 .deny("conflict_lint")
2537 .build();
2538 let errors = LintConfigValidator::validate(&config);
2539 assert!(!errors.is_empty());
2540 assert!(errors[0].contains("conflict_lint"));
2541 }
2542}
2543
2544#[allow(dead_code)]
2550pub struct LintRunSummary {
2551 pub files_checked: usize,
2552 pub total_diagnostics: usize,
2553 pub errors: usize,
2554 pub warnings: usize,
2555 pub elapsed_ms: u64,
2556 pub fix_suggestions: usize,
2557}
2558
2559impl LintRunSummary {
2560 #[allow(dead_code)]
2561 pub fn new() -> Self {
2562 Self {
2563 files_checked: 0,
2564 total_diagnostics: 0,
2565 errors: 0,
2566 warnings: 0,
2567 elapsed_ms: 0,
2568 fix_suggestions: 0,
2569 }
2570 }
2571
2572 #[allow(dead_code)]
2574 pub fn is_success(&self) -> bool {
2575 self.errors == 0
2576 }
2577
2578 #[allow(dead_code)]
2580 pub fn throughput(&self) -> f64 {
2581 if self.elapsed_ms == 0 {
2582 return 0.0;
2583 }
2584 self.total_diagnostics as f64 / self.elapsed_ms as f64
2585 }
2586}
2587
2588impl Default for LintRunSummary {
2589 fn default() -> Self {
2590 Self::new()
2591 }
2592}
2593
2594#[cfg(test)]
2595mod lint_run_summary_tests {
2596 use super::*;
2597
2598 #[test]
2599 fn lint_run_summary_is_success() {
2600 let mut s = LintRunSummary::new();
2601 assert!(s.is_success());
2602 s.errors = 1;
2603 assert!(!s.is_success());
2604 }
2605
2606 #[test]
2607 fn lint_run_summary_throughput() {
2608 let s = LintRunSummary {
2609 total_diagnostics: 100,
2610 elapsed_ms: 50,
2611 ..LintRunSummary::new()
2612 };
2613 assert!((s.throughput() - 2.0).abs() < 1e-9);
2614 }
2615
2616 #[test]
2617 fn lint_run_summary_zero_elapsed() {
2618 let s = LintRunSummary::new();
2619 assert_eq!(s.throughput(), 0.0);
2620 }
2621}