1#![allow(clippy::collapsible_if)]
4
5#![cfg_attr(
34 test,
35 allow(
36 clippy::field_reassign_with_default,
37 clippy::len_zero,
38 clippy::useless_vec
39 )
40)]
41
42rust_i18n::i18n!("locales", fallback = "en");
43
44pub mod authoring;
48pub mod config;
52pub mod diagnostics;
56#[cfg(feature = "filesystem")]
60pub mod eval;
61pub mod file_types;
65mod file_utils;
66pub mod fixes;
70pub mod fs;
74pub mod i18n;
78pub(crate) mod parsers;
82mod pipeline;
83mod regex_util;
84mod registry;
85mod rules;
86mod schemas;
87pub(crate) mod span_utils;
88pub mod validation;
92
93pub use config::{ConfigWarning, FilesConfig, LintConfig, generate_schema};
94pub use diagnostics::{
95 ConfigError, CoreError, Diagnostic, DiagnosticLevel, FileError, Fix, FixConfidenceTier,
96 LintError, LintResult, RuleMetadata, ValidationError, ValidationOutcome,
97};
98pub use file_types::{FileType, detect_file_type};
99pub use file_types::{FileTypeDetector, FileTypeDetectorChain};
100pub use fixes::{
101 FixApplyMode, FixApplyOptions, FixResult, apply_fixes, apply_fixes_with_fs,
102 apply_fixes_with_fs_options, apply_fixes_with_options,
103};
104pub use fs::{FileSystem, MockFileSystem, RealFileSystem};
105pub use pipeline::{ValidationResult, resolve_file_type, validate_content};
106#[cfg(feature = "filesystem")]
107pub use pipeline::{
108 validate_file, validate_file_with_registry, validate_project, validate_project_rules,
109 validate_project_with_registry,
110};
111pub use registry::{
112 ValidatorFactory, ValidatorProvider, ValidatorRegistry, ValidatorRegistryBuilder,
113};
114pub use rules::{Validator, ValidatorMetadata};
115
116pub use parsers::frontmatter::normalize_line_endings;
122
123#[doc(hidden)]
129#[cfg(any(test, feature = "__internal"))]
130pub mod __internal {
131 pub use crate::parsers::ImportCache;
132 pub use crate::parsers::frontmatter::{FrontmatterParts, split_frontmatter};
133 pub use crate::parsers::json::parse_json_config;
134 pub use crate::parsers::markdown::Import;
135 pub use crate::parsers::markdown::{
136 MAX_REGEX_INPUT_SIZE, MarkdownLink, XmlTag, check_xml_balance,
137 check_xml_balance_with_content_end, extract_imports, extract_markdown_links,
138 extract_xml_tags, sanitize_for_pulldown_cmark,
139 };
140 pub use crate::schemas::cross_platform::is_instruction_file;
141}
142
143#[cfg(test)]
144mod i18n_tests {
145 use rust_i18n::t;
146 use std::sync::Mutex;
147
148 static LOCALE_MUTEX: Mutex<()> = Mutex::new(());
150
151 #[test]
153 fn test_english_translations_load() {
154 let _lock = LOCALE_MUTEX.lock().unwrap();
155 rust_i18n::set_locale("en");
156
157 let xml_msg = t!("rules.xml_001.message", tag = "test");
159 assert!(
160 xml_msg.contains("Unclosed XML tag"),
161 "Expected English translation, got: {}",
162 xml_msg
163 );
164
165 let cli_validating = t!("cli.validating");
166 assert_eq!(cli_validating, "Validating:");
167
168 let lsp_label = t!("lsp.suggestion_label");
169 assert_eq!(lsp_label, "Suggestion:");
170 }
171
172 #[test]
174 fn test_spanish_translations_load() {
175 let _lock = LOCALE_MUTEX.lock().unwrap();
176 rust_i18n::set_locale("es");
177
178 let xml_msg = t!("rules.xml_001.message", tag = "test");
179 assert!(
180 xml_msg.contains("Etiqueta XML sin cerrar"),
181 "Expected Spanish translation, got: {}",
182 xml_msg
183 );
184
185 let cli_validating = t!("cli.validating");
186 assert_eq!(cli_validating, "Validando:");
187
188 rust_i18n::set_locale("en");
189 }
190
191 #[test]
193 fn test_chinese_translations_load() {
194 let _lock = LOCALE_MUTEX.lock().unwrap();
195 rust_i18n::set_locale("zh-CN");
196
197 let xml_msg = t!("rules.xml_001.message", tag = "test");
198 assert!(
199 xml_msg.contains("\u{672A}\u{5173}\u{95ED}"),
200 "Expected Chinese translation, got: {}",
201 xml_msg
202 );
203
204 let cli_validating = t!("cli.validating");
205 assert!(
206 cli_validating.contains("\u{6B63}\u{5728}\u{9A8C}\u{8BC1}"),
207 "Expected Chinese translation, got: {}",
208 cli_validating
209 );
210
211 rust_i18n::set_locale("en");
212 }
213
214 #[test]
216 fn test_fallback_to_english() {
217 let _lock = LOCALE_MUTEX.lock().unwrap();
218 rust_i18n::set_locale("fr"); let msg = t!("cli.validating");
221 assert_eq!(
222 msg, "Validating:",
223 "Should fall back to English, got: {}",
224 msg
225 );
226
227 rust_i18n::set_locale("en");
228 }
229
230 #[test]
232 fn test_parameterized_translations() {
233 let _lock = LOCALE_MUTEX.lock().unwrap();
234 rust_i18n::set_locale("en");
235
236 let msg = t!("rules.as_004.message", name = "TestName");
237 assert!(
238 msg.contains("TestName"),
239 "Parameter should be interpolated, got: {}",
240 msg
241 );
242 assert!(
243 msg.contains("must be 1-64 characters"),
244 "Message template should be filled, got: {}",
245 msg
246 );
247 }
248
249 #[test]
251 fn test_available_locales() {
252 let locales = rust_i18n::available_locales!();
253 assert!(
254 locales.contains(&"en"),
255 "English locale must be available, found: {:?}",
256 locales
257 );
258 assert!(
259 locales.contains(&"es"),
260 "Spanish locale must be available, found: {:?}",
261 locales
262 );
263 assert!(
264 locales.contains(&"zh-CN"),
265 "Chinese locale must be available, found: {:?}",
266 locales
267 );
268 }
269
270 #[test]
272 fn test_rule_ids_not_translated() {
273 use super::*;
274 use std::path::Path;
275
276 let _lock = LOCALE_MUTEX.lock().unwrap();
277 rust_i18n::set_locale("es"); let config = config::LintConfig::default();
280 let content = "---\nname: test\n---\nSome content";
281 let path = Path::new("test/.claude/skills/test/SKILL.md");
282
283 let validator = rules::skill::SkillValidator;
284 let diagnostics = validator.validate(path, content, &config);
285
286 for diag in &diagnostics {
288 assert!(
289 diag.rule.is_ascii(),
290 "Rule ID should be ASCII: {}",
291 diag.rule
292 );
293 }
294
295 rust_i18n::set_locale("en");
296 }
297
298 #[test]
300 fn test_spanish_diagnostics() {
301 use super::*;
302 use std::path::Path;
303
304 let _lock = LOCALE_MUTEX.lock().unwrap();
305 rust_i18n::set_locale("es");
306
307 let config = config::LintConfig::default();
308 let content = "<unclosed>";
309 let path = Path::new("test/CLAUDE.md");
310
311 let validator = rules::xml::XmlValidator;
312 let diagnostics = validator.validate(path, content, &config);
313
314 assert!(!diagnostics.is_empty(), "Should produce diagnostics");
315 let xml_diag = diagnostics.iter().find(|d| d.rule == "XML-001").unwrap();
316 assert!(
317 xml_diag.message.contains("Etiqueta XML sin cerrar"),
318 "Message should be in Spanish, got: {}",
319 xml_diag.message
320 );
321
322 rust_i18n::set_locale("en");
323 }
324
325 #[test]
327 fn test_new_suggestion_keys_resolve() {
328 let _lock = LOCALE_MUTEX.lock().unwrap();
329 rust_i18n::set_locale("en");
330
331 macro_rules! assert_key_resolves {
333 ($key:expr) => {
334 let value = t!($key);
335 assert!(
336 !value.starts_with("rules."),
337 "Locale key '{}' should resolve to text, not raw key path: {}",
338 $key,
339 value
340 );
341 };
342 }
343 assert_key_resolves!("rules.as_016.suggestion");
344 assert_key_resolves!("rules.cc_hk_012.suggestion");
345 assert_key_resolves!("rules.mcp_007.suggestion");
346 assert_key_resolves!("rules.cc_pl_006.suggestion");
347 assert_key_resolves!("rules.cc_ag_007.parse_error_suggestion");
348 assert_key_resolves!("rules.cdx_000.suggestion");
349 assert_key_resolves!("rules.file_read_error_suggestion");
350 assert_key_resolves!("rules.xp_004_read_error_suggestion");
351
352 let cdx_msg = t!("rules.cdx_000.message", error = "test error");
354 assert!(
355 cdx_msg.contains("test error"),
356 "CDX-000 message should interpolate error param, got: {}",
357 cdx_msg
358 );
359
360 let file_msg = t!("rules.file_read_error", error = "permission denied");
362 assert!(
363 file_msg.contains("permission denied"),
364 "file_read_error should interpolate error param, got: {}",
365 file_msg
366 );
367 let xp_msg = t!("rules.xp_004_read_error", error = "not found");
368 assert!(
369 xp_msg.contains("not found"),
370 "xp_004_read_error should interpolate error param, got: {}",
371 xp_msg
372 );
373 }
374
375 #[test]
379 fn test_keys_resolve_to_text_not_raw_paths() {
380 let _lock = LOCALE_MUTEX.lock().unwrap();
381 rust_i18n::set_locale("en");
382
383 macro_rules! assert_not_raw_key {
385 ($key:expr) => {
386 let value = t!($key);
387 assert!(
388 (!value.starts_with("rules."))
389 && (!value.starts_with("cli."))
390 && (!value.starts_with("lsp."))
391 && (!value.starts_with("core.")),
392 "Key '{}' returned raw path instead of translated text: {}",
393 $key,
394 value
395 );
396 };
397 ($key:expr, $($param:ident = $val:expr),+) => {
398 let value = t!($key, $($param = $val),+);
399 assert!(
400 (!value.starts_with("rules."))
401 && (!value.starts_with("cli."))
402 && (!value.starts_with("lsp."))
403 && (!value.starts_with("core.")),
404 "Key '{}' returned raw path instead of translated text: {}",
405 $key,
406 value
407 );
408 };
409 }
410
411 assert_not_raw_key!("rules.as_001.message");
413 assert_not_raw_key!("rules.as_004.message", name = "test");
414 assert_not_raw_key!("rules.cc_ag_009.message", tool = "x", known = "y");
415 assert_not_raw_key!("rules.xml_001.message", tag = "div");
416 assert_not_raw_key!("rules.cc_hk_001.message");
417 assert_not_raw_key!("rules.pe_003.message");
418 assert_not_raw_key!("rules.cc_mem_009.message");
419
420 assert_not_raw_key!("rules.as_001.suggestion");
422 assert_not_raw_key!("rules.as_004.suggestion");
423 assert_not_raw_key!("rules.cc_ag_009.suggestion");
424
425 assert_not_raw_key!("rules.cc_hk_010.assumption");
427 assert_not_raw_key!("rules.mcp_008.assumption");
428
429 assert_not_raw_key!("cli.validating");
431 assert_not_raw_key!("cli.no_issues_found");
432 assert_not_raw_key!(
433 "cli.found_errors_warnings",
434 errors = "1",
435 error_word = "error",
436 warnings = "0",
437 warning_word = "warnings"
438 );
439
440 assert_not_raw_key!("lsp.suggestion_label");
442
443 assert_not_raw_key!("core.error.file_read", path = "/tmp/test");
445 }
446}