1use anyhow::Result;
2use ignore::WalkBuilder;
3use rayon::prelude::*;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::sync::Arc;
8
9pub mod analysis;
10pub mod config;
11pub mod directives;
12pub mod formatter;
13pub mod rule_pool;
14pub mod rules;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum OutputFormat {
18 Standard,
19 Colored,
20}
21
22#[derive(Debug, Clone)]
23pub struct ProcessingOptions {
24 pub recursive: bool,
25 pub verbose: bool,
26 pub output_format: OutputFormat,
27 pub show_progress: bool,
28}
29
30impl Default for ProcessingOptions {
31 fn default() -> Self {
32 Self {
33 recursive: false,
34 verbose: false,
35 output_format: OutputFormat::Colored,
36 show_progress: true,
37 }
38 }
39}
40
41pub fn detect_output_format(format_str: &str) -> OutputFormat {
42 match format_str {
43 "standard" => OutputFormat::Standard,
44 "colored" => OutputFormat::Colored,
45 "auto" | _ => {
46 if std::env::var("NO_COLOR").is_ok() {
47 return OutputFormat::Standard;
48 }
49
50 if !atty::is(atty::Stream::Stdout) {
51 return OutputFormat::Standard;
52 }
53
54 OutputFormat::Colored
55 }
56 }
57}
58
59pub struct FileProcessor {
60 options: ProcessingOptions,
61 rules: Arc<Vec<Box<dyn rules::Rule>>>,
62 fix_mode: bool,
63 config: Option<Arc<config::Config>>,
64 formatter: Box<dyn formatter::Formatter>,
65}
66
67impl FileProcessor {
68 fn should_run_rule_for_file(
69 rule_id: &str,
70 file_path: &str,
71 config: &Option<Arc<config::Config>>,
72 ) -> bool {
73 if let Some(config) = config {
74 if let Some(rule_config) = config.get_rule_config(rule_id) {
75 if let Some(ignore_val) = rule_config.other.get("ignore") {
76 if let Some(ignore_str) = ignore_val.as_str() {
77 let patterns: Vec<&str> = ignore_str
78 .lines()
79 .map(|line| line.trim())
80 .filter(|line| !line.is_empty())
81 .collect();
82
83 for pattern in patterns {
84 if file_path.contains(pattern) {
85 return false;
86 }
87 }
88 }
89 }
90 }
91 }
92 true
93 }
94
95 pub fn new(options: ProcessingOptions) -> Self {
96 let formatter = formatter::create_formatter(options.output_format);
97 Self {
98 options,
99 rules: Arc::new(Vec::new()),
100 fix_mode: false,
101 config: None,
102 formatter,
103 }
104 }
105
106 pub fn with_default_rules(options: ProcessingOptions) -> Self {
107 let factory = rules::factory::RuleFactory::new();
108 let config = config::Config::default();
109 let enabled_rules = config.get_enabled_rules();
110 let mut rules = factory.create_rules_by_ids_with_config(&enabled_rules, &config);
111 let config_arc = Arc::new(config);
112
113 for rule in &mut rules {
114 let severity = config_arc.get_rule_severity(rule.rule_id());
115 rule.set_severity(severity);
116 }
117
118 let formatter = formatter::create_formatter(options.output_format);
119 Self {
120 options,
121 rules: Arc::new(rules),
122 fix_mode: false,
123 config: Some(config_arc),
124 formatter,
125 }
126 }
127
128 pub fn with_fix_mode(options: ProcessingOptions) -> Self {
129 let mut processor = Self::with_default_rules(options);
130 processor.fix_mode = true;
131 processor
132 }
133
134 pub fn with_config(options: ProcessingOptions, config: config::Config) -> Self {
135 let factory = rules::factory::RuleFactory::new();
136 let enabled_rules = config.get_enabled_rules();
137
138 let config_arc = Arc::new(config);
139 let mut rules = factory.create_rules_by_ids_with_config(&enabled_rules, &config_arc);
140
141 for rule in &mut rules {
142 let severity = config_arc.get_rule_severity(rule.rule_id());
143 rule.set_severity(severity);
144 }
145
146 let formatter = formatter::create_formatter(options.output_format);
147 Self {
148 options,
149 rules: Arc::new(rules),
150 fix_mode: false,
151 config: Some(config_arc),
152 formatter,
153 }
154 }
155
156 pub fn with_config_and_fix_mode(options: ProcessingOptions, config: config::Config) -> Self {
157 let mut processor = Self::with_config(options, config);
158 processor.fix_mode = true;
159 processor
160 }
161
162 pub fn add_rule(&mut self, rule: Box<dyn rules::Rule>) {
163 Arc::get_mut(&mut self.rules)
164 .expect("Cannot add rule when rules are shared")
165 .push(rule);
166 }
167
168 pub fn process_file<P: AsRef<Path>>(&self, file_path: P) -> Result<LintResult> {
169 let path = file_path.as_ref();
170
171 if let Some(config) = &self.config {
172 let cwd = std::env::current_dir().ok();
173 let config_dir = cwd.as_deref();
174 if config.is_file_ignored(path, config_dir) {
175 return Ok(LintResult {
176 file: self.get_relative_path(path),
177 issues: vec![],
178 });
179 }
180 }
181
182 let relative_path = self.get_relative_path(path);
183
184 if self.options.verbose {
185 println!("Processing file: {}", relative_path);
186 }
187
188 let content = std::fs::read_to_string(path)?;
189
190 if self.fix_mode {
191 self.process_file_with_fixes(path, &content, &relative_path)
192 } else {
193 self.process_file_check_only(&content, &relative_path)
194 }
195 }
196
197 fn check_file_content(
198 rules: &[Box<dyn rules::Rule>],
199 content: &str,
200 relative_path: &str,
201 config: &Option<Arc<config::Config>>,
202 ) -> LintResult {
203 let all_rule_ids: std::collections::HashSet<String> =
204 rules.iter().map(|r| r.rule_id().to_string()).collect();
205 let mut directive_state = directives::DirectiveState::new(all_rule_ids);
206 directive_state.parse_from_content(content);
207
208 let analysis = analysis::ContentAnalysis::analyze(content);
209
210 let estimated_issues = rules.len() * 3;
211 let mut all_issues = Vec::with_capacity(estimated_issues);
212 for rule in rules {
213 let rule_id = rule.rule_id();
214 if !Self::should_run_rule_for_file(rule_id, relative_path, config) {
215 continue;
216 }
217 let issues = rule.check_with_analysis(content, relative_path, &analysis);
218 for issue in issues {
219 all_issues.push((issue, rule_id.to_string()));
220 }
221 }
222
223 let filtered_issues = directive_state.filter_issues(all_issues);
224 let mut sorted_issues = filtered_issues;
225 sorted_issues.sort_by(|a, b| a.0.line.cmp(&b.0.line).then(a.0.column.cmp(&b.0.column)));
226
227 LintResult {
228 file: relative_path.to_string(),
229 issues: sorted_issues,
230 }
231 }
232
233 fn process_file_check_only(&self, content: &str, relative_path: &str) -> Result<LintResult> {
234 let result =
235 Self::check_file_content(self.rules.as_slice(), content, relative_path, &self.config);
236
237 if result.issues.is_empty() {
238 if self.options.verbose {
239 println!("✓ No issues found in {}", result.file);
240 }
241 } else {
242 println!("{}", self.formatter.format_filename(&result.file));
243
244 let mut output = String::with_capacity(result.issues.len() * 120);
245
246 for (issue, rule_name) in &result.issues {
247 let formatted = self.formatter.format_issue(issue, rule_name);
248 output.push_str(&formatted);
249 }
250
251 print!("{}", output);
252 }
253
254 Ok(result)
255 }
256
257 fn apply_fixes_and_check(
258 rules: &[Box<dyn rules::Rule>],
259 content: &str,
260 relative_path: &str,
261 config: &Option<Arc<config::Config>>,
262 ) -> (String, usize, usize, Vec<(LintIssue, String)>) {
263 let registry = rules::registry::RuleRegistry::new();
264 let mut fixed_content = String::with_capacity(content.len());
265 fixed_content.push_str(content);
266 let mut total_fixes = 0;
267 let mut fixable_issues = 0;
268
269 let mut fixable_rules: Vec<(usize, usize)> = rules
270 .iter()
271 .enumerate()
272 .filter_map(|(idx, rule)| {
273 let rule_id = rule.rule_id();
274 if !Self::should_run_rule_for_file(rule_id, relative_path, config) {
275 return None;
276 }
277 if !rule.can_fix() {
278 return None;
279 }
280 let metadata = registry.get_rule_metadata(rule_id)?;
281 let order = metadata.fix_order.unwrap_or(999);
282 Some((idx, order))
283 })
284 .collect();
285
286 fixable_rules.sort_by_key(|(_, order)| *order);
287
288 for (idx, _) in fixable_rules {
289 let rule = &rules[idx];
290 let fix_result = rule.fix(&fixed_content, relative_path);
291 if fix_result.changed || fix_result.fixes_applied > 0 {
292 fixed_content = fix_result.content;
293 total_fixes += fix_result.fixes_applied;
294 fixable_issues += fix_result.fixes_applied;
295 }
296 }
297
298 let analysis = analysis::ContentAnalysis::analyze(&fixed_content);
299 let estimated_issues = rules.len() * 3;
300 let mut all_issues = Vec::with_capacity(estimated_issues);
301 for rule in rules {
302 let rule_id = rule.rule_id();
303 if !Self::should_run_rule_for_file(rule_id, relative_path, config) {
304 continue;
305 }
306 let issues = rule.check_with_analysis(&fixed_content, relative_path, &analysis);
307 for issue in issues {
308 all_issues.push((issue, rule_id.to_string()));
309 }
310 }
311
312 all_issues.sort_by(|a, b| a.0.line.cmp(&b.0.line).then(a.0.column.cmp(&b.0.column)));
313
314 (fixed_content, total_fixes, fixable_issues, all_issues)
315 }
316
317 fn process_file_with_fixes<P: AsRef<Path>>(
318 &self,
319 path: P,
320 content: &str,
321 relative_path: &str,
322 ) -> Result<LintResult> {
323 let (fixed_content, total_fixes, fixable_issues, all_issues) = Self::apply_fixes_and_check(
324 self.rules.as_slice(),
325 content,
326 relative_path,
327 &self.config,
328 );
329
330 let _non_fixable_issues = all_issues.len();
331
332 if fixed_content != content {
333 std::fs::write(path, &fixed_content)?;
334 if total_fixes > 0 {
335 println!(
336 "Fixed {} issues in {} ({} fixable, {} remaining)",
337 total_fixes, relative_path, fixable_issues, _non_fixable_issues
338 );
339 }
340 } else if _non_fixable_issues > 0 {
341 println!(
342 "Found {} non-fixable issues in {}:",
343 _non_fixable_issues, relative_path
344 );
345 for (issue, _rule_name) in &all_issues {
346 println!(
347 " {}:{}: {}: {}",
348 issue.line,
349 issue.column,
350 format!("{:?}", issue.severity).to_lowercase(),
351 issue.message
352 );
353 }
354 } else {
355 if self.options.verbose {
356 println!("✓ No issues found in {}", relative_path);
357 }
358 }
359
360 Ok(LintResult {
361 file: relative_path.to_string(),
362 issues: all_issues,
363 })
364 }
365
366 pub fn process_directory<P: AsRef<Path>>(&self, dir_path: P) -> Result<usize> {
367 let path = dir_path.as_ref();
368
369 if !path.is_dir() {
370 return Err(anyhow::anyhow!(
371 "Path is not a directory: {}",
372 path.display()
373 ));
374 }
375
376 if self.options.verbose {
377 println!("Processing directory: {}", path.display());
378 }
379
380 let mut yaml_files = Vec::with_capacity(100);
381
382 let walker = WalkBuilder::new(path).follow_links(false).build();
383
384 for result in walker {
385 let entry = result?;
386 let file_path = entry.path();
387 if file_path.is_file() && self.is_yaml_file(file_path) {
388 if let Some(config) = &self.config {
389 let config_dir = Some(path);
390 if config.is_file_ignored(file_path, config_dir) {
391 continue;
392 }
393 }
394 yaml_files.push(file_path.to_path_buf());
395 }
396 }
397
398 if yaml_files.is_empty() {
399 if self.options.verbose {
400 println!("No YAML files found in directory");
401 }
402 return Ok(0);
403 }
404
405 if self.options.verbose {
406 println!(
407 "Found {} YAML files, processing in parallel...",
408 yaml_files.len()
409 );
410 }
411
412 let options = self.options.clone();
413 let fix_mode = self.fix_mode;
414 let shared_rules = self.rules.clone();
415
416 let results = if options.show_progress {
417 let total = yaml_files.len();
418 let counter = Arc::new(AtomicUsize::new(0));
419 Self::process_files_list(
420 &yaml_files,
421 shared_rules,
422 &options,
423 fix_mode,
424 &self.config,
425 Some(counter),
426 Some(total),
427 )?
428 } else {
429 Self::process_files_list(
430 &yaml_files,
431 shared_rules,
432 &options,
433 fix_mode,
434 &self.config,
435 None,
436 None,
437 )?
438 };
439
440 let formatter = formatter::create_formatter(options.output_format);
441 let mut stdout = std::io::stdout().lock();
442 let mut total_issues = 0;
443 for result in &results {
444 if !result.issues.is_empty() {
445 total_issues += result.issues.len();
446 writeln!(stdout, "{}", formatter.format_filename(&result.file))?;
447
448 let mut output = String::with_capacity(result.issues.len() * 120);
449
450 for (issue, rule_name) in &result.issues {
451 let formatted = formatter.format_issue(issue, rule_name);
452 output.push_str(&formatted);
453 }
454
455 write!(stdout, "{}", output)?;
456 }
457 }
458
459 if self.options.verbose {
460 writeln!(stdout, "Successfully processed {} files", results.len())?;
461 }
462
463 if self.options.verbose {
464 writeln!(stdout, "Completed processing {} files", yaml_files.len())?;
465 }
466
467 Ok(total_issues)
468 }
469
470 fn is_yaml_file(&self, path: &Path) -> bool {
471 if let Some(ext) = path.extension() {
472 matches!(
473 ext.to_string_lossy().to_lowercase().as_str(),
474 "yaml" | "yml"
475 )
476 } else {
477 false
478 }
479 }
480
481 fn get_relative_path(&self, path: &Path) -> String {
482 Self::get_relative_path_static(path)
483 }
484
485 fn get_relative_path_static(path: &Path) -> String {
486 if let Ok(cwd) = std::env::current_dir() {
487 if let Ok(relative) = path.strip_prefix(&cwd) {
488 return relative.to_string_lossy().to_string();
489 }
490 }
491 path.to_string_lossy().to_string()
492 }
493
494 fn process_files_list(
495 files: &[PathBuf],
496 rules: Arc<Vec<Box<dyn rules::Rule>>>,
497 options: &ProcessingOptions,
498 fix_mode: bool,
499 config: &Option<Arc<config::Config>>,
500 counter: Option<Arc<AtomicUsize>>,
501 total: Option<usize>,
502 ) -> Result<Vec<LintResult>> {
503 if files.len() > 3 {
504 files
505 .par_iter()
506 .map(|file| {
507 Self::process_single_file(
508 rules.clone(),
509 file,
510 options,
511 fix_mode,
512 config,
513 counter.as_ref().map(Arc::clone),
514 total,
515 )
516 })
517 .collect()
518 } else {
519 files
520 .iter()
521 .map(|file| {
522 Self::process_single_file(
523 rules.clone(),
524 file,
525 options,
526 fix_mode,
527 config,
528 counter.as_ref().map(Arc::clone),
529 total,
530 )
531 })
532 .collect()
533 }
534 }
535
536 fn process_single_file(
537 rules: Arc<Vec<Box<dyn rules::Rule>>>,
538 file_path: &Path,
539 options: &ProcessingOptions,
540 fix_mode: bool,
541 config: &Option<Arc<config::Config>>,
542 counter: Option<Arc<AtomicUsize>>,
543 total: Option<usize>,
544 ) -> Result<LintResult> {
545 let relative_path = Self::get_relative_path_static(file_path);
546
547 if options.verbose {
548 eprintln!("Processing file: {}", relative_path);
549 }
550
551 let content = std::fs::read_to_string(file_path)?;
552
553 let result = if fix_mode {
554 Self::process_file_with_fixes_static(
555 &rules,
556 file_path,
557 &content,
558 &relative_path,
559 config,
560 )
561 } else {
562 Self::process_file_check_only_static(&rules, &content, &relative_path, config)
563 }?;
564
565 if let (Some(counter), Some(total)) = (counter, total) {
566 let count = counter.fetch_add(1, Ordering::Relaxed) + 1;
567 if count % 1000 == 0 || count == total {
568 let percent = (count * 100) / total;
569 eprintln!(
570 "[Progress] Processed {}/{} files ({}%)",
571 count, total, percent
572 );
573 }
574 }
575
576 Ok(result)
577 }
578
579 fn process_file_check_only_static(
580 rules: &[Box<dyn rules::Rule>],
581 content: &str,
582 relative_path: &str,
583 config: &Option<Arc<config::Config>>,
584 ) -> Result<LintResult> {
585 let result = Self::check_file_content(rules, content, relative_path, config);
586 Ok(result)
587 }
588
589 fn process_file_with_fixes_static(
590 rules: &[Box<dyn rules::Rule>],
591 path: &Path,
592 content: &str,
593 relative_path: &str,
594 config: &Option<Arc<config::Config>>,
595 ) -> Result<LintResult> {
596 let (fixed_content, total_fixes, fixable_issues, all_issues) =
597 Self::apply_fixes_and_check(rules, content, relative_path, config);
598
599 let _non_fixable_issues = all_issues.len();
600
601 if total_fixes > 0 {
602 std::fs::write(path, &fixed_content)?;
603 println!(
604 "Fixed {} issues in {} ({} fixable, {} remaining)",
605 total_fixes, relative_path, fixable_issues, _non_fixable_issues
606 );
607 } else if !all_issues.is_empty() {
608 println!(
609 "Found {} non-fixable issues in {}:",
610 _non_fixable_issues, relative_path
611 );
612 for (issue, rule_name) in &all_issues {
613 let level = match issue.severity {
614 crate::Severity::Error => "error",
615 crate::Severity::Warning => "warning",
616 crate::Severity::Info => "info",
617 };
618 println!(
619 " {}:{}:{}: {} {} ({})",
620 relative_path, issue.line, issue.column, level, issue.message, rule_name
621 );
622 }
623 }
624
625 Ok(LintResult {
626 file: relative_path.to_string(),
627 issues: all_issues,
628 })
629 }
630}
631
632pub fn load_config<P: AsRef<Path>>(path: P) -> Result<config::Config> {
633 let content = std::fs::read_to_string(path)?;
634
635 match parse_original_yamllint_format(&content) {
636 Ok(original_config) => return Ok(original_config),
637 Err(e) => {
638 if !e.to_string().contains("Not original yamllint format") {
639 return Err(e);
640 }
641 }
642 }
643
644 let config: config::Config = serde_yaml::from_str(&content)?;
645 Ok(config)
646}
647
648fn yaml_value_to_json(yaml_val: &serde_yaml::Value) -> serde_json::Value {
649 match yaml_val {
650 serde_yaml::Value::Null => serde_json::Value::Null,
651 serde_yaml::Value::Bool(b) => serde_json::Value::Bool(*b),
652 serde_yaml::Value::Number(n) => {
653 if let Some(u) = n.as_u64() {
654 serde_json::Value::Number(u.into())
655 } else if let Some(i) = n.as_i64() {
656 serde_json::Value::Number(i.into())
657 } else if let Some(f) = n.as_f64() {
658 serde_json::Number::from_f64(f)
659 .map(serde_json::Value::Number)
660 .unwrap_or(serde_json::Value::Null)
661 } else {
662 serde_json::Value::Null
663 }
664 }
665 serde_yaml::Value::String(s) => serde_json::Value::String(s.clone()),
666 serde_yaml::Value::Sequence(seq) => {
667 serde_json::Value::Array(seq.iter().map(yaml_value_to_json).collect())
668 }
669 serde_yaml::Value::Mapping(map) => serde_json::Value::Object(
670 map.iter()
671 .filter_map(|(k, v)| {
672 k.as_str()
673 .map(|key| (key.to_string(), yaml_value_to_json(v)))
674 })
675 .collect(),
676 ),
677 serde_yaml::Value::Tagged(_) => serde_json::Value::Null,
678 }
679}
680
681fn parse_original_yamllint_format(content: &str) -> Result<config::Config> {
682 use serde_yaml::Value;
683
684 let yaml_value: Value = serde_yaml::from_str(content)?;
685
686 let has_extends = yaml_value.get("extends").is_some();
687 let has_rules_simple_format = yaml_value
688 .get("rules")
689 .and_then(|r| r.as_mapping())
690 .map(|rules_map| {
691 rules_map
692 .values()
693 .any(|v| v.is_string() || (v.is_mapping() && v.get("level").is_some()))
694 })
695 .unwrap_or(false);
696
697 if has_extends {
698 return convert_original_yamllint_config(yaml_value);
699 }
700
701 if has_rules_simple_format {
702 if let Some(rules) = yaml_value.get("rules") {
703 if let Some(rules_map) = rules.as_mapping() {
704 let has_simple_values = rules_map
705 .values()
706 .any(|v| v.is_string() || (v.is_mapping() && v.get("level").is_some()));
707
708 if has_simple_values {
709 return convert_original_yamllint_config(yaml_value);
710 }
711 }
712 }
713 }
714
715 Err(anyhow::anyhow!("Not original yamllint format"))
716}
717
718fn convert_original_yamllint_config(yaml_value: serde_yaml::Value) -> Result<config::Config> {
719 let mut config = config::Config::new();
720
721 if let Some(ignore_val) = yaml_value.get("ignore") {
722 if let Some(ignore_str) = ignore_val.as_str() {
723 config.ignore = Some(ignore_str.to_string());
724 } else if let Some(ignore_seq) = ignore_val.as_sequence() {
725 let patterns: Vec<String> = ignore_seq
726 .iter()
727 .filter_map(|v| v.as_str().map(|s| s.to_string()))
728 .collect();
729 config.ignore = Some(patterns.join("\n"));
730 }
731 }
732
733 if let Some(ignore_from_file_val) = yaml_value.get("ignore-from-file") {
734 if let Some(ignore_file_str) = ignore_from_file_val.as_str() {
735 config.ignore_from_file = Some(ignore_file_str.to_string());
736 } else if let Some(ignore_file_seq) = ignore_from_file_val.as_sequence() {
737 if let Some(first_file) = ignore_file_seq.first().and_then(|v| v.as_str()) {
738 config.ignore_from_file = Some(first_file.to_string());
739 }
740 }
741 }
742
743 if let Some(rules) = yaml_value.get("rules").and_then(|r| r.as_mapping()) {
744 for (rule_name, rule_config) in rules {
745 let rule_name = rule_name.as_str().unwrap_or("");
746
747 if let Some(rule_str) = rule_config.as_str() {
748 match rule_str {
749 "disable" => {
750 config.set_rule_enabled(rule_name, false);
751 }
752 "enable" => {
753 config.set_rule_enabled(rule_name, true);
754 }
755 _ => {
756 config.set_rule_enabled(rule_name, true);
757 }
758 }
759 } else if let Some(rule_map) = rule_config.as_mapping() {
760 let mut enabled = None;
761 let mut severity = None;
762 let mut settings: Option<serde_json::Value> = None;
763
764 if let Some(enable_val) = rule_map.get("enable") {
765 enabled = enable_val.as_bool();
766 }
767 if let Some(disable_val) = rule_map.get("disable") {
768 if let Some(disable_bool) = disable_val.as_bool() {
769 enabled = Some(!disable_bool);
770 }
771 }
772
773 if let Some(level_val) = rule_map.get("level") {
774 if let Some(level_str) = level_val.as_str() {
775 match level_str {
776 "error" => severity = Some(crate::Severity::Error),
777 "warning" => severity = Some(crate::Severity::Warning),
778 "info" => severity = Some(crate::Severity::Info),
779 "disable" => enabled = Some(false),
780 _ => {}
781 }
782 }
783 }
784
785 match rule_name {
786 "line-length" => {
787 let mut max_length = 80;
788 let mut allow_non_breakable_words = true;
789
790 if let Some(max_val) = rule_map.get("max").and_then(|v| v.as_u64()) {
791 max_length = max_val as usize;
792 }
793 if let Some(allow_val) = rule_map.get("allow-non-breakable-words") {
794 if let Some(allow_bool) = allow_val.as_bool() {
795 allow_non_breakable_words = allow_bool;
796 }
797 }
798
799 let mut allow_non_breakable_inline_mappings = false;
800 if let Some(allow_val) = rule_map.get("allow-non-breakable-inline-mappings")
801 {
802 if let Some(allow_bool) = allow_val.as_bool() {
803 allow_non_breakable_inline_mappings = allow_bool;
804 }
805 }
806
807 let rule_settings = serde_json::to_value(config::LineLengthConfig {
808 max_length,
809 allow_non_breakable_words,
810 allow_non_breakable_inline_mappings,
811 })
812 .unwrap();
813 settings = Some(rule_settings);
814 }
815 "document-start" => {
816 if let Some(present_val) = rule_map.get("present") {
817 if let Some(present_bool) = present_val.as_bool() {
818 let rule_settings =
819 serde_json::to_value(config::DocumentStartConfig {
820 present: Some(present_bool),
821 })
822 .unwrap();
823 settings = Some(rule_settings);
824 }
825 }
826 }
827 "indentation" => {
828 let mut spaces = Some(2);
829 let mut indent_sequences = Some(true);
830 let check_multi_line_strings = Some(false);
831 let mut ignore = None;
832
833 if let Some(spaces_val) = rule_map.get("spaces").and_then(|v| v.as_u64()) {
834 spaces = Some(spaces_val as usize);
835 }
836 if let Some(indent_val) = rule_map.get("indent-sequences") {
837 if let Some(indent_bool) = indent_val.as_bool() {
838 indent_sequences = Some(indent_bool);
839 } else {
840 enabled = Some(false);
841 }
842 }
843
844 if let Some(ignore_val) = rule_map.get("ignore") {
845 if let Some(s) = ignore_val.as_str() {
846 ignore = Some(s.to_string());
847 } else {
848 ignore = serde_yaml::to_string(ignore_val)
849 .ok()
850 .map(|s| s.trim_matches('"').to_string());
851 }
852 }
853 let rule_settings = serde_json::to_value(config::IndentationConfig {
854 spaces,
855 indent_sequences,
856 check_multi_line_strings,
857 ignore,
858 })
859 .unwrap();
860 settings = Some(rule_settings);
861 }
862 "comments" => {
863 if let Some(min_spaces_val) = rule_map
864 .get("min-spaces-from-content")
865 .and_then(|v| v.as_u64())
866 {
867 let rule_settings = serde_json::to_value(config::CommentsConfig {
868 min_spaces_from_content: Some(min_spaces_val as usize),
869 })
870 .unwrap();
871 settings = Some(rule_settings);
872 }
873 }
874 "truthy" => {
875 let mut allowed_values = vec!["false".to_string(), "true".to_string()];
876 if let Some(allowed_vals) =
877 rule_map.get("allowed-values").and_then(|v| v.as_sequence())
878 {
879 allowed_values = allowed_vals
880 .iter()
881 .filter_map(|v| v.as_str().map(|s| s.to_string()))
882 .collect();
883 }
884 let rule_settings =
885 serde_json::to_value(config::TruthyConfig { allowed_values }).unwrap();
886 settings = Some(rule_settings);
887 }
888 "empty-lines" => {
889 let mut max = None;
890 let mut max_start = None;
891 let mut max_end = None;
892
893 if let Some(max_val) = rule_map.get("max").and_then(|v| v.as_u64()) {
894 max = Some(max_val as usize);
895 }
896 if let Some(start_val) = rule_map.get("max-start").and_then(|v| v.as_u64())
897 {
898 max_start = Some(start_val as usize);
899 }
900 if let Some(end_val) = rule_map.get("max-end").and_then(|v| v.as_u64()) {
901 max_end = Some(end_val as usize);
902 }
903
904 let rule_settings = serde_json::to_value(config::EmptyLinesConfig {
905 max,
906 max_start,
907 max_end,
908 })
909 .unwrap();
910 settings = Some(rule_settings);
911 }
912 "trailing-spaces" => {
913 let allow = rule_map
914 .get("allow")
915 .and_then(|v| v.as_bool())
916 .unwrap_or(false);
917 let rule_settings =
918 serde_json::to_value(config::TrailingSpacesConfig { allow }).unwrap();
919 settings = Some(rule_settings);
920 }
921 "document-end" => {
922 if let Some(present_val) = rule_map.get("present") {
923 if let Some(present_bool) = present_val.as_bool() {
924 let rule_settings =
925 serde_json::to_value(config::DocumentEndConfig {
926 present: Some(present_bool),
927 })
928 .unwrap();
929 settings = Some(rule_settings);
930 }
931 }
932 }
933 "key-ordering" => {
934 if let Some(order_vals) =
935 rule_map.get("order").and_then(|v| v.as_sequence())
936 {
937 let order: Vec<String> = order_vals
938 .iter()
939 .filter_map(|v| v.as_str().map(|s| s.to_string()))
940 .collect();
941 let rule_settings = serde_json::to_value(config::KeyOrderingConfig {
942 order: Some(order),
943 })
944 .unwrap();
945 settings = Some(rule_settings);
946 }
947 }
948 "anchors" => {
949 if let Some(max_len_val) =
950 rule_map.get("max-length").and_then(|v| v.as_u64())
951 {
952 let rule_settings = serde_json::to_value(config::AnchorsConfig {
953 max_length: Some(max_len_val as usize),
954 })
955 .unwrap();
956 settings = Some(rule_settings);
957 }
958 }
959 "new-lines" => {
960 if let Some(type_val) = rule_map.get("type").and_then(|v| v.as_str()) {
961 let type_str = type_val.to_string();
962 let rule_settings = serde_json::to_value(config::NewLinesConfig {
963 type_: Some(type_str),
964 })
965 .unwrap();
966 settings = Some(rule_settings);
967 }
968 }
969 _ => {}
970 }
971
972 let existing = config.rules.get(rule_name).cloned();
973 let final_enabled = if let Some(ref existing_config) = existing {
974 enabled.or(existing_config.enabled)
975 } else {
976 enabled
977 };
978
979 let final_severity =
980 severity.or_else(|| existing.as_ref().and_then(|c| c.severity));
981 let final_settings = settings.or_else(|| existing.clone().and_then(|c| c.settings));
982
983 let mut final_other = existing.map(|c| c.other).unwrap_or_default();
984
985 for (key, value) in rule_map {
986 if let Some(key_str) = key.as_str() {
987 let json_val = yaml_value_to_json(value);
988 final_other.insert(key_str.to_string(), json_val);
989 }
990 }
991
992 config.rules.insert(
993 rule_name.to_string(),
994 config::RuleConfig {
995 enabled: final_enabled,
996 severity: final_severity,
997 settings: final_settings,
998 other: final_other,
999 },
1000 );
1001 }
1002 }
1003 }
1004
1005 Ok(config)
1006}
1007
1008pub fn discover_config_file() -> Option<PathBuf> {
1009 discover_config_file_from_dir(std::env::current_dir().ok()?)
1010}
1011
1012pub fn discover_config_file_from_dir(start_dir: PathBuf) -> Option<PathBuf> {
1013 let mut dir = start_dir.as_path();
1014 loop {
1015 let config_path = dir.join(".yamllint");
1016 if config_path.exists() {
1017 return Some(config_path);
1018 }
1019
1020 if let Some(parent) = dir.parent() {
1021 dir = parent;
1022 } else {
1023 break;
1024 }
1025 }
1026
1027 None
1028}
1029
1030#[derive(Debug, Clone)]
1031pub struct LintResult {
1032 pub file: String,
1033 pub issues: Vec<(LintIssue, String)>,
1034}
1035
1036#[derive(Debug, Clone)]
1037pub struct LintIssue {
1038 pub line: usize,
1039 pub column: usize,
1040 pub message: String,
1041 pub severity: Severity,
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
1045pub enum Severity {
1046 Error,
1047 Warning,
1048 Info,
1049}
1050
1051impl Severity {
1052 pub fn from_str(s: &str) -> anyhow::Result<Self> {
1053 match s.to_lowercase().as_str() {
1054 "error" => Ok(Severity::Error),
1055 "warning" => Ok(Severity::Warning),
1056 "info" => Ok(Severity::Info),
1057 _ => Err(anyhow::anyhow!("Invalid severity: {}", s)),
1058 }
1059 }
1060
1061 pub fn to_string(&self) -> String {
1062 match self {
1063 Severity::Error => "error".to_string(),
1064 Severity::Warning => "warning".to_string(),
1065 Severity::Info => "info".to_string(),
1066 }
1067 }
1068}
1069
1070pub fn lint_yaml<P: AsRef<Path>>(file_path: P) -> Result<LintResult> {
1071 let path = file_path.as_ref();
1072 let _content = std::fs::read_to_string(path)?;
1073
1074 let result = LintResult {
1075 file: path.to_string_lossy().to_string(),
1076 issues: vec![],
1077 };
1078
1079 Ok(result)
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use std::io::Write;
1086 use tempfile::NamedTempFile;
1087
1088 #[test]
1089 fn test_lint_valid_yaml() {
1090 let mut file = NamedTempFile::new().expect("Failed to create temp file");
1091 writeln!(file, "key: value").expect("Failed to write to temp file");
1092 writeln!(file, "nested:").expect("Failed to write to temp file");
1093 writeln!(file, " subkey: subvalue").expect("Failed to write to temp file");
1094
1095 let result = lint_yaml(file.path()).expect("Failed to lint YAML");
1096 assert_eq!(result.issues.len(), 0);
1097 }
1098
1099 #[test]
1100 fn test_default_config() {
1101 let config = config::Config::default();
1102 assert!(config.rules.contains_key("line-length"));
1103 assert!(config.rules.contains_key("indentation"));
1104 }
1105}