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