Skip to main content

rumdl_lib/code_block_tools/
registry.rs

1//! Built-in tool registry with definitions for common formatters and linters.
2//!
3//! This module provides default configurations for popular tools like ruff, prettier,
4//! shellcheck, etc. Users can override these in their configuration.
5
6use super::config::ToolDefinition;
7use std::collections::BTreeMap;
8use std::collections::HashMap;
9use std::sync::LazyLock;
10
11/// Registry of built-in tool definitions.
12pub struct ToolRegistry {
13    /// User-defined tools (override built-ins)
14    user_tools: BTreeMap<String, ToolDefinition>,
15}
16
17impl ToolRegistry {
18    /// Create a new registry with user-defined tools.
19    pub fn new(user_tools: BTreeMap<String, ToolDefinition>) -> Self {
20        Self { user_tools }
21    }
22
23    /// Get a tool definition by ID.
24    ///
25    /// Checks user tools first, then falls back to built-in tools.
26    pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
27        self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
28    }
29
30    /// Check if a tool ID is valid (either user-defined or built-in).
31    pub fn contains(&self, tool_id: &str) -> bool {
32        self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
33    }
34
35    /// List all available tool IDs.
36    pub fn list_tools(&self) -> Vec<&str> {
37        let mut tools: Vec<&str> = self.user_tools.keys().map(std::string::String::as_str).collect();
38        for key in BUILTIN_TOOLS.keys() {
39            if !self.user_tools.contains_key(*key) {
40                tools.push(key);
41            }
42        }
43        tools.sort_unstable();
44        tools
45    }
46}
47
48impl Default for ToolRegistry {
49    fn default() -> Self {
50        Self::new(BTreeMap::new())
51    }
52}
53
54/// Built-in tool definitions.
55///
56/// These are common formatters and linters that work well with stdin/stdout.
57static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
58    let mut m = HashMap::new();
59
60    // Python - ruff
61    m.insert(
62        "ruff:check",
63        ToolDefinition {
64            command: vec![
65                "ruff".to_string(),
66                "check".to_string(),
67                "--output-format=concise".to_string(),
68                "--stdin-filename=_.py".to_string(),
69                "-".to_string(),
70            ],
71            stdin: true,
72            stdout: true,
73            lint_args: vec![],
74            format_args: vec![],
75        },
76    );
77
78    m.insert(
79        "ruff:format",
80        ToolDefinition {
81            command: vec![
82                "ruff".to_string(),
83                "format".to_string(),
84                "--stdin-filename=_.py".to_string(),
85                "-".to_string(),
86            ],
87            stdin: true,
88            stdout: true,
89            lint_args: vec![],
90            format_args: vec![],
91        },
92    );
93
94    // Python - black
95    m.insert(
96        "black",
97        ToolDefinition {
98            command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
99            stdin: true,
100            stdout: true,
101            lint_args: vec!["--check".to_string()],
102            format_args: vec![],
103        },
104    );
105
106    // JavaScript/TypeScript - prettier
107    m.insert(
108        "prettier",
109        ToolDefinition {
110            command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
111            stdin: true,
112            stdout: true,
113            lint_args: vec!["--check".to_string()],
114            format_args: vec![],
115        },
116    );
117
118    m.insert(
119        "prettier:json",
120        ToolDefinition {
121            command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
122            stdin: true,
123            stdout: true,
124            lint_args: vec!["--check".to_string()],
125            format_args: vec![],
126        },
127    );
128
129    m.insert(
130        "prettier:yaml",
131        ToolDefinition {
132            command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
133            stdin: true,
134            stdout: true,
135            lint_args: vec!["--check".to_string()],
136            format_args: vec![],
137        },
138    );
139
140    m.insert(
141        "prettier:html",
142        ToolDefinition {
143            command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
144            stdin: true,
145            stdout: true,
146            lint_args: vec!["--check".to_string()],
147            format_args: vec![],
148        },
149    );
150
151    m.insert(
152        "prettier:css",
153        ToolDefinition {
154            command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
155            stdin: true,
156            stdout: true,
157            lint_args: vec!["--check".to_string()],
158            format_args: vec![],
159        },
160    );
161
162    m.insert(
163        "prettier:markdown",
164        ToolDefinition {
165            command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
166            stdin: true,
167            stdout: true,
168            lint_args: vec!["--check".to_string()],
169            format_args: vec![],
170        },
171    );
172
173    // JavaScript/TypeScript - eslint (lint only)
174    m.insert(
175        "eslint",
176        ToolDefinition {
177            command: vec![
178                "eslint".to_string(),
179                "--stdin".to_string(),
180                "--stdin-filename=_.js".to_string(),
181            ],
182            stdin: true,
183            stdout: true,
184            lint_args: vec![],
185            format_args: vec!["--fix-dry-run".to_string()],
186        },
187    );
188
189    // Shell - shellcheck (lint only)
190    m.insert(
191        "shellcheck",
192        ToolDefinition {
193            command: vec!["shellcheck".to_string(), "-".to_string()],
194            stdin: true,
195            stdout: true,
196            lint_args: vec![],
197            format_args: vec![],
198        },
199    );
200
201    // Shell - shfmt
202    m.insert(
203        "shfmt",
204        ToolDefinition {
205            command: vec!["shfmt".to_string()],
206            stdin: true,
207            stdout: true,
208            lint_args: vec!["-d".to_string()], // diff mode for lint
209            format_args: vec![],
210        },
211    );
212
213    // Rust - rustfmt
214    m.insert(
215        "rustfmt",
216        ToolDefinition {
217            command: vec!["rustfmt".to_string()],
218            stdin: true,
219            stdout: true,
220            lint_args: vec!["--check".to_string()],
221            format_args: vec![],
222        },
223    );
224
225    // Go - gofmt
226    m.insert(
227        "gofmt",
228        ToolDefinition {
229            command: vec!["gofmt".to_string()],
230            stdin: true,
231            stdout: true,
232            lint_args: vec!["-d".to_string()], // diff mode for lint
233            format_args: vec![],
234        },
235    );
236
237    // Go - goimports
238    m.insert(
239        "goimports",
240        ToolDefinition {
241            command: vec!["goimports".to_string()],
242            stdin: true,
243            stdout: true,
244            lint_args: vec!["-d".to_string()],
245            format_args: vec![],
246        },
247    );
248
249    // C/C++ - clang-format
250    m.insert(
251        "clang-format",
252        ToolDefinition {
253            command: vec!["clang-format".to_string()],
254            stdin: true,
255            stdout: true,
256            lint_args: vec!["--dry-run".to_string(), "--Werror".to_string()],
257            format_args: vec![],
258        },
259    );
260
261    // SQL - sqlfluff
262    m.insert(
263        "sqlfluff:lint",
264        ToolDefinition {
265            command: vec!["sqlfluff".to_string(), "lint".to_string(), "-".to_string()],
266            stdin: true,
267            stdout: true,
268            lint_args: vec![],
269            format_args: vec![],
270        },
271    );
272
273    m.insert(
274        "sqlfluff:fix",
275        ToolDefinition {
276            command: vec!["sqlfluff".to_string(), "fix".to_string(), "-".to_string()],
277            stdin: true,
278            stdout: true,
279            lint_args: vec![],
280            format_args: vec![],
281        },
282    );
283
284    // JSON - jq (format/lint)
285    m.insert(
286        "jq",
287        ToolDefinition {
288            command: vec!["jq".to_string(), ".".to_string()],
289            stdin: true,
290            stdout: true,
291            lint_args: vec![],
292            format_args: vec![],
293        },
294    );
295
296    // YAML - yamlfmt
297    m.insert(
298        "yamlfmt",
299        ToolDefinition {
300            command: vec!["yamlfmt".to_string()],
301            stdin: true,
302            stdout: true,
303            lint_args: vec!["-lint".to_string(), "-".to_string()],
304            format_args: vec!["-".to_string()],
305        },
306    );
307
308    // TOML - taplo
309    m.insert(
310        "taplo",
311        ToolDefinition {
312            command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
313            stdin: true,
314            stdout: true,
315            lint_args: vec!["--check".to_string()],
316            format_args: vec![],
317        },
318    );
319
320    // Terraform - terraform fmt
321    m.insert(
322        "terraform-fmt",
323        ToolDefinition {
324            command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
325            stdin: true,
326            stdout: true,
327            lint_args: vec!["-check".to_string()],
328            format_args: vec![],
329        },
330    );
331
332    // Nix - nixfmt
333    m.insert(
334        "nixfmt",
335        ToolDefinition {
336            command: vec!["nixfmt".to_string()],
337            stdin: true,
338            stdout: true,
339            lint_args: vec!["--check".to_string()],
340            format_args: vec![],
341        },
342    );
343
344    // Lua - stylua
345    m.insert(
346        "stylua",
347        ToolDefinition {
348            command: vec!["stylua".to_string(), "-".to_string()],
349            stdin: true,
350            stdout: true,
351            lint_args: vec!["--check".to_string()],
352            format_args: vec![],
353        },
354    );
355
356    // Ruby - rubocop
357    m.insert(
358        "rubocop",
359        ToolDefinition {
360            command: vec!["rubocop".to_string(), "--stdin".to_string(), "_.rb".to_string()],
361            stdin: true,
362            stdout: true,
363            lint_args: vec![],
364            format_args: vec!["--autocorrect".to_string()],
365        },
366    );
367
368    // Haskell - ormolu
369    m.insert(
370        "ormolu",
371        ToolDefinition {
372            command: vec!["ormolu".to_string()],
373            stdin: true,
374            stdout: true,
375            lint_args: vec!["--check-idempotence".to_string()],
376            format_args: vec![],
377        },
378    );
379
380    // Elm - elm-format
381    m.insert(
382        "elm-format",
383        ToolDefinition {
384            command: vec!["elm-format".to_string(), "--stdin".to_string()],
385            stdin: true,
386            stdout: true,
387            lint_args: vec!["--validate".to_string()],
388            format_args: vec![],
389        },
390    );
391
392    // Zig - zig fmt
393    m.insert(
394        "zig-fmt",
395        ToolDefinition {
396            command: vec!["zig".to_string(), "fmt".to_string(), "--stdin".to_string()],
397            stdin: true,
398            stdout: true,
399            lint_args: vec!["--check".to_string()],
400            format_args: vec![],
401        },
402    );
403
404    // Dart - dart format
405    m.insert(
406        "dart-format",
407        ToolDefinition {
408            command: vec!["dart".to_string(), "format".to_string()],
409            stdin: true,
410            stdout: true,
411            lint_args: vec!["--output=none".to_string(), "--set-exit-if-changed".to_string()],
412            format_args: vec![],
413        },
414    );
415
416    // Swift - swift-format
417    m.insert(
418        "swift-format",
419        ToolDefinition {
420            command: vec!["swift-format".to_string()],
421            stdin: true,
422            stdout: true,
423            lint_args: vec!["lint".to_string()],
424            format_args: vec![],
425        },
426    );
427
428    // Kotlin - ktfmt
429    m.insert(
430        "ktfmt",
431        ToolDefinition {
432            command: vec!["ktfmt".to_string(), "--stdin".to_string()],
433            stdin: true,
434            stdout: true,
435            lint_args: vec!["--dry-run".to_string()],
436            format_args: vec![],
437        },
438    );
439
440    // Jinja/HTML - djlint
441    m.insert(
442        "djlint",
443        ToolDefinition {
444            command: vec!["djlint".to_string(), "-".to_string()],
445            stdin: true,
446            stdout: true,
447            lint_args: vec![],
448            format_args: vec!["--reformat".to_string()],
449        },
450    );
451
452    m.insert(
453        "djlint:lint",
454        ToolDefinition {
455            command: vec!["djlint".to_string(), "-".to_string()],
456            stdin: true,
457            stdout: true,
458            lint_args: vec![],
459            format_args: vec![],
460        },
461    );
462
463    m.insert(
464        "djlint:reformat",
465        ToolDefinition {
466            command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
467            stdin: true,
468            stdout: true,
469            lint_args: vec![],
470            format_args: vec![],
471        },
472    );
473
474    // Shell - beautysh
475    m.insert(
476        "beautysh",
477        ToolDefinition {
478            command: vec!["beautysh".to_string(), "-".to_string()],
479            stdin: true,
480            stdout: true,
481            lint_args: vec!["--check".to_string()],
482            format_args: vec![],
483        },
484    );
485
486    // TOML - tombi (default runs `tombi lint` since users typically configure it in the lint slot)
487    m.insert(
488        "tombi",
489        ToolDefinition {
490            command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
491            stdin: true,
492            stdout: true,
493            lint_args: vec![],
494            format_args: vec![],
495        },
496    );
497
498    m.insert(
499        "tombi:format",
500        ToolDefinition {
501            command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
502            stdin: true,
503            stdout: true,
504            lint_args: vec![],
505            format_args: vec![],
506        },
507    );
508
509    m.insert(
510        "tombi:lint",
511        ToolDefinition {
512            command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
513            stdin: true,
514            stdout: true,
515            lint_args: vec![],
516            format_args: vec![],
517        },
518    );
519
520    // JavaScript/CSS/HTML/JSON - oxfmt (OXC formatter)
521    m.insert(
522        "oxfmt",
523        ToolDefinition {
524            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
525            stdin: true,
526            stdout: true,
527            lint_args: vec!["--check".to_string()],
528            format_args: vec![],
529        },
530    );
531
532    m.insert(
533        "oxfmt:js",
534        ToolDefinition {
535            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
536            stdin: true,
537            stdout: true,
538            lint_args: vec!["--check".to_string()],
539            format_args: vec![],
540        },
541    );
542
543    m.insert(
544        "oxfmt:ts",
545        ToolDefinition {
546            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
547            stdin: true,
548            stdout: true,
549            lint_args: vec!["--check".to_string()],
550            format_args: vec![],
551        },
552    );
553
554    m.insert(
555        "oxfmt:jsx",
556        ToolDefinition {
557            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
558            stdin: true,
559            stdout: true,
560            lint_args: vec!["--check".to_string()],
561            format_args: vec![],
562        },
563    );
564
565    m.insert(
566        "oxfmt:tsx",
567        ToolDefinition {
568            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
569            stdin: true,
570            stdout: true,
571            lint_args: vec!["--check".to_string()],
572            format_args: vec![],
573        },
574    );
575
576    m.insert(
577        "oxfmt:json",
578        ToolDefinition {
579            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
580            stdin: true,
581            stdout: true,
582            lint_args: vec!["--check".to_string()],
583            format_args: vec![],
584        },
585    );
586
587    m.insert(
588        "oxfmt:css",
589        ToolDefinition {
590            command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
591            stdin: true,
592            stdout: true,
593            lint_args: vec!["--check".to_string()],
594            format_args: vec![],
595        },
596    );
597
598    m
599});
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_get_builtin_tool() {
607        let registry = ToolRegistry::default();
608
609        let tool = registry.get("ruff:check").expect("Should find ruff:check");
610        assert!(tool.command.contains(&"ruff".to_string()));
611        assert!(tool.stdin);
612        assert!(tool.stdout);
613
614        let tool = registry.get("shellcheck").expect("Should find shellcheck");
615        assert!(tool.command.contains(&"shellcheck".to_string()));
616    }
617
618    #[test]
619    fn test_builtin_yamlfmt_lint_command_validates_stdin() {
620        let registry = ToolRegistry::default();
621
622        let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
623        let mut argv = tool.command.clone();
624        argv.extend(tool.lint_args.clone());
625
626        assert_eq!(argv, vec!["yamlfmt", "-lint", "-"]);
627    }
628
629    #[test]
630    fn test_get_user_tool_overrides_builtin() {
631        let mut user_tools = BTreeMap::new();
632        user_tools.insert(
633            "ruff:check".to_string(),
634            ToolDefinition {
635                command: vec!["custom-ruff".to_string()],
636                stdin: false,
637                stdout: false,
638                lint_args: vec![],
639                format_args: vec![],
640            },
641        );
642
643        let registry = ToolRegistry::new(user_tools);
644
645        let tool = registry.get("ruff:check").expect("Should find ruff:check");
646        assert_eq!(tool.command, vec!["custom-ruff"]);
647        assert!(!tool.stdin); // User override
648    }
649
650    #[test]
651    fn test_contains() {
652        let registry = ToolRegistry::default();
653
654        assert!(registry.contains("ruff:check"));
655        assert!(registry.contains("prettier"));
656        assert!(registry.contains("shellcheck"));
657        assert!(!registry.contains("nonexistent-tool"));
658    }
659
660    #[test]
661    fn test_list_tools() {
662        let registry = ToolRegistry::default();
663        let tools = registry.list_tools();
664
665        assert!(tools.contains(&"ruff:check"));
666        assert!(tools.contains(&"ruff:format"));
667        assert!(tools.contains(&"prettier"));
668        assert!(tools.contains(&"shellcheck"));
669        assert!(tools.contains(&"shfmt"));
670        assert!(tools.contains(&"rustfmt"));
671        assert!(tools.contains(&"gofmt"));
672    }
673
674    #[test]
675    fn test_user_tools_in_list() {
676        let mut user_tools = BTreeMap::new();
677        user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
678
679        let registry = ToolRegistry::new(user_tools);
680        let tools = registry.list_tools();
681
682        assert!(tools.contains(&"my-custom-tool"));
683        assert!(tools.contains(&"ruff:check")); // Built-in still available
684    }
685
686    #[test]
687    fn test_new_builtin_tools() {
688        let registry = ToolRegistry::default();
689
690        // djlint
691        let tool = registry.get("djlint").expect("Should find djlint");
692        assert!(tool.command.contains(&"djlint".to_string()));
693        assert!(tool.stdin);
694
695        // beautysh
696        let tool = registry.get("beautysh").expect("Should find beautysh");
697        assert!(tool.command.contains(&"beautysh".to_string()));
698        assert!(tool.stdin);
699
700        // tombi
701        let tool = registry.get("tombi").expect("Should find tombi");
702        assert!(tool.command.contains(&"tombi".to_string()));
703        assert!(tool.stdin);
704
705        let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
706        assert!(tool.command.contains(&"lint".to_string()));
707
708        let tool = registry.get("tombi:format").expect("Should find tombi:format");
709        assert!(
710            tool.command.contains(&"format".to_string()),
711            "tombi:format should use 'format' subcommand, got: {:?}",
712            tool.command
713        );
714
715        // oxfmt
716        let tool = registry.get("oxfmt").expect("Should find oxfmt");
717        assert!(tool.command.contains(&"oxfmt".to_string()));
718        assert!(tool.stdin);
719
720        let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
721        assert!(tool.command.iter().any(|s| s.contains("_.ts")));
722    }
723
724    // =========================================================================
725    // Issue #527: bare "tombi" in format slot resolves to lint command
726    // =========================================================================
727
728    /// The bare "tombi" registry entry defaults to `tombi lint -`.
729    /// The processor's `resolve_tool` method handles context-aware resolution:
730    /// in format context, it resolves "tombi" to "tombi:format" automatically.
731    #[test]
732    fn test_bare_tombi_resolves_to_lint_not_format() {
733        let registry = ToolRegistry::default();
734
735        let bare = registry.get("tombi").expect("Should find bare tombi");
736        let format = registry.get("tombi:format").expect("Should find tombi:format");
737
738        // The bare entry uses `lint` subcommand
739        assert!(
740            bare.command.contains(&"lint".to_string()),
741            "Bare 'tombi' uses lint subcommand: {:?}",
742            bare.command
743        );
744
745        // The format entry uses `format` subcommand
746        assert!(
747            format.command.contains(&"format".to_string()),
748            "tombi:format uses format subcommand: {:?}",
749            format.command
750        );
751
752        // These are different commands — using bare "tombi" in format = [...] is a bug
753        assert_ne!(
754            bare.command, format.command,
755            "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
756        );
757    }
758
759    /// Tools that have both lint and format variants should have distinct entries.
760    /// The processor resolves bare names to context-specific variants automatically.
761    #[test]
762    fn test_tools_with_lint_format_variants_are_distinct() {
763        let registry = ToolRegistry::default();
764
765        // ruff has both check and format
766        let ruff_check = registry.get("ruff:check").expect("ruff:check");
767        let ruff_format = registry.get("ruff:format").expect("ruff:format");
768        assert_ne!(
769            ruff_check.command, ruff_format.command,
770            "ruff:check and ruff:format should be distinct"
771        );
772
773        // tombi has both lint and format
774        let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
775        let tombi_format = registry.get("tombi:format").expect("tombi:format");
776        assert_ne!(
777            tombi_lint.command, tombi_format.command,
778            "tombi:lint and tombi:format should be distinct"
779        );
780    }
781}