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