nu_lint/
rule.rs

1use crate::{
2    context::LintContext,
3    lint::{RuleViolation, Severity},
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RuleCategory {
8    /// Rules about identifier naming conventions (`snake_case`, kebab-case,
9    /// etc.)
10    Naming,
11    /// Code layout and whitespace formatting rules
12    Formatting,
13    /// Nushell-specific best practices and preferred patterns
14    Idioms,
15    /// Error management and safety patterns
16    ErrorHandling,
17    /// General code cleanliness and maintainability
18    CodeQuality,
19    /// Documentation requirements and standards
20    Documentation,
21    /// Type annotations and type safety
22    TypeSafety,
23    /// Performance optimizations and efficient patterns
24    Performance,
25}
26
27impl RuleCategory {
28    /// Get the string representation of this category
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            RuleCategory::Naming => "naming",
33            RuleCategory::Formatting => "formatting",
34            RuleCategory::Idioms => "idioms",
35            RuleCategory::ErrorHandling => "error-handling",
36            RuleCategory::CodeQuality => "code-quality",
37            RuleCategory::Documentation => "documentation",
38            RuleCategory::TypeSafety => "type-safety",
39            RuleCategory::Performance => "performance",
40        }
41    }
42}
43
44impl std::fmt::Display for RuleCategory {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.write_str(self.as_str())
47    }
48}
49
50/// A concrete rule struct that wraps the check function
51pub struct Rule {
52    pub id: &'static str,
53    pub category: RuleCategory,
54    pub severity: Severity,
55    pub description: &'static str,
56    pub check: fn(&LintContext) -> Vec<RuleViolation>,
57}
58
59impl Rule {
60    /// Create a new rule
61    pub const fn new(
62        id: &'static str,
63        category: RuleCategory,
64        severity: Severity,
65        description: &'static str,
66        check: fn(&LintContext) -> Vec<RuleViolation>,
67    ) -> Self {
68        Self {
69            id,
70            category,
71            severity,
72            description,
73            check,
74        }
75    }
76
77    /// Run the rule's check function on the given context
78    #[must_use]
79    pub fn check(&self, context: &LintContext) -> Vec<RuleViolation> {
80        (self.check)(context)
81    }
82
83    #[cfg(test)]
84    #[allow(clippy::missing_panics_doc)]
85    #[track_caller]
86    /// Test helper: assert that the rule finds violations in the given code
87    pub fn assert_detects(&self, code: &str) {
88        let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
89        assert!(
90            !violations.is_empty(),
91            "Expected rule '{}' to detect violations in code, but found none",
92            self.id
93        );
94    }
95
96    #[cfg(test)]
97    #[allow(clippy::missing_panics_doc)]
98    #[track_caller]
99    /// Test helper: assert that the rule finds no violations in the given code
100    pub fn assert_ignores(&self, code: &str) {
101        let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
102        assert!(
103            violations.is_empty(),
104            "Expected rule '{}' to ignore code, but found {} violations",
105            self.id,
106            violations.len()
107        );
108    }
109
110    #[cfg(test)]
111    #[allow(clippy::missing_panics_doc)]
112    #[track_caller]
113    /// Test helper: assert that the rule finds at least the expected number of
114    /// violations
115    pub fn assert_violation_count(&self, code: &str, expected_min: usize) {
116        let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
117        assert!(
118            violations.len() >= expected_min,
119            "Expected rule '{}' to find at least {} violations, but found {}",
120            self.id,
121            expected_min,
122            violations.len()
123        );
124    }
125
126    #[cfg(test)]
127    #[allow(clippy::missing_panics_doc)]
128    #[track_caller]
129    /// Test helper: assert that the rule finds exactly the expected number of
130    /// violations
131    pub fn assert_violation_count_exact(&self, code: &str, expected: usize) {
132        let violations = LintContext::test_with_parsed_source(code, |context| self.check(&context));
133        assert_eq!(
134            violations.len(),
135            expected,
136            "Expected rule '{}' to find exactly {} violation(s), but found {}",
137            self.id,
138            expected,
139            violations.len()
140        );
141    }
142}