1use crate::config::Config;
2use crate::lint_context::LintContext;
3use crate::rule::{LintWarning, Rule};
4use std::collections::hash_map::DefaultHasher;
5use std::collections::{HashMap, HashSet};
6use std::hash::{Hash, Hasher};
7
8const MAX_ITERATIONS: usize = 100;
10
11#[derive(Debug, Clone)]
16pub struct FixResult {
17 pub rules_fixed: usize,
19 pub iterations: usize,
21 pub context_creations: usize,
23 pub fixed_rule_names: HashSet<String>,
25 pub converged: bool,
27}
28
29fn hash_content(content: &str) -> u64 {
31 let mut hasher = DefaultHasher::new();
32 content.hash(&mut hasher);
33 hasher.finish()
34}
35
36pub struct FixCoordinator {
38 dependencies: HashMap<&'static str, Vec<&'static str>>,
40}
41
42impl Default for FixCoordinator {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl FixCoordinator {
49 pub fn new() -> Self {
50 let mut dependencies = HashMap::new();
51
52 dependencies.insert("MD010", vec!["MD007", "MD005"]);
59
60 dependencies.insert("MD013", vec!["MD009", "MD012"]);
65
66 dependencies.insert("MD004", vec!["MD007"]);
69
70 dependencies.insert("MD022", vec!["MD012"]);
73 dependencies.insert("MD023", vec!["MD012"]);
74
75 dependencies.insert("MD070", vec!["MD040", "MD031"]);
79
80 Self { dependencies }
81 }
82
83 pub fn get_optimal_order<'a>(&self, rules: &'a [Box<dyn Rule>]) -> Vec<&'a dyn Rule> {
85 let rule_map: HashMap<&str, &dyn Rule> = rules.iter().map(|r| (r.name(), r.as_ref())).collect();
87
88 let mut reverse_deps: HashMap<&str, HashSet<&str>> = HashMap::new();
90 for (prereq, dependents) in &self.dependencies {
91 for dependent in dependents {
92 reverse_deps.entry(dependent).or_default().insert(prereq);
93 }
94 }
95
96 let mut sorted = Vec::new();
98 let mut visited = HashSet::new();
99 let mut visiting = HashSet::new();
100
101 fn visit<'a>(
102 rule_name: &str,
103 rule_map: &HashMap<&str, &'a dyn Rule>,
104 reverse_deps: &HashMap<&str, HashSet<&str>>,
105 visited: &mut HashSet<String>,
106 visiting: &mut HashSet<String>,
107 sorted: &mut Vec<&'a dyn Rule>,
108 ) {
109 if visited.contains(rule_name) {
110 return;
111 }
112
113 if visiting.contains(rule_name) {
114 return;
116 }
117
118 visiting.insert(rule_name.to_string());
119
120 if let Some(deps) = reverse_deps.get(rule_name) {
122 for dep in deps {
123 if rule_map.contains_key(dep) {
124 visit(dep, rule_map, reverse_deps, visited, visiting, sorted);
125 }
126 }
127 }
128
129 visiting.remove(rule_name);
130 visited.insert(rule_name.to_string());
131
132 if let Some(&rule) = rule_map.get(rule_name) {
134 sorted.push(rule);
135 }
136 }
137
138 for rule in rules {
140 visit(
141 rule.name(),
142 &rule_map,
143 &reverse_deps,
144 &mut visited,
145 &mut visiting,
146 &mut sorted,
147 );
148 }
149
150 for rule in rules {
152 if !sorted.iter().any(|r| r.name() == rule.name()) {
153 sorted.push(rule.as_ref());
154 }
155 }
156
157 sorted
158 }
159
160 pub fn apply_fixes_iterative(
162 &self,
163 rules: &[Box<dyn Rule>],
164 all_warnings: &[LintWarning],
165 content: &mut String,
166 config: &Config,
167 max_iterations: usize,
168 ) -> Result<FixResult, String> {
169 let max_iterations = max_iterations.min(MAX_ITERATIONS);
171
172 let ordered_rules = self.get_optimal_order(rules);
174
175 let mut warnings_by_rule: HashMap<&str, Vec<&LintWarning>> = HashMap::new();
177 for warning in all_warnings {
178 if let Some(ref rule_name) = warning.rule_name {
179 warnings_by_rule.entry(rule_name.as_str()).or_default().push(warning);
180 }
181 }
182
183 let mut total_fixed = 0;
184 let mut total_ctx_creations = 0;
185 let mut iterations = 0;
186 let mut previous_hash = hash_content(content);
187
188 let mut processed_rules = HashSet::new();
190
191 let mut fixed_rule_names = HashSet::new();
193
194 while iterations < max_iterations {
196 iterations += 1;
197
198 let mut fixes_in_iteration = 0;
199 let mut any_fix_applied = false;
200
201 for rule in &ordered_rules {
203 if processed_rules.contains(rule.name()) {
205 continue;
206 }
207
208 if !warnings_by_rule.contains_key(rule.name()) {
210 processed_rules.insert(rule.name());
211 continue;
212 }
213
214 if config
216 .global
217 .unfixable
218 .iter()
219 .any(|r| r.eq_ignore_ascii_case(rule.name()))
220 {
221 processed_rules.insert(rule.name());
222 continue;
223 }
224
225 if !config.global.fixable.is_empty()
226 && !config
227 .global
228 .fixable
229 .iter()
230 .any(|r| r.eq_ignore_ascii_case(rule.name()))
231 {
232 processed_rules.insert(rule.name());
233 continue;
234 }
235
236 let ctx = LintContext::new(content, config.markdown_flavor(), None);
238 total_ctx_creations += 1;
239
240 match rule.fix(&ctx) {
242 Ok(fixed_content) => {
243 if fixed_content != *content {
244 *content = fixed_content;
245 fixes_in_iteration += 1;
246 any_fix_applied = true;
247 processed_rules.insert(rule.name());
248 fixed_rule_names.insert(rule.name().to_string());
249
250 if self.dependencies.contains_key(rule.name()) {
252 break;
253 }
254 } else {
256 processed_rules.insert(rule.name());
258 }
259 }
260 Err(_) => {
261 processed_rules.insert(rule.name());
263 }
264 }
265 }
266
267 total_fixed += fixes_in_iteration;
268
269 let current_hash = hash_content(content);
271 if current_hash == previous_hash {
272 return Ok(FixResult {
274 rules_fixed: total_fixed,
275 iterations,
276 context_creations: total_ctx_creations,
277 fixed_rule_names,
278 converged: true,
279 });
280 }
281 previous_hash = current_hash;
282
283 if !any_fix_applied {
285 break;
286 }
287
288 if processed_rules.len() >= ordered_rules.len() {
290 break;
291 }
292 }
293
294 let converged = iterations < max_iterations;
296 Ok(FixResult {
297 rules_fixed: total_fixed,
298 iterations,
299 context_creations: total_ctx_creations,
300 fixed_rule_names,
301 converged,
302 })
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::config::GlobalConfig;
310 use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory};
311
312 #[derive(Clone)]
314 struct MockRule {
315 name: &'static str,
316 warnings: Vec<LintWarning>,
317 fix_content: String,
318 }
319
320 impl Rule for MockRule {
321 fn name(&self) -> &'static str {
322 self.name
323 }
324
325 fn check(&self, _ctx: &LintContext) -> LintResult {
326 Ok(self.warnings.clone())
327 }
328
329 fn fix(&self, _ctx: &LintContext) -> Result<String, LintError> {
330 Ok(self.fix_content.clone())
331 }
332
333 fn description(&self) -> &'static str {
334 "Mock rule for testing"
335 }
336
337 fn category(&self) -> RuleCategory {
338 RuleCategory::Whitespace
339 }
340
341 fn as_any(&self) -> &dyn std::any::Any {
342 self
343 }
344 }
345
346 #[test]
347 fn test_dependency_ordering() {
348 let coordinator = FixCoordinator::new();
349
350 let rules: Vec<Box<dyn Rule>> = vec![
351 Box::new(MockRule {
352 name: "MD009",
353 warnings: vec![],
354 fix_content: "".to_string(),
355 }),
356 Box::new(MockRule {
357 name: "MD013",
358 warnings: vec![],
359 fix_content: "".to_string(),
360 }),
361 Box::new(MockRule {
362 name: "MD010",
363 warnings: vec![],
364 fix_content: "".to_string(),
365 }),
366 Box::new(MockRule {
367 name: "MD007",
368 warnings: vec![],
369 fix_content: "".to_string(),
370 }),
371 ];
372
373 let ordered = coordinator.get_optimal_order(&rules);
374 let ordered_names: Vec<&str> = ordered.iter().map(|r| r.name()).collect();
375
376 let md010_idx = ordered_names.iter().position(|&n| n == "MD010").unwrap();
378 let md007_idx = ordered_names.iter().position(|&n| n == "MD007").unwrap();
379 assert!(md010_idx < md007_idx, "MD010 should come before MD007");
380
381 let md013_idx = ordered_names.iter().position(|&n| n == "MD013").unwrap();
383 let md009_idx = ordered_names.iter().position(|&n| n == "MD009").unwrap();
384 assert!(md013_idx < md009_idx, "MD013 should come before MD009");
385 }
386
387 #[test]
388 fn test_single_iteration_fix() {
389 let coordinator = FixCoordinator::new();
390
391 let rules: Vec<Box<dyn Rule>> = vec![Box::new(MockRule {
392 name: "MD001",
393 warnings: vec![LintWarning {
394 line: 1,
395 column: 1,
396 end_line: 1,
397 end_column: 10,
398 message: "Test warning".to_string(),
399 rule_name: Some("MD001".to_string()),
400 severity: crate::rule::Severity::Error,
401 fix: None,
402 }],
403 fix_content: "fixed content".to_string(),
404 })];
405
406 let warnings = vec![LintWarning {
407 line: 1,
408 column: 1,
409 end_line: 1,
410 end_column: 10,
411 message: "Test warning".to_string(),
412 rule_name: Some("MD001".to_string()),
413 severity: crate::rule::Severity::Error,
414 fix: None,
415 }];
416
417 let mut content = "original content".to_string();
418 let config = Config {
419 global: GlobalConfig::default(),
420 per_file_ignores: HashMap::new(),
421 rules: Default::default(),
422 project_root: None,
423 };
424
425 let result = coordinator.apply_fixes_iterative(&rules, &warnings, &mut content, &config, 5);
426
427 assert!(result.is_ok());
428 let result = result.unwrap();
429 assert_eq!(result.rules_fixed, 1);
430 assert_eq!(result.iterations, 1);
431 assert_eq!(result.context_creations, 1);
432 assert!(result.converged);
433 assert_eq!(content, "fixed content");
434 }
435
436 #[test]
437 fn test_multiple_iteration_with_dependencies() {
438 let coordinator = FixCoordinator::new();
439
440 let rules: Vec<Box<dyn Rule>> = vec![
441 Box::new(MockRule {
442 name: "MD010", warnings: vec![LintWarning {
444 line: 1,
445 column: 1,
446 end_line: 1,
447 end_column: 10,
448 message: "Tabs".to_string(),
449 rule_name: Some("MD010".to_string()),
450 severity: crate::rule::Severity::Error,
451 fix: None,
452 }],
453 fix_content: "content with spaces".to_string(),
454 }),
455 Box::new(MockRule {
456 name: "MD007", warnings: vec![LintWarning {
458 line: 1,
459 column: 1,
460 end_line: 1,
461 end_column: 10,
462 message: "Indentation".to_string(),
463 rule_name: Some("MD007".to_string()),
464 severity: crate::rule::Severity::Error,
465 fix: None,
466 }],
467 fix_content: "content with spaces and proper indent".to_string(),
468 }),
469 ];
470
471 let warnings = vec![
472 LintWarning {
473 line: 1,
474 column: 1,
475 end_line: 1,
476 end_column: 10,
477 message: "Tabs".to_string(),
478 rule_name: Some("MD010".to_string()),
479 severity: crate::rule::Severity::Error,
480 fix: None,
481 },
482 LintWarning {
483 line: 1,
484 column: 1,
485 end_line: 1,
486 end_column: 10,
487 message: "Indentation".to_string(),
488 rule_name: Some("MD007".to_string()),
489 severity: crate::rule::Severity::Error,
490 fix: None,
491 },
492 ];
493
494 let mut content = "content with tabs".to_string();
495 let config = Config {
496 global: GlobalConfig::default(),
497 per_file_ignores: HashMap::new(),
498 rules: Default::default(),
499 project_root: None,
500 };
501
502 let result = coordinator.apply_fixes_iterative(&rules, &warnings, &mut content, &config, 5);
503
504 assert!(result.is_ok());
505 let result = result.unwrap();
506 assert_eq!(result.rules_fixed, 2);
507 assert_eq!(result.iterations, 2); assert!(result.context_creations >= 2);
509 assert!(result.converged);
510 }
511
512 #[test]
513 fn test_unfixable_rules_skipped() {
514 let coordinator = FixCoordinator::new();
515
516 let rules: Vec<Box<dyn Rule>> = vec![Box::new(MockRule {
517 name: "MD001",
518 warnings: vec![LintWarning {
519 line: 1,
520 column: 1,
521 end_line: 1,
522 end_column: 10,
523 message: "Test".to_string(),
524 rule_name: Some("MD001".to_string()),
525 severity: crate::rule::Severity::Error,
526 fix: None,
527 }],
528 fix_content: "fixed".to_string(),
529 })];
530
531 let warnings = vec![LintWarning {
532 line: 1,
533 column: 1,
534 end_line: 1,
535 end_column: 10,
536 message: "Test".to_string(),
537 rule_name: Some("MD001".to_string()),
538 severity: crate::rule::Severity::Error,
539 fix: None,
540 }];
541
542 let mut content = "original".to_string();
543 let mut config = Config {
544 global: GlobalConfig::default(),
545 per_file_ignores: HashMap::new(),
546 rules: Default::default(),
547 project_root: None,
548 };
549 config.global.unfixable = vec!["MD001".to_string()];
550
551 let result = coordinator.apply_fixes_iterative(&rules, &warnings, &mut content, &config, 5);
552
553 assert!(result.is_ok());
554 let result = result.unwrap();
555 assert_eq!(result.rules_fixed, 0);
556 assert!(result.converged);
557 assert_eq!(content, "original"); }
559
560 #[test]
561 fn test_max_iterations_limit() {
562 let coordinator = FixCoordinator::new();
564
565 #[derive(Clone)]
567 struct AlwaysChangeRule;
568 impl Rule for AlwaysChangeRule {
569 fn name(&self) -> &'static str {
570 "MD999"
571 }
572 fn check(&self, _: &LintContext) -> LintResult {
573 Ok(vec![LintWarning {
574 line: 1,
575 column: 1,
576 end_line: 1,
577 end_column: 10,
578 message: "Always warns".to_string(),
579 rule_name: Some("MD999".to_string()),
580 severity: crate::rule::Severity::Error,
581 fix: None,
582 }])
583 }
584 fn fix(&self, ctx: &LintContext) -> Result<String, LintError> {
585 Ok(format!("{}x", ctx.content))
586 }
587 fn description(&self) -> &'static str {
588 "Always changes"
589 }
590 fn category(&self) -> RuleCategory {
591 RuleCategory::Whitespace
592 }
593 fn as_any(&self) -> &dyn std::any::Any {
594 self
595 }
596 }
597
598 let rules: Vec<Box<dyn Rule>> = vec![Box::new(AlwaysChangeRule)];
599 let warnings = vec![LintWarning {
600 line: 1,
601 column: 1,
602 end_line: 1,
603 end_column: 10,
604 message: "Always warns".to_string(),
605 rule_name: Some("MD999".to_string()),
606 severity: crate::rule::Severity::Error,
607 fix: None,
608 }];
609
610 let mut content = "test".to_string();
611 let config = Config {
612 global: GlobalConfig::default(),
613 per_file_ignores: HashMap::new(),
614 rules: Default::default(),
615 project_root: None,
616 };
617
618 let result = coordinator.apply_fixes_iterative(&rules, &warnings, &mut content, &config, 3);
619
620 assert!(result.is_ok());
621 let result = result.unwrap();
622 assert_eq!(result.iterations, 1); assert!(result.converged);
624 }
625
626 #[test]
627 fn test_empty_rules_and_warnings() {
628 let coordinator = FixCoordinator::new();
629 let rules: Vec<Box<dyn Rule>> = vec![];
630 let warnings: Vec<LintWarning> = vec![];
631
632 let mut content = "unchanged".to_string();
633 let config = Config {
634 global: GlobalConfig::default(),
635 per_file_ignores: HashMap::new(),
636 rules: Default::default(),
637 project_root: None,
638 };
639
640 let result = coordinator.apply_fixes_iterative(&rules, &warnings, &mut content, &config, 5);
641
642 assert!(result.is_ok());
643 let result = result.unwrap();
644 assert_eq!(result.rules_fixed, 0);
645 assert_eq!(result.iterations, 1);
646 assert_eq!(result.context_creations, 0);
647 assert!(result.converged);
648 assert_eq!(content, "unchanged");
649 }
650
651 #[test]
652 fn test_cyclic_dependencies_handled() {
653 let mut coordinator = FixCoordinator::new();
655
656 coordinator.dependencies.insert("RuleA", vec!["RuleB"]);
658 coordinator.dependencies.insert("RuleB", vec!["RuleC"]);
659 coordinator.dependencies.insert("RuleC", vec!["RuleA"]);
660
661 let rules: Vec<Box<dyn Rule>> = vec![
662 Box::new(MockRule {
663 name: "RuleA",
664 warnings: vec![],
665 fix_content: "".to_string(),
666 }),
667 Box::new(MockRule {
668 name: "RuleB",
669 warnings: vec![],
670 fix_content: "".to_string(),
671 }),
672 Box::new(MockRule {
673 name: "RuleC",
674 warnings: vec![],
675 fix_content: "".to_string(),
676 }),
677 ];
678
679 let ordered = coordinator.get_optimal_order(&rules);
681
682 assert_eq!(ordered.len(), 3);
684 }
685}