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