oparry_validators/
typescript.rs1use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9#[derive(Debug, Clone)]
11pub struct TypeScriptConfig {
12 pub block_any: bool,
14 pub block_assertions: bool,
16 pub require_return_types: bool,
18 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
33pub struct TypeScriptValidator {
35 config: TypeScriptConfig,
36 any_regex: Regex,
37 type_assertion_regex: Regex,
38 non_null_regex: Regex,
39}
40
41impl TypeScriptValidator {
42 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 pub fn default_config() -> Self {
54 Self::new(TypeScriptConfig::default())
55 }
56
57 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 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}