Skip to main content

oparry_validators/
typescript.rs

1//! TypeScript validator - TypeScript strict mode and type safety
2
3use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9/// TypeScript validation configuration
10#[derive(Debug, Clone)]
11pub struct TypeScriptConfig {
12    /// Block 'any' type usage
13    pub block_any: bool,
14    /// Block type assertions (as)
15    pub block_assertions: bool,
16    /// Require explicit return types
17    pub require_return_types: bool,
18    /// Require strict null checks
19    pub require_strict_nulls: bool,
20}
21
22impl Default for TypeScriptConfig {
23    fn default() -> Self {
24        Self {
25            block_any: true,
26            block_assertions: false,
27            require_return_types: false,
28            require_strict_nulls: true,
29        }
30    }
31}
32
33/// TypeScript validator
34pub struct TypeScriptValidator {
35    config: TypeScriptConfig,
36    any_regex: Regex,
37    type_assertion_regex: Regex,
38    non_null_regex: Regex,
39}
40
41impl TypeScriptValidator {
42    /// Create new TypeScript validator
43    pub fn new(config: TypeScriptConfig) -> Self {
44        Self {
45            config,
46            any_regex: Regex::new(r":\s*any\b|<any>").unwrap(),
47            type_assertion_regex: Regex::new(r"\s+as\s+\w+").unwrap(),
48            non_null_regex: Regex::new(r"!\s*[,\);]").unwrap(),
49        }
50    }
51
52    /// Create with default config
53    pub fn default_config() -> Self {
54        Self::new(TypeScriptConfig::default())
55    }
56
57    /// Check for 'any' type usage
58    fn check_any_usage(&self, source: &str, file: &str) -> Vec<Issue> {
59        let mut issues = Vec::new();
60        if !self.config.block_any {
61            return issues;
62        }
63
64        for (idx, line) in source.lines().enumerate() {
65            let trimmed = line.trim();
66            if trimmed.starts_with("//") || trimmed.starts_with("/*") {
67                continue;
68            }
69
70            if self.any_regex.is_match(line) {
71                issues.push(Issue::warning(
72                    "ts-any-type",
73                    "'any' type detected - defeats type safety",
74                )
75                .with_file(file)
76                .with_line(idx + 1)
77                .with_suggestion("Use specific type or unknown with type guards"));
78            }
79        }
80        issues
81    }
82
83    /// Check for non-null assertions
84    fn check_non_null_assertions(&self, source: &str, file: &str) -> Vec<Issue> {
85        let mut issues = Vec::new();
86        if !self.config.require_strict_nulls {
87            return issues;
88        }
89
90        for (idx, line) in source.lines().enumerate() {
91            if self.non_null_regex.is_match(line) {
92                issues.push(Issue::warning(
93                    "ts-non-null-assertion",
94                    "Non-null assertion (!) detected",
95                )
96                .with_file(file)
97                .with_line(idx + 1)
98                .with_suggestion("Use optional chaining (?.) or nullish coalescing (??)"));
99            }
100        }
101        issues
102    }
103}
104
105impl Validator for TypeScriptValidator {
106    fn name(&self) -> &str {
107        "TypeScript"
108    }
109
110    fn supports(&self, language: Language) -> bool {
111        matches!(language, Language::TypeScript | Language::Tsx | Language::Jsx)
112    }
113
114    fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
115        let mut result = ValidationResult::new();
116        let source = code.source();
117        let file_str = file.to_string_lossy().to_string();
118
119        for issue in self.check_any_usage(source, &file_str) {
120            result.add_issue(issue);
121        }
122        for issue in self.check_non_null_assertions(source, &file_str) {
123            result.add_issue(issue);
124        }
125
126        Ok(result)
127    }
128
129    fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
130        let parsed = ParsedCode::Generic(source.to_string());
131        self.validate_parsed(&parsed, file)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_ts_validator_valid() {
141        let validator = TypeScriptValidator::default_config();
142        let code = r#"function greet(name: string): string { return name; }"#;
143        let result = validator.validate_raw(code, Path::new("greet.ts")).unwrap();
144        assert!(result.passed);
145    }
146
147    #[test]
148    fn test_ts_any_type() {
149        let validator = TypeScriptValidator::default_config();
150        let code = r#"function process(data: any): void { }"#;
151        let result = validator.validate_raw(code, Path::new("process.ts")).unwrap();
152        assert!(!result.passed || result.warning_count() >= 1);
153    }
154
155    #[test]
156    fn test_ts_validator_supports() {
157        let validator = TypeScriptValidator::default_config();
158        assert!(validator.supports(Language::TypeScript));
159        assert!(!validator.supports(Language::Rust));
160    }
161}