1use super::config::ToolDefinition;
7use super::processor::RUMDL_BUILTIN_TOOL;
8use std::collections::BTreeMap;
9use std::collections::HashMap;
10use std::sync::LazyLock;
11
12pub struct ToolRegistry {
14 user_tools: BTreeMap<String, ToolDefinition>,
16}
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq)]
20pub enum ToolSlot {
21 Lint,
23 Format,
25}
26
27impl ToolRegistry {
28 pub fn new(user_tools: BTreeMap<String, ToolDefinition>) -> Self {
30 Self { user_tools }
31 }
32
33 pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
37 self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
38 }
39
40 pub fn contains(&self, tool_id: &str) -> bool {
42 self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
43 }
44
45 pub fn lint_mode(&self, tool_id: &str) -> Option<BuiltinLintMode> {
50 if self.user_tools.contains_key(tool_id) {
51 return None;
52 }
53 builtin_lint_mode(tool_id)
54 }
55
56 pub(crate) fn native_format_check(&self, tool_id: &str) -> Option<ToolDefinition> {
59 if self.user_tools.contains_key(tool_id) {
60 return None;
61 }
62 let command = match tool_id {
63 "shuck:format-check" => vec!["shuck", "format", "-", "--check"],
64 "djlint:html:format-check" => vec!["djlint", "-", "--check", "--profile=html"],
65 "djlint:jinja:format-check" => vec!["djlint", "-", "--check", "--profile=jinja"],
66 _ => return None,
67 };
68 Some(ToolDefinition {
69 command: command.into_iter().map(str::to_string).collect(),
70 stdin: true,
71 stdout: true,
72 lint_args: Vec::new(),
73 format_args: Vec::new(),
74 })
75 }
76
77 pub fn resolve_id(&self, tool_id: &str, slot: ToolSlot) -> Option<String> {
85 if tool_id.contains(':') {
87 return self.contains(tool_id).then(|| tool_id.to_string());
88 }
89
90 let suffixes = match slot {
91 ToolSlot::Format => &["format", "fmt", "fix", "reformat"][..],
92 ToolSlot::Lint => &["lint", "check"][..],
93 };
94 for suffix in suffixes {
96 let qualified = format!("{tool_id}:{suffix}");
97 if self.user_tools.contains_key(&qualified) {
98 return Some(qualified);
99 }
100 }
101 if self.user_tools.contains_key(tool_id) {
102 return Some(tool_id.to_string());
103 }
104 for suffix in suffixes {
105 let qualified = format!("{tool_id}:{suffix}");
106 if self.contains(&qualified) {
107 return Some(qualified);
108 }
109 }
110
111 if self.contains(tool_id) {
112 return Some(tool_id.to_string());
113 }
114
115 if slot == ToolSlot::Lint {
119 for suffix in ["format", "fmt"] {
120 let qualified = format!("{tool_id}:{suffix}");
121 if self.contains(&qualified) {
122 return Some(qualified);
123 }
124 }
125 }
126
127 None
128 }
129
130 pub fn invalid_custom_commands(&self) -> Vec<(String, String)> {
132 self.user_tools
133 .iter()
134 .filter_map(|(id, definition)| Self::command_problem(definition).map(|problem| (id.clone(), problem)))
135 .collect()
136 }
137
138 fn command_problem(definition: &ToolDefinition) -> Option<String> {
139 if definition.command.first().is_none_or(|binary| binary.trim().is_empty()) {
140 Some("has an empty or unusable command".into())
141 } else if definition
142 .command
143 .iter()
144 .chain(&definition.lint_args)
145 .chain(&definition.format_args)
146 .any(|arg| arg.contains('\0'))
147 {
148 Some("has a command argument containing a NUL byte".into())
149 } else {
150 None
151 }
152 }
153
154 pub fn definition_problem(&self, tool_id: &str, slot: ToolSlot) -> Option<String> {
156 if super::processor::is_rumdl_builtin(tool_id) {
157 return (slot == ToolSlot::Format && tool_id == "rumdl:lint")
158 .then(|| "is a linter (move it to lint)".into());
159 }
160 let Some(definition) = self.resolve(tool_id, slot) else {
161 return Some("is an unknown tool".into());
162 };
163 if let Some(problem) = Self::command_problem(definition) {
164 return Some(problem);
165 }
166 if slot == ToolSlot::Format {
167 if self.fills_format_slot(tool_id) == Some(false) {
168 return Some("is a linter (move it to lint)".into());
169 }
170 if !definition.stdout {
171 return Some("cannot provide formatted output (stdout = false)".into());
172 }
173 }
174 None
175 }
176
177 pub fn resolve(&self, tool_id: &str, slot: ToolSlot) -> Option<&ToolDefinition> {
179 self.resolve_id(tool_id, slot).and_then(|id| self.get(&id))
180 }
181
182 pub fn fills_format_slot(&self, tool_id: &str) -> Option<bool> {
188 let resolved = self.resolve_id(tool_id, ToolSlot::Format)?;
189 Some(self.user_tools.contains_key(&resolved) || builtin_tool_formats(&resolved) == Some(true))
190 }
191
192 pub fn list_tools(&self) -> Vec<&str> {
194 let mut tools: Vec<&str> = self.user_tools.keys().map(std::string::String::as_str).collect();
195 for key in BUILTIN_TOOLS.keys() {
196 if !self.user_tools.contains_key(*key) {
197 tools.push(key);
198 }
199 }
200 tools.sort_unstable();
201 tools
202 }
203}
204
205pub fn builtin_tool_ids() -> Vec<&'static str> {
212 let mut ids: Vec<&'static str> = BUILTIN_TOOLS.keys().copied().collect();
213 ids.sort_unstable();
214 ids
215}
216
217impl Default for ToolRegistry {
218 fn default() -> Self {
219 Self::new(BTreeMap::new())
220 }
221}
222
223static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
227 let mut m = HashMap::new();
228
229 m.insert(
231 "ruff:check",
232 ToolDefinition {
233 command: vec![
234 "ruff".to_string(),
235 "check".to_string(),
236 "--output-format=concise".to_string(),
237 "--stdin-filename=_.py".to_string(),
238 "-".to_string(),
239 ],
240 stdin: true,
241 stdout: true,
242 lint_args: vec![],
243 format_args: vec![],
244 },
245 );
246
247 m.insert(
248 "ruff:format",
249 ToolDefinition {
250 command: vec![
251 "ruff".to_string(),
252 "format".to_string(),
253 "--stdin-filename=_.py".to_string(),
254 "-".to_string(),
255 ],
256 stdin: true,
257 stdout: true,
258 lint_args: vec![],
259 format_args: vec![],
260 },
261 );
262
263 m.insert(
265 "black",
266 ToolDefinition {
267 command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
268 stdin: true,
269 stdout: true,
270 lint_args: vec![],
271 format_args: vec![],
272 },
273 );
274
275 m.insert(
277 "prettier",
278 ToolDefinition {
279 command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
280 stdin: true,
281 stdout: true,
282 lint_args: vec![],
283 format_args: vec![],
284 },
285 );
286
287 m.insert(
288 "prettier:json",
289 ToolDefinition {
290 command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
291 stdin: true,
292 stdout: true,
293 lint_args: vec![],
294 format_args: vec![],
295 },
296 );
297
298 m.insert(
299 "prettier:yaml",
300 ToolDefinition {
301 command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
302 stdin: true,
303 stdout: true,
304 lint_args: vec![],
305 format_args: vec![],
306 },
307 );
308
309 m.insert(
310 "prettier:html",
311 ToolDefinition {
312 command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
313 stdin: true,
314 stdout: true,
315 lint_args: vec![],
316 format_args: vec![],
317 },
318 );
319
320 m.insert(
321 "prettier:css",
322 ToolDefinition {
323 command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
324 stdin: true,
325 stdout: true,
326 lint_args: vec![],
327 format_args: vec![],
328 },
329 );
330
331 m.insert(
332 "prettier:markdown",
333 ToolDefinition {
334 command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
335 stdin: true,
336 stdout: true,
337 lint_args: vec![],
338 format_args: vec![],
339 },
340 );
341
342 m.insert(
347 "shellcheck",
348 ToolDefinition {
349 command: vec!["shellcheck".to_string(), "--shell=bash".to_string(), "-".to_string()],
350 stdin: true,
351 stdout: true,
352 lint_args: vec![],
353 format_args: vec![],
354 },
355 );
356
357 m.insert(
359 "shfmt",
360 ToolDefinition {
361 command: vec!["shfmt".to_string()],
362 stdin: true,
363 stdout: true,
364 lint_args: vec![],
365 format_args: vec![],
366 },
367 );
368
369 m.insert(
378 "shuck",
379 ToolDefinition {
380 command: vec![
381 "shuck".to_string(),
382 "check".to_string(),
383 "--output-format".to_string(),
384 "concise".to_string(),
385 "-".to_string(),
386 ],
387 stdin: true,
388 stdout: true,
389 lint_args: vec![],
390 format_args: vec![],
391 },
392 );
393
394 m.insert(
395 "shuck:format",
396 ToolDefinition {
397 command: vec!["shuck".to_string(), "format".to_string(), "-".to_string()],
398 stdin: true,
399 stdout: true,
400 lint_args: vec![],
401 format_args: vec![],
402 },
403 );
404
405 m.insert(
407 "rustfmt",
408 ToolDefinition {
409 command: vec!["rustfmt".to_string()],
410 stdin: true,
411 stdout: true,
412 lint_args: vec![],
413 format_args: vec![],
414 },
415 );
416
417 m.insert(
419 "gofmt",
420 ToolDefinition {
421 command: vec!["gofmt".to_string()],
422 stdin: true,
423 stdout: true,
424 lint_args: vec![],
425 format_args: vec![],
426 },
427 );
428
429 m.insert(
431 "goimports",
432 ToolDefinition {
433 command: vec!["goimports".to_string()],
434 stdin: true,
435 stdout: true,
436 lint_args: vec![],
437 format_args: vec![],
438 },
439 );
440
441 m.insert(
443 "clang-format",
444 ToolDefinition {
445 command: vec!["clang-format".to_string()],
446 stdin: true,
447 stdout: true,
448 lint_args: vec![],
449 format_args: vec![],
450 },
451 );
452
453 m.insert(
459 "sqlfluff:lint",
460 ToolDefinition {
461 command: vec![
462 "sqlfluff".to_string(),
463 "lint".to_string(),
464 "--dialect".to_string(),
465 "ansi".to_string(),
466 "--format".to_string(),
467 "github-annotation-native".to_string(),
468 "-".to_string(),
469 ],
470 stdin: true,
471 stdout: true,
472 lint_args: vec![],
473 format_args: vec![],
474 },
475 );
476
477 m.insert(
478 "sqlfluff:fix",
479 ToolDefinition {
480 command: vec![
481 "sqlfluff".to_string(),
482 "fix".to_string(),
483 "--dialect".to_string(),
484 "ansi".to_string(),
485 "-".to_string(),
486 ],
487 stdin: true,
488 stdout: true,
489 lint_args: vec![],
490 format_args: vec![],
491 },
492 );
493
494 m.insert(
497 "jq:lint",
498 ToolDefinition {
499 command: vec!["jq".to_string(), "empty".to_string()],
500 stdin: true,
501 stdout: true,
502 lint_args: vec![],
503 format_args: vec![],
504 },
505 );
506 m.insert(
507 "jq",
508 ToolDefinition {
509 command: vec!["jq".to_string(), ".".to_string()],
510 stdin: true,
511 stdout: true,
512 lint_args: vec![],
513 format_args: vec![],
514 },
515 );
516
517 m.insert(
519 "yamlfmt",
520 ToolDefinition {
521 command: vec!["yamlfmt".to_string()],
522 stdin: true,
523 stdout: true,
524 lint_args: vec![],
525 format_args: vec!["-".to_string()],
526 },
527 );
528
529 m.insert(
531 "taplo",
532 ToolDefinition {
533 command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
534 stdin: true,
535 stdout: true,
536 lint_args: vec![],
537 format_args: vec![],
538 },
539 );
540
541 let terraform_fmt = || ToolDefinition {
546 command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
547 stdin: true,
548 stdout: true,
549 lint_args: vec![],
550 format_args: vec![],
551 };
552 m.insert("terraform:format", terraform_fmt());
553 m.insert("terraform-fmt", terraform_fmt());
554
555 m.insert(
557 "nixfmt",
558 ToolDefinition {
559 command: vec!["nixfmt".to_string(), "-".to_string()],
560 stdin: true,
561 stdout: true,
562 lint_args: vec![],
563 format_args: vec![],
564 },
565 );
566
567 m.insert(
569 "stylua",
570 ToolDefinition {
571 command: vec!["stylua".to_string(), "-".to_string()],
572 stdin: true,
573 stdout: true,
574 lint_args: vec![],
575 format_args: vec![],
576 },
577 );
578
579 m.insert(
581 "ormolu",
582 ToolDefinition {
583 command: vec!["ormolu".to_string(), "--stdin-input-file=_.hs".to_string()],
584 stdin: true,
585 stdout: true,
586 lint_args: vec![],
587 format_args: vec![],
588 },
589 );
590
591 m.insert(
593 "elm-format",
594 ToolDefinition {
595 command: vec!["elm-format".to_string(), "--stdin".to_string()],
596 stdin: true,
597 stdout: true,
598 lint_args: vec![],
599 format_args: vec![],
600 },
601 );
602
603 m.insert(
605 "swift-format",
606 ToolDefinition {
607 command: vec!["swift-format".to_string(), "format".to_string(), "-".to_string()],
608 stdin: true,
609 stdout: true,
610 lint_args: vec![],
611 format_args: vec![],
612 },
613 );
614
615 m.insert(
617 "ktfmt",
618 ToolDefinition {
619 command: vec!["ktfmt".to_string(), "-".to_string()],
620 stdin: true,
621 stdout: true,
622 lint_args: vec![],
623 format_args: vec![],
624 },
625 );
626
627 const DJLINT_OUTPUT_FORMAT: &str = "{filename}:{line}: {code} {message}";
635
636 m.insert(
637 "djlint",
638 ToolDefinition {
639 command: vec![
640 "djlint".to_string(),
641 "-".to_string(),
642 "--linter-output-format".to_string(),
643 DJLINT_OUTPUT_FORMAT.to_string(),
644 ],
645 stdin: true,
646 stdout: true,
647 lint_args: vec![],
648 format_args: vec!["--reformat".to_string()],
649 },
650 );
651
652 m.insert(
653 "djlint:lint",
654 ToolDefinition {
655 command: vec![
656 "djlint".to_string(),
657 "-".to_string(),
658 "--linter-output-format".to_string(),
659 DJLINT_OUTPUT_FORMAT.to_string(),
660 ],
661 stdin: true,
662 stdout: true,
663 lint_args: vec![],
664 format_args: vec![],
665 },
666 );
667
668 m.insert(
669 "djlint:reformat",
670 ToolDefinition {
671 command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
672 stdin: true,
673 stdout: true,
674 lint_args: vec![],
675 format_args: vec![],
676 },
677 );
678
679 m.insert(
681 "beautysh",
682 ToolDefinition {
683 command: vec!["beautysh".to_string(), "-".to_string()],
684 stdin: true,
685 stdout: true,
686 lint_args: vec![],
687 format_args: vec![],
688 },
689 );
690
691 m.insert(
693 "tombi",
694 ToolDefinition {
695 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
696 stdin: true,
697 stdout: true,
698 lint_args: vec![],
699 format_args: vec![],
700 },
701 );
702
703 m.insert(
704 "tombi:format",
705 ToolDefinition {
706 command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
707 stdin: true,
708 stdout: true,
709 lint_args: vec![],
710 format_args: vec![],
711 },
712 );
713
714 m.insert(
715 "tombi:lint",
716 ToolDefinition {
717 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
718 stdin: true,
719 stdout: true,
720 lint_args: vec![],
721 format_args: vec![],
722 },
723 );
724
725 m.insert(
727 "oxfmt",
728 ToolDefinition {
729 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
730 stdin: true,
731 stdout: true,
732 lint_args: vec![],
733 format_args: vec![],
734 },
735 );
736
737 m.insert(
738 "oxfmt:js",
739 ToolDefinition {
740 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
741 stdin: true,
742 stdout: true,
743 lint_args: vec![],
744 format_args: vec![],
745 },
746 );
747
748 m.insert(
749 "oxfmt:ts",
750 ToolDefinition {
751 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
752 stdin: true,
753 stdout: true,
754 lint_args: vec![],
755 format_args: vec![],
756 },
757 );
758
759 m.insert(
760 "oxfmt:jsx",
761 ToolDefinition {
762 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
763 stdin: true,
764 stdout: true,
765 lint_args: vec![],
766 format_args: vec![],
767 },
768 );
769
770 m.insert(
771 "oxfmt:tsx",
772 ToolDefinition {
773 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
774 stdin: true,
775 stdout: true,
776 lint_args: vec![],
777 format_args: vec![],
778 },
779 );
780
781 m.insert(
782 "oxfmt:json",
783 ToolDefinition {
784 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
785 stdin: true,
786 stdout: true,
787 lint_args: vec![],
788 format_args: vec![],
789 },
790 );
791
792 m.insert(
793 "oxfmt:css",
794 ToolDefinition {
795 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
796 stdin: true,
797 stdout: true,
798 lint_args: vec![],
799 format_args: vec![],
800 },
801 );
802
803 for (id, ext) in [
806 ("bun-fmt", "js"),
807 ("bun-fmt:js", "js"),
808 ("bun-fmt:ts", "ts"),
809 ("bun-fmt:jsx", "jsx"),
810 ("bun-fmt:tsx", "tsx"),
811 ("bun-fmt:json", "json"),
812 ("bun-fmt:jsonc", "jsonc"),
813 ("bun-fmt:json5", "json5"),
814 ("bun-fmt:css", "css"),
815 ("bun-fmt:scss", "scss"),
816 ("bun-fmt:less", "less"),
817 ("bun-fmt:html", "html"),
818 ("bun-fmt:vue", "vue"),
819 ("bun-fmt:yaml", "yaml"),
820 ("bun-fmt:toml", "toml"),
821 ("bun-fmt:graphql", "graphql"),
822 ("bun-fmt:markdown", "md"),
823 ("bun-fmt:mdx", "mdx"),
824 ] {
825 m.insert(
826 id,
827 ToolDefinition {
828 command: vec![
829 "bun".to_string(),
830 "fmt".to_string(),
831 format!("--stdin-filepath=_.{ext}"),
832 ],
833 stdin: true,
834 stdout: true,
835 lint_args: vec![],
836 format_args: vec![],
837 },
838 );
839 }
840
841 let deno_fmt = |ext: &str| ToolDefinition {
844 command: vec![
845 "deno".to_string(),
846 "fmt".to_string(),
847 format!("--ext={ext}"),
848 "-".to_string(),
849 ],
850 stdin: true,
851 stdout: true,
852 lint_args: vec![],
853 format_args: vec![],
854 };
855 m.insert("deno-fmt", deno_fmt("ts"));
856 m.insert("deno-fmt:ts", deno_fmt("ts"));
857 m.insert("deno-fmt:js", deno_fmt("js"));
858 m.insert("deno-fmt:json", deno_fmt("json"));
859 m.insert("deno-fmt:jsonc", deno_fmt("jsonc"));
860 m.insert("deno-fmt:md", deno_fmt("md"));
861
862 for (id, existing) in [
865 ("oxfmt:lint", "oxfmt"),
866 ("oxfmt:format", "oxfmt"),
867 ("shuck:lint", "shuck"),
868 ("shuck:format-check", "shuck:format"),
869 ] {
870 m.insert(id, m[existing].clone());
871 }
872 let mut shuck_fix = m["shuck"].clone();
873 shuck_fix.command.push("--fix".to_string());
874 m.insert("shuck:lint-fix", shuck_fix);
877
878 for (profile, lint_id, check_id, format_id) in [
879 (
880 "html",
881 "djlint:html:lint",
882 "djlint:html:format-check",
883 "djlint:html:format",
884 ),
885 (
886 "jinja",
887 "djlint:jinja:lint",
888 "djlint:jinja:format-check",
889 "djlint:jinja:format",
890 ),
891 ] {
892 let mut lint = m["djlint:lint"].clone();
893 lint.command.push(format!("--profile={profile}"));
894 m.insert(lint_id, lint);
895 let mut format = m["djlint:reformat"].clone();
896 format.command.push(format!("--profile={profile}"));
897 m.insert(check_id, format.clone());
898 m.insert(format_id, format);
899 }
900
901 m
902});
903
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906enum ToolKind {
907 Lint,
908 FormatCheck,
910 Format,
911 Both,
912}
913
914impl ToolKind {
915 const fn label(self) -> &'static str {
916 match self {
917 ToolKind::Lint | ToolKind::FormatCheck => "Lint",
918 ToolKind::Format => "Format",
919 ToolKind::Both => "Both",
920 }
921 }
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Eq)]
926pub enum BuiltinLintMode {
927 Diagnostics,
929 FormatCheck,
936}
937
938pub fn builtin_lint_mode(tool_id: &str) -> Option<BuiltinLintMode> {
940 BUILTIN_TOOLS_DOCS
941 .iter()
942 .find(|m| m.id == tool_id && m.runtime)
943 .map(|m| match m.kind {
944 ToolKind::Lint | ToolKind::Both => BuiltinLintMode::Diagnostics,
945 ToolKind::Format | ToolKind::FormatCheck => BuiltinLintMode::FormatCheck,
946 })
947}
948
949pub fn builtin_tool_formats(tool_id: &str) -> Option<bool> {
955 BUILTIN_TOOLS_DOCS
956 .iter()
957 .find(|m| m.id == tool_id && m.runtime)
958 .map(|m| matches!(m.kind, ToolKind::Format | ToolKind::Both))
959}
960
961struct ToolDocMeta {
975 id: &'static str,
976 language: &'static str,
977 kind: ToolKind,
978 doc_group: &'static str,
980 display_command: Option<&'static str>,
982 runtime: bool,
984}
985
986const BUILTIN_TOOLS_DOCS: &[ToolDocMeta] = &[
988 ToolDocMeta {
989 id: "ruff:check",
990 language: "Python",
991 kind: ToolKind::Lint,
992 doc_group: "ruff:check",
993 display_command: Some("ruff check --output-format=concise -"),
994 runtime: true,
995 },
996 ToolDocMeta {
997 id: "ruff:format",
998 language: "Python",
999 kind: ToolKind::Format,
1000 doc_group: "ruff:format",
1001 display_command: Some("ruff format -"),
1002 runtime: true,
1003 },
1004 ToolDocMeta {
1005 id: "black",
1006 language: "Python",
1007 kind: ToolKind::Format,
1008 doc_group: "black",
1009 display_command: None,
1010 runtime: true,
1011 },
1012 ToolDocMeta {
1013 id: "prettier",
1014 language: "Multi",
1015 kind: ToolKind::Format,
1016 doc_group: "prettier",
1017 display_command: Some("prettier --stdin-filepath=_.EXT"),
1018 runtime: true,
1019 },
1020 ToolDocMeta {
1021 id: "prettier:json",
1022 language: "Multi",
1023 kind: ToolKind::Format,
1024 doc_group: "prettier",
1025 display_command: Some("prettier --stdin-filepath=_.EXT"),
1026 runtime: true,
1027 },
1028 ToolDocMeta {
1029 id: "prettier:yaml",
1030 language: "Multi",
1031 kind: ToolKind::Format,
1032 doc_group: "prettier",
1033 display_command: Some("prettier --stdin-filepath=_.EXT"),
1034 runtime: true,
1035 },
1036 ToolDocMeta {
1037 id: "prettier:html",
1038 language: "Multi",
1039 kind: ToolKind::Format,
1040 doc_group: "prettier",
1041 display_command: Some("prettier --stdin-filepath=_.EXT"),
1042 runtime: true,
1043 },
1044 ToolDocMeta {
1045 id: "prettier:css",
1046 language: "Multi",
1047 kind: ToolKind::Format,
1048 doc_group: "prettier",
1049 display_command: Some("prettier --stdin-filepath=_.EXT"),
1050 runtime: true,
1051 },
1052 ToolDocMeta {
1053 id: "prettier:markdown",
1054 language: "Multi",
1055 kind: ToolKind::Format,
1056 doc_group: "prettier",
1057 display_command: Some("prettier --stdin-filepath=_.EXT"),
1058 runtime: true,
1059 },
1060 ToolDocMeta {
1061 id: "shellcheck",
1062 language: "Shell",
1063 kind: ToolKind::Lint,
1064 doc_group: "shellcheck",
1065 display_command: None,
1066 runtime: true,
1067 },
1068 ToolDocMeta {
1069 id: "shfmt",
1070 language: "Shell",
1071 kind: ToolKind::Format,
1072 doc_group: "shfmt",
1073 display_command: None,
1074 runtime: true,
1075 },
1076 ToolDocMeta {
1077 id: "shuck",
1078 language: "Shell",
1079 kind: ToolKind::Lint,
1080 doc_group: "shuck",
1081 display_command: None,
1082 runtime: true,
1083 },
1084 ToolDocMeta {
1085 id: "shuck:format",
1086 language: "Shell",
1087 kind: ToolKind::Format,
1088 doc_group: "shuck:format",
1089 display_command: None,
1090 runtime: true,
1091 },
1092 ToolDocMeta {
1093 id: "rustfmt",
1094 language: "Rust",
1095 kind: ToolKind::Format,
1096 doc_group: "rustfmt",
1097 display_command: None,
1098 runtime: true,
1099 },
1100 ToolDocMeta {
1101 id: "gofmt",
1102 language: "Go",
1103 kind: ToolKind::Format,
1104 doc_group: "gofmt",
1105 display_command: None,
1106 runtime: true,
1107 },
1108 ToolDocMeta {
1109 id: "goimports",
1110 language: "Go",
1111 kind: ToolKind::Format,
1112 doc_group: "goimports",
1113 display_command: None,
1114 runtime: true,
1115 },
1116 ToolDocMeta {
1117 id: "clang-format",
1118 language: "C/C++",
1119 kind: ToolKind::Format,
1120 doc_group: "clang-format",
1121 display_command: None,
1122 runtime: true,
1123 },
1124 ToolDocMeta {
1125 id: "sqlfluff:lint",
1126 language: "SQL",
1127 kind: ToolKind::Lint,
1128 doc_group: "sqlfluff:lint",
1129 display_command: None,
1130 runtime: true,
1131 },
1132 ToolDocMeta {
1133 id: "sqlfluff:fix",
1134 language: "SQL",
1135 kind: ToolKind::Format,
1136 doc_group: "sqlfluff:fix",
1137 display_command: None,
1138 runtime: true,
1139 },
1140 ToolDocMeta {
1141 id: "jq",
1142 language: "JSON",
1143 kind: ToolKind::Both,
1144 doc_group: "jq",
1145 display_command: Some("jq empty / jq ."),
1146 runtime: true,
1147 },
1148 ToolDocMeta {
1149 id: "jq:lint",
1150 language: "JSON",
1151 kind: ToolKind::Lint,
1152 doc_group: "jq:lint",
1153 display_command: None,
1154 runtime: true,
1155 },
1156 ToolDocMeta {
1157 id: "yamlfmt",
1158 language: "YAML",
1159 kind: ToolKind::Format,
1160 doc_group: "yamlfmt",
1161 display_command: None,
1162 runtime: true,
1163 },
1164 ToolDocMeta {
1165 id: "taplo",
1166 language: "TOML",
1167 kind: ToolKind::Format,
1168 doc_group: "taplo",
1169 display_command: None,
1170 runtime: true,
1171 },
1172 ToolDocMeta {
1173 id: "terraform:format",
1174 language: "Terraform",
1175 kind: ToolKind::Format,
1176 doc_group: "terraform:format",
1177 display_command: Some("terraform fmt -"),
1178 runtime: true,
1179 },
1180 ToolDocMeta {
1181 id: "terraform-fmt",
1182 language: "Terraform",
1183 kind: ToolKind::Format,
1184 doc_group: "terraform:format",
1185 display_command: Some("terraform fmt -"),
1186 runtime: true,
1187 },
1188 ToolDocMeta {
1189 id: "nixfmt",
1190 language: "Nix",
1191 kind: ToolKind::Format,
1192 doc_group: "nixfmt",
1193 display_command: None,
1194 runtime: true,
1195 },
1196 ToolDocMeta {
1197 id: "stylua",
1198 language: "Lua",
1199 kind: ToolKind::Format,
1200 doc_group: "stylua",
1201 display_command: None,
1202 runtime: true,
1203 },
1204 ToolDocMeta {
1205 id: "ormolu",
1206 language: "Haskell",
1207 kind: ToolKind::Format,
1208 doc_group: "ormolu",
1209 display_command: None,
1210 runtime: true,
1211 },
1212 ToolDocMeta {
1213 id: "elm-format",
1214 language: "Elm",
1215 kind: ToolKind::Format,
1216 doc_group: "elm-format",
1217 display_command: None,
1218 runtime: true,
1219 },
1220 ToolDocMeta {
1221 id: "swift-format",
1222 language: "Swift",
1223 kind: ToolKind::Format,
1224 doc_group: "swift-format",
1225 display_command: None,
1226 runtime: true,
1227 },
1228 ToolDocMeta {
1229 id: "ktfmt",
1230 language: "Kotlin",
1231 kind: ToolKind::Format,
1232 doc_group: "ktfmt",
1233 display_command: None,
1234 runtime: true,
1235 },
1236 ToolDocMeta {
1239 id: "djlint",
1240 language: "Jinja/HTML",
1241 kind: ToolKind::Both,
1242 doc_group: "djlint",
1243 display_command: Some("djlint - / djlint - --reformat"),
1244 runtime: true,
1245 },
1246 ToolDocMeta {
1247 id: "djlint:lint",
1248 language: "Jinja/HTML",
1249 kind: ToolKind::Lint,
1250 doc_group: "djlint:lint",
1251 display_command: Some("djlint -"),
1252 runtime: true,
1253 },
1254 ToolDocMeta {
1255 id: "djlint:reformat",
1256 language: "Jinja/HTML",
1257 kind: ToolKind::Format,
1258 doc_group: "djlint:reformat",
1259 display_command: None,
1260 runtime: true,
1261 },
1262 ToolDocMeta {
1263 id: "beautysh",
1264 language: "Shell",
1265 kind: ToolKind::Format,
1266 doc_group: "beautysh",
1267 display_command: None,
1268 runtime: true,
1269 },
1270 ToolDocMeta {
1271 id: "tombi",
1272 language: "TOML",
1273 kind: ToolKind::Lint,
1274 doc_group: "tombi",
1275 display_command: None,
1276 runtime: true,
1277 },
1278 ToolDocMeta {
1279 id: "tombi:format",
1280 language: "TOML",
1281 kind: ToolKind::Format,
1282 doc_group: "tombi:format",
1283 display_command: None,
1284 runtime: true,
1285 },
1286 ToolDocMeta {
1287 id: "tombi:lint",
1288 language: "TOML",
1289 kind: ToolKind::Lint,
1290 doc_group: "tombi:lint",
1291 display_command: None,
1292 runtime: true,
1293 },
1294 ToolDocMeta {
1295 id: "oxfmt",
1296 language: "Multi",
1297 kind: ToolKind::Format,
1298 doc_group: "oxfmt",
1299 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1300 runtime: true,
1301 },
1302 ToolDocMeta {
1303 id: "oxfmt:js",
1304 language: "Multi",
1305 kind: ToolKind::Format,
1306 doc_group: "oxfmt",
1307 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1308 runtime: true,
1309 },
1310 ToolDocMeta {
1311 id: "oxfmt:ts",
1312 language: "Multi",
1313 kind: ToolKind::Format,
1314 doc_group: "oxfmt",
1315 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1316 runtime: true,
1317 },
1318 ToolDocMeta {
1319 id: "oxfmt:jsx",
1320 language: "Multi",
1321 kind: ToolKind::Format,
1322 doc_group: "oxfmt",
1323 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1324 runtime: true,
1325 },
1326 ToolDocMeta {
1327 id: "oxfmt:tsx",
1328 language: "Multi",
1329 kind: ToolKind::Format,
1330 doc_group: "oxfmt",
1331 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1332 runtime: true,
1333 },
1334 ToolDocMeta {
1335 id: "oxfmt:json",
1336 language: "Multi",
1337 kind: ToolKind::Format,
1338 doc_group: "oxfmt",
1339 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1340 runtime: true,
1341 },
1342 ToolDocMeta {
1343 id: "oxfmt:css",
1344 language: "Multi",
1345 kind: ToolKind::Format,
1346 doc_group: "oxfmt",
1347 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1348 runtime: true,
1349 },
1350 ToolDocMeta {
1351 id: "bun-fmt",
1352 language: "Multi",
1353 kind: ToolKind::Format,
1354 doc_group: "bun-fmt",
1355 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1356 runtime: true,
1357 },
1358 ToolDocMeta {
1359 id: "bun-fmt:js",
1360 language: "Multi",
1361 kind: ToolKind::Format,
1362 doc_group: "bun-fmt",
1363 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1364 runtime: true,
1365 },
1366 ToolDocMeta {
1367 id: "bun-fmt:ts",
1368 language: "Multi",
1369 kind: ToolKind::Format,
1370 doc_group: "bun-fmt",
1371 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1372 runtime: true,
1373 },
1374 ToolDocMeta {
1375 id: "bun-fmt:jsx",
1376 language: "Multi",
1377 kind: ToolKind::Format,
1378 doc_group: "bun-fmt",
1379 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1380 runtime: true,
1381 },
1382 ToolDocMeta {
1383 id: "bun-fmt:tsx",
1384 language: "Multi",
1385 kind: ToolKind::Format,
1386 doc_group: "bun-fmt",
1387 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1388 runtime: true,
1389 },
1390 ToolDocMeta {
1391 id: "bun-fmt:json",
1392 language: "Multi",
1393 kind: ToolKind::Format,
1394 doc_group: "bun-fmt",
1395 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1396 runtime: true,
1397 },
1398 ToolDocMeta {
1399 id: "bun-fmt:jsonc",
1400 language: "Multi",
1401 kind: ToolKind::Format,
1402 doc_group: "bun-fmt",
1403 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1404 runtime: true,
1405 },
1406 ToolDocMeta {
1407 id: "bun-fmt:json5",
1408 language: "Multi",
1409 kind: ToolKind::Format,
1410 doc_group: "bun-fmt",
1411 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1412 runtime: true,
1413 },
1414 ToolDocMeta {
1415 id: "bun-fmt:css",
1416 language: "Multi",
1417 kind: ToolKind::Format,
1418 doc_group: "bun-fmt",
1419 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1420 runtime: true,
1421 },
1422 ToolDocMeta {
1423 id: "bun-fmt:scss",
1424 language: "Multi",
1425 kind: ToolKind::Format,
1426 doc_group: "bun-fmt",
1427 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1428 runtime: true,
1429 },
1430 ToolDocMeta {
1431 id: "bun-fmt:less",
1432 language: "Multi",
1433 kind: ToolKind::Format,
1434 doc_group: "bun-fmt",
1435 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1436 runtime: true,
1437 },
1438 ToolDocMeta {
1439 id: "bun-fmt:html",
1440 language: "Multi",
1441 kind: ToolKind::Format,
1442 doc_group: "bun-fmt",
1443 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1444 runtime: true,
1445 },
1446 ToolDocMeta {
1447 id: "bun-fmt:vue",
1448 language: "Multi",
1449 kind: ToolKind::Format,
1450 doc_group: "bun-fmt",
1451 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1452 runtime: true,
1453 },
1454 ToolDocMeta {
1455 id: "bun-fmt:yaml",
1456 language: "Multi",
1457 kind: ToolKind::Format,
1458 doc_group: "bun-fmt",
1459 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1460 runtime: true,
1461 },
1462 ToolDocMeta {
1463 id: "bun-fmt:toml",
1464 language: "Multi",
1465 kind: ToolKind::Format,
1466 doc_group: "bun-fmt",
1467 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1468 runtime: true,
1469 },
1470 ToolDocMeta {
1471 id: "bun-fmt:graphql",
1472 language: "Multi",
1473 kind: ToolKind::Format,
1474 doc_group: "bun-fmt",
1475 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1476 runtime: true,
1477 },
1478 ToolDocMeta {
1479 id: "bun-fmt:markdown",
1480 language: "Multi",
1481 kind: ToolKind::Format,
1482 doc_group: "bun-fmt",
1483 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1484 runtime: true,
1485 },
1486 ToolDocMeta {
1487 id: "bun-fmt:mdx",
1488 language: "Multi",
1489 kind: ToolKind::Format,
1490 doc_group: "bun-fmt",
1491 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1492 runtime: true,
1493 },
1494 ToolDocMeta {
1495 id: "deno-fmt",
1496 language: "Multi",
1497 kind: ToolKind::Format,
1498 doc_group: "deno-fmt",
1499 display_command: Some("deno fmt --ext=EXT -"),
1500 runtime: true,
1501 },
1502 ToolDocMeta {
1503 id: "deno-fmt:ts",
1504 language: "Multi",
1505 kind: ToolKind::Format,
1506 doc_group: "deno-fmt",
1507 display_command: Some("deno fmt --ext=EXT -"),
1508 runtime: true,
1509 },
1510 ToolDocMeta {
1511 id: "deno-fmt:js",
1512 language: "Multi",
1513 kind: ToolKind::Format,
1514 doc_group: "deno-fmt",
1515 display_command: Some("deno fmt --ext=EXT -"),
1516 runtime: true,
1517 },
1518 ToolDocMeta {
1519 id: "deno-fmt:json",
1520 language: "Multi",
1521 kind: ToolKind::Format,
1522 doc_group: "deno-fmt",
1523 display_command: Some("deno fmt --ext=EXT -"),
1524 runtime: true,
1525 },
1526 ToolDocMeta {
1527 id: "deno-fmt:jsonc",
1528 language: "Multi",
1529 kind: ToolKind::Format,
1530 doc_group: "deno-fmt",
1531 display_command: Some("deno fmt --ext=EXT -"),
1532 runtime: true,
1533 },
1534 ToolDocMeta {
1535 id: "deno-fmt:md",
1536 language: "Multi",
1537 kind: ToolKind::Format,
1538 doc_group: "deno-fmt",
1539 display_command: Some("deno fmt --ext=EXT -"),
1540 runtime: true,
1541 },
1542 ToolDocMeta {
1543 id: "shuck:lint",
1544 language: "Shell",
1545 kind: ToolKind::Lint,
1546 doc_group: "shuck:lint",
1547 display_command: None,
1548 runtime: true,
1549 },
1550 ToolDocMeta {
1551 id: "shuck:lint-fix",
1552 language: "Shell",
1553 kind: ToolKind::Format,
1554 doc_group: "shuck:lint-fix",
1555 display_command: None,
1556 runtime: true,
1557 },
1558 ToolDocMeta {
1559 id: "shuck:format-check",
1560 language: "Shell",
1561 kind: ToolKind::FormatCheck,
1562 doc_group: "shuck:format-check",
1563 display_command: Some("shuck format - --check"),
1564 runtime: true,
1565 },
1566 ToolDocMeta {
1567 id: "oxfmt:lint",
1568 language: "JavaScript",
1569 kind: ToolKind::FormatCheck,
1570 doc_group: "oxfmt:lint",
1571 display_command: None,
1572 runtime: true,
1573 },
1574 ToolDocMeta {
1575 id: "oxfmt:format",
1576 language: "JavaScript",
1577 kind: ToolKind::Format,
1578 doc_group: "oxfmt:format",
1579 display_command: None,
1580 runtime: true,
1581 },
1582 ToolDocMeta {
1583 id: "djlint:html:lint",
1584 language: "HTML",
1585 kind: ToolKind::Lint,
1586 doc_group: "djlint:html:lint",
1587 display_command: Some("djlint - --profile=html"),
1588 runtime: true,
1589 },
1590 ToolDocMeta {
1591 id: "djlint:html:format-check",
1592 language: "HTML",
1593 kind: ToolKind::FormatCheck,
1594 doc_group: "djlint:html:format-check",
1595 display_command: Some("djlint - --check --profile=html"),
1596 runtime: true,
1597 },
1598 ToolDocMeta {
1599 id: "djlint:html:format",
1600 language: "HTML",
1601 kind: ToolKind::Format,
1602 doc_group: "djlint:html:format",
1603 display_command: Some("djlint - --reformat --profile=html"),
1604 runtime: true,
1605 },
1606 ToolDocMeta {
1607 id: "djlint:jinja:lint",
1608 language: "Jinja",
1609 kind: ToolKind::Lint,
1610 doc_group: "djlint:jinja:lint",
1611 display_command: Some("djlint - --profile=jinja"),
1612 runtime: true,
1613 },
1614 ToolDocMeta {
1615 id: "djlint:jinja:format-check",
1616 language: "Jinja",
1617 kind: ToolKind::FormatCheck,
1618 doc_group: "djlint:jinja:format-check",
1619 display_command: Some("djlint - --check --profile=jinja"),
1620 runtime: true,
1621 },
1622 ToolDocMeta {
1623 id: "djlint:jinja:format",
1624 language: "Jinja",
1625 kind: ToolKind::Format,
1626 doc_group: "djlint:jinja:format",
1627 display_command: Some("djlint - --reformat --profile=jinja"),
1628 runtime: true,
1629 },
1630 ToolDocMeta {
1631 id: "rumdl:lint",
1632 language: "Markdown",
1633 kind: ToolKind::Lint,
1634 doc_group: "rumdl:lint",
1635 display_command: Some("built-in markdown linting"),
1636 runtime: false,
1637 },
1638 ToolDocMeta {
1639 id: "rumdl:format",
1640 language: "Markdown",
1641 kind: ToolKind::Format,
1642 doc_group: "rumdl:format",
1643 display_command: Some("built-in markdown formatting"),
1644 runtime: false,
1645 },
1646 ToolDocMeta {
1649 id: RUMDL_BUILTIN_TOOL,
1650 language: "Markdown",
1651 kind: ToolKind::Lint,
1652 doc_group: RUMDL_BUILTIN_TOOL,
1653 display_command: Some("built-in markdown linting"),
1654 runtime: false,
1655 },
1656];
1657
1658const TABLE_BEGIN: &str = "<!-- BEGIN builtin-tools (generated) -->";
1660const TABLE_END: &str = "<!-- END builtin-tools (generated) -->";
1661
1662#[derive(Debug, Clone, PartialEq, Eq)]
1664pub enum DocsError {
1665 MissingMarker,
1667 DuplicateMarker,
1669 MarkerOrder,
1671 CountRowMissing,
1673 CountRowAmbiguous,
1675 CountRowMalformed,
1677}
1678
1679impl std::fmt::Display for DocsError {
1680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1681 let msg = match self {
1682 DocsError::MissingMarker => "missing `<!-- BEGIN/END builtin-tools (generated) -->` marker pair",
1683 DocsError::DuplicateMarker => "duplicate builtin-tools marker",
1684 DocsError::MarkerOrder => "END builtin-tools marker precedes BEGIN",
1685 DocsError::CountRowMissing => "`| Built-in tools` count row not found",
1686 DocsError::CountRowAmbiguous => "multiple `| Built-in tools` count rows found",
1687 DocsError::CountRowMalformed => "`| Built-in tools` count row has an unexpected layout",
1688 };
1689 f.write_str(msg)
1690 }
1691}
1692
1693impl std::error::Error for DocsError {}
1694
1695fn builtin_tools_group_count() -> usize {
1697 let mut seen: Vec<&str> = Vec::new();
1698 for m in BUILTIN_TOOLS_DOCS {
1699 if !seen.contains(&m.doc_group) {
1700 seen.push(m.doc_group);
1701 }
1702 }
1703 seen.len()
1704}
1705
1706pub fn render_builtin_tools_table() -> String {
1712 let headers = ["Tool ID", "Language", "Type", "Command"];
1713 let mut rows: Vec<[String; 4]> = Vec::new();
1714
1715 let mut seen: Vec<&str> = Vec::new();
1716 for m in BUILTIN_TOOLS_DOCS {
1717 if seen.contains(&m.doc_group) {
1718 continue;
1719 }
1720 seen.push(m.doc_group);
1721
1722 let command = match m.display_command {
1725 Some(cmd) => cmd.to_string(),
1726 None if m.runtime => runtime_command_for_kind(m.id, m.kind),
1727 None => String::new(),
1728 };
1729
1730 rows.push([
1731 format!("`{}`", m.doc_group),
1732 m.language.to_string(),
1733 m.kind.label().to_string(),
1734 format!("`{command}`"),
1735 ]);
1736 }
1737
1738 let mut widths = headers.map(str::chars).map(Iterator::count);
1740 for row in &rows {
1741 for (i, cell) in row.iter().enumerate() {
1742 widths[i] = widths[i].max(cell.chars().count());
1743 }
1744 }
1745
1746 let mut out = String::new();
1747 push_table_row(&mut out, &headers.map(String::from), &widths);
1748 let separators = std::array::from_fn(|i| "-".repeat(widths[i]));
1749 push_table_row(&mut out, &separators, &widths);
1750 for row in &rows {
1751 push_table_row(&mut out, row, &widths);
1752 }
1753 out
1754}
1755
1756fn runtime_command_for_kind(id: &str, kind: ToolKind) -> String {
1761 let Some(def) = BUILTIN_TOOLS.get(id) else {
1762 return String::new();
1763 };
1764 let invocation =
1765 |extra: &[String]| -> String { def.command.iter().chain(extra).cloned().collect::<Vec<_>>().join(" ") };
1766 match kind {
1767 ToolKind::Lint => invocation(&def.lint_args),
1768 ToolKind::Format | ToolKind::FormatCheck => invocation(&def.format_args),
1769 ToolKind::Both => {
1770 let lint = invocation(&def.lint_args);
1771 let format = invocation(&def.format_args);
1772 if lint == format {
1773 lint
1774 } else {
1775 format!("{lint} / {format}")
1776 }
1777 }
1778 }
1779}
1780
1781fn push_table_row(out: &mut String, cells: &[String; 4], widths: &[usize; 4]) {
1783 out.push('|');
1784 for (i, cell) in cells.iter().enumerate() {
1785 out.push(' ');
1786 out.push_str(cell);
1787 for _ in cell.chars().count()..widths[i] {
1788 out.push(' ');
1789 }
1790 out.push_str(" |");
1791 }
1792 out.push('\n');
1793}
1794
1795pub fn splice_builtin_tools_docs(existing: &str) -> Result<String, DocsError> {
1801 if existing.matches(TABLE_BEGIN).count() == 0 || existing.matches(TABLE_END).count() == 0 {
1802 return Err(DocsError::MissingMarker);
1803 }
1804 if existing.matches(TABLE_BEGIN).count() > 1 || existing.matches(TABLE_END).count() > 1 {
1805 return Err(DocsError::DuplicateMarker);
1806 }
1807 let begin_pos = existing.find(TABLE_BEGIN).unwrap();
1808 let end_pos = existing.find(TABLE_END).unwrap();
1809 if end_pos < begin_pos {
1810 return Err(DocsError::MarkerOrder);
1811 }
1812
1813 let table = render_builtin_tools_table();
1814 let replacement = format!("{TABLE_BEGIN}\n\n{table}\n{TABLE_END}");
1815
1816 let mut result = String::with_capacity(existing.len() + replacement.len());
1817 result.push_str(&existing[..begin_pos]);
1818 result.push_str(&replacement);
1819 result.push_str(&existing[end_pos + TABLE_END.len()..]);
1820
1821 update_builtin_tools_count(&result, builtin_tools_group_count())
1822}
1823
1824fn update_builtin_tools_count(text: &str, count: usize) -> Result<String, DocsError> {
1827 let lines: Vec<&str> = text.lines().collect();
1828 let mut target: Option<usize> = None;
1829 for (i, line) in lines.iter().enumerate() {
1830 if line.trim_start().starts_with("| Built-in tools") {
1831 if target.is_some() {
1832 return Err(DocsError::CountRowAmbiguous);
1833 }
1834 target = Some(i);
1835 }
1836 }
1837 let i = target.ok_or(DocsError::CountRowMissing)?;
1838 let new_line = replace_count_cell(lines[i], count).ok_or(DocsError::CountRowMalformed)?;
1839
1840 let mut out = String::with_capacity(text.len());
1841 for (j, line) in lines.iter().enumerate() {
1842 out.push_str(if j == i { &new_line } else { line });
1843 out.push('\n');
1844 }
1845 if !text.ends_with('\n') {
1846 out.pop();
1847 }
1848 Ok(out)
1849}
1850
1851fn replace_count_cell(line: &str, count: usize) -> Option<String> {
1853 let pipes: Vec<usize> = line.match_indices('|').map(|(i, _)| i).collect();
1854 if pipes.len() < 3 {
1855 return None;
1856 }
1857 let start = pipes[1] + 1;
1858 let end = pipes[2];
1859 let cell = line.get(start..end)?;
1860 let width = cell.len();
1861 let num = count.to_string();
1862
1863 let mut new_cell = String::with_capacity(width.max(num.len() + 2));
1864 new_cell.push(' ');
1865 new_cell.push_str(&num);
1866 while new_cell.len() < width {
1867 new_cell.push(' ');
1868 }
1869 if new_cell.len() > width {
1870 new_cell = format!(" {num} ");
1872 }
1873
1874 Some(format!("{}{}{}", &line[..start], new_cell, &line[end..]))
1875}
1876
1877#[cfg(test)]
1878mod tests {
1879 use super::*;
1880
1881 #[test]
1882 fn test_get_builtin_tool() {
1883 let registry = ToolRegistry::default();
1884
1885 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1886 assert!(tool.command.contains(&"ruff".to_string()));
1887 assert!(tool.stdin);
1888 assert!(tool.stdout);
1889
1890 let tool = registry.get("shellcheck").expect("Should find shellcheck");
1891 assert!(tool.command.contains(&"shellcheck".to_string()));
1892 }
1893
1894 #[test]
1895 fn test_builtin_yamlfmt_answers_lint_by_formatting() {
1896 let registry = ToolRegistry::default();
1897
1898 assert_eq!(registry.lint_mode("yamlfmt"), Some(BuiltinLintMode::FormatCheck));
1899
1900 let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
1902 let mut argv = tool.command.clone();
1903 argv.extend(tool.format_args.clone());
1904
1905 assert_eq!(argv, vec!["yamlfmt", "-"]);
1906 assert!(tool.lint_args.is_empty());
1907 }
1908
1909 #[test]
1910 fn test_get_user_tool_overrides_builtin() {
1911 let mut user_tools = BTreeMap::new();
1912 user_tools.insert(
1913 "ruff:check".to_string(),
1914 ToolDefinition {
1915 command: vec!["custom-ruff".to_string()],
1916 stdin: false,
1917 stdout: false,
1918 lint_args: vec![],
1919 format_args: vec![],
1920 },
1921 );
1922
1923 let registry = ToolRegistry::new(user_tools);
1924
1925 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1926 assert_eq!(tool.command, vec!["custom-ruff"]);
1927 assert!(!tool.stdin); }
1929
1930 #[test]
1931 fn bun_formatter_variants_select_the_stdin_parser_and_check_by_comparison() {
1932 let registry = ToolRegistry::default();
1933 for (id, ext) in [
1934 ("bun-fmt", "js"),
1935 ("bun-fmt:ts", "ts"),
1936 ("bun-fmt:json", "json"),
1937 ("bun-fmt:mdx", "mdx"),
1938 ] {
1939 let tool = registry.get(id).unwrap();
1940 assert_eq!(tool.command, vec!["bun", "fmt", &format!("--stdin-filepath=_.{ext}")]);
1941 assert!(tool.stdin && tool.stdout);
1942 assert_eq!(registry.lint_mode(id), Some(BuiltinLintMode::FormatCheck));
1943 assert_eq!(builtin_tool_formats(id), Some(true));
1944 assert_eq!(registry.resolve_id(id, ToolSlot::Format).as_deref(), Some(id));
1945 assert_eq!(registry.resolve_id(id, ToolSlot::Lint).as_deref(), Some(id));
1946 }
1947 }
1948
1949 #[test]
1950 fn test_contains() {
1951 let registry = ToolRegistry::default();
1952
1953 assert!(registry.contains("ruff:check"));
1954 assert!(registry.contains("prettier"));
1955 assert!(registry.contains("shellcheck"));
1956 assert!(!registry.contains("nonexistent-tool"));
1957 }
1958
1959 #[test]
1960 fn test_list_tools() {
1961 let registry = ToolRegistry::default();
1962 let tools = registry.list_tools();
1963
1964 assert!(tools.contains(&"ruff:check"));
1965 assert!(tools.contains(&"ruff:format"));
1966 assert!(tools.contains(&"prettier"));
1967 assert!(tools.contains(&"shellcheck"));
1968 assert!(tools.contains(&"shfmt"));
1969 assert!(tools.contains(&"rustfmt"));
1970 assert!(tools.contains(&"gofmt"));
1971 }
1972
1973 #[test]
1974 fn test_user_tools_in_list() {
1975 let mut user_tools = BTreeMap::new();
1976 user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
1977
1978 let registry = ToolRegistry::new(user_tools);
1979 let tools = registry.list_tools();
1980
1981 assert!(tools.contains(&"my-custom-tool"));
1982 assert!(tools.contains(&"ruff:check")); }
1984
1985 #[test]
1986 fn test_new_builtin_tools() {
1987 let registry = ToolRegistry::default();
1988
1989 let tool = registry.get("djlint").expect("Should find djlint");
1991 assert!(tool.command.contains(&"djlint".to_string()));
1992 assert!(tool.stdin);
1993
1994 let tool = registry.get("beautysh").expect("Should find beautysh");
1996 assert!(tool.command.contains(&"beautysh".to_string()));
1997 assert!(tool.stdin);
1998
1999 let tool = registry.get("tombi").expect("Should find tombi");
2001 assert!(tool.command.contains(&"tombi".to_string()));
2002 assert!(tool.stdin);
2003
2004 let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
2005 assert!(tool.command.contains(&"lint".to_string()));
2006
2007 let tool = registry.get("tombi:format").expect("Should find tombi:format");
2008 assert!(
2009 tool.command.contains(&"format".to_string()),
2010 "tombi:format should use 'format' subcommand, got: {:?}",
2011 tool.command
2012 );
2013
2014 let tool = registry.get("oxfmt").expect("Should find oxfmt");
2016 assert!(tool.command.contains(&"oxfmt".to_string()));
2017 assert!(tool.stdin);
2018
2019 let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
2020 assert!(tool.command.iter().any(|s| s.contains("_.ts")));
2021 }
2022
2023 #[test]
2031 fn test_bare_tombi_resolves_to_lint_not_format() {
2032 let registry = ToolRegistry::default();
2033
2034 let bare = registry.get("tombi").expect("Should find bare tombi");
2035 let format = registry.get("tombi:format").expect("Should find tombi:format");
2036
2037 assert!(
2039 bare.command.contains(&"lint".to_string()),
2040 "Bare 'tombi' uses lint subcommand: {:?}",
2041 bare.command
2042 );
2043
2044 assert!(
2046 format.command.contains(&"format".to_string()),
2047 "tombi:format uses format subcommand: {:?}",
2048 format.command
2049 );
2050
2051 assert_ne!(
2053 bare.command, format.command,
2054 "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
2055 );
2056 }
2057
2058 #[test]
2061 fn test_tools_with_lint_format_variants_are_distinct() {
2062 let registry = ToolRegistry::default();
2063
2064 let ruff_check = registry.get("ruff:check").expect("ruff:check");
2066 let ruff_format = registry.get("ruff:format").expect("ruff:format");
2067 assert_ne!(
2068 ruff_check.command, ruff_format.command,
2069 "ruff:check and ruff:format should be distinct"
2070 );
2071
2072 let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
2074 let tombi_format = registry.get("tombi:format").expect("tombi:format");
2075 assert_ne!(
2076 tombi_lint.command, tombi_format.command,
2077 "tombi:lint and tombi:format should be distinct"
2078 );
2079 }
2080
2081 #[test]
2082 fn test_deno_fmt_has_per_extension_variants() {
2083 let registry = ToolRegistry::default();
2086
2087 let deno_json = registry.get("deno-fmt:json").expect("deno-fmt:json");
2088 assert!(deno_json.command.iter().any(|a| a == "--ext=json"));
2089 let deno_md = registry.get("deno-fmt:md").expect("deno-fmt:md");
2090 assert!(deno_md.command.iter().any(|a| a == "--ext=md"));
2091
2092 let deno = registry.get("deno-fmt").expect("deno-fmt");
2094 assert!(deno.command.iter().any(|a| a == "--ext=ts"));
2095 }
2096
2097 #[test]
2098 fn jq_resolves_lint_and_format_separately_and_preserves_overrides() {
2099 let registry = ToolRegistry::default();
2100 assert_eq!(registry.resolve_id("jq", ToolSlot::Lint).as_deref(), Some("jq:lint"));
2101 assert_eq!(registry.resolve_id("jq", ToolSlot::Format).as_deref(), Some("jq"));
2102 assert_eq!(registry.fills_format_slot("jq:lint"), Some(false));
2103
2104 let mut user_tools = BTreeMap::new();
2106 user_tools.insert("jq".to_string(), registry.get("jq").unwrap().clone());
2107 let custom = ToolRegistry::new(user_tools);
2108 assert_eq!(custom.resolve_id("jq", ToolSlot::Lint).as_deref(), Some("jq"));
2109 assert_eq!(custom.resolve_id("jq", ToolSlot::Format).as_deref(), Some("jq"));
2110 }
2111
2112 #[test]
2121 fn no_builtin_declares_lint_args() {
2122 let offenders: Vec<&str> = BUILTIN_TOOLS
2123 .iter()
2124 .filter(|(_, def)| !def.lint_args.is_empty())
2125 .map(|(id, _)| *id)
2126 .collect();
2127
2128 assert!(
2129 offenders.is_empty(),
2130 "built-in tools must not declare lint_args (a formatter is lint-checked by \
2131 formatting and comparing; a linter's args belong in `command`): {offenders:?}"
2132 );
2133 }
2134
2135 #[test]
2139 fn builtin_lint_mode_follows_documented_kind() {
2140 let registry = ToolRegistry::default();
2141
2142 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2143 let def = registry.get(meta.id).expect("runtime metadata matches the registry");
2144 let mode = registry.lint_mode(meta.id);
2145
2146 match meta.kind {
2147 ToolKind::Format | ToolKind::FormatCheck => {
2148 assert_eq!(
2149 mode,
2150 Some(BuiltinLintMode::FormatCheck),
2151 "{} is documented as a formatter",
2152 meta.id
2153 );
2154 assert!(
2155 def.stdin && def.stdout,
2156 "{} is lint-checked by comparing its output, so it must read stdin \
2157 and write stdout",
2158 meta.id
2159 );
2160 }
2161 ToolKind::Lint | ToolKind::Both => {
2162 assert_eq!(
2163 mode,
2164 Some(BuiltinLintMode::Diagnostics),
2165 "{} is documented as a linter",
2166 meta.id
2167 );
2168 }
2169 }
2170 }
2171 }
2172
2173 #[test]
2183 fn format_slot_never_resolves_to_a_linter() {
2184 let registry = ToolRegistry::default();
2185 let documented_kind = |id: &str| {
2186 BUILTIN_TOOLS_DOCS
2187 .iter()
2188 .find(|m| m.id == id && m.runtime)
2189 .map(|m| m.kind)
2190 };
2191
2192 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2193 let fills = registry.fills_format_slot(meta.id);
2194 let resolved = registry.resolve_id(meta.id, ToolSlot::Format);
2195
2196 match fills {
2197 Some(true) => {
2198 let resolved = resolved.expect("a tool that fills the slot resolves in it");
2199 assert!(
2200 !matches!(documented_kind(&resolved), Some(ToolKind::Lint | ToolKind::FormatCheck)),
2201 "{} fills a format slot by running {resolved}, which only lints",
2202 meta.id
2203 );
2204 }
2205 Some(false) => assert!(
2206 matches!(documented_kind(meta.id), Some(ToolKind::Lint | ToolKind::FormatCheck)),
2207 "{} declines the format slot, so it must be documented as a linter",
2208 meta.id
2209 ),
2210 None => panic!("{} is in the registry, so it resolves somewhere", meta.id),
2211 }
2212 }
2213
2214 assert_eq!(registry.fills_format_slot("ruff:check"), Some(false));
2217 assert_eq!(registry.fills_format_slot("shellcheck"), Some(false));
2218 assert_eq!(registry.fills_format_slot("sqlfluff:lint"), Some(false));
2219 assert_eq!(registry.fills_format_slot("tombi"), Some(true));
2220 assert_eq!(
2221 registry.resolve_id("tombi", ToolSlot::Format).as_deref(),
2222 Some("tombi:format")
2223 );
2224 }
2225
2226 #[test]
2228 fn user_tool_is_never_lint_checked_by_formatting() {
2229 let mut user_tools = BTreeMap::new();
2230 user_tools.insert(
2231 "yamlfmt".to_string(),
2232 ToolDefinition {
2233 command: vec!["my-yaml-linter".to_string()],
2234 stdin: true,
2235 stdout: true,
2236 lint_args: vec!["--check".to_string()],
2237 format_args: vec![],
2238 },
2239 );
2240 let registry = ToolRegistry::new(user_tools);
2241
2242 assert_eq!(registry.lint_mode("yamlfmt"), None);
2243 assert_eq!(
2245 ToolRegistry::default().lint_mode("yamlfmt"),
2246 Some(BuiltinLintMode::FormatCheck)
2247 );
2248 }
2249
2250 #[test]
2255 fn explicit_mode_aliases_preserve_custom_bare_definitions() {
2256 for id in ["oxfmt", "shuck"] {
2257 let custom = ToolDefinition {
2258 command: vec!["custom-tool".to_string()],
2259 stdin: true,
2260 stdout: true,
2261 lint_args: vec![],
2262 format_args: vec![],
2263 };
2264 let registry = ToolRegistry::new(BTreeMap::from([(id.to_string(), custom.clone())]));
2265 for slot in [ToolSlot::Lint, ToolSlot::Format] {
2266 assert_eq!(registry.resolve(id, slot), Some(&custom));
2267 }
2268 }
2269 }
2270
2271 #[test]
2272 fn explicit_check_variants_compare_output_but_cannot_format() {
2273 let registry = ToolRegistry::default();
2274 for id in [
2275 "oxfmt:lint",
2276 "djlint:html:format-check",
2277 "djlint:jinja:format-check",
2278 "shuck:format-check",
2279 ] {
2280 assert_eq!(registry.lint_mode(id), Some(BuiltinLintMode::FormatCheck));
2281 assert_eq!(registry.fills_format_slot(id), Some(false));
2282 }
2283 for profile in ["html", "jinja"] {
2284 for mode in ["lint", "format-check", "format"] {
2285 let id = format!("djlint:{profile}:{mode}");
2286 assert!(
2287 registry
2288 .get(&id)
2289 .unwrap()
2290 .command
2291 .contains(&format!("--profile={profile}"))
2292 );
2293 }
2294 }
2295 }
2296
2297 #[test]
2298 fn test_docs_metadata_ids_unique() {
2299 let mut seen = std::collections::BTreeSet::new();
2300 for m in BUILTIN_TOOLS_DOCS {
2301 assert!(seen.insert(m.id), "duplicate metadata id: {}", m.id);
2302 }
2303 }
2304
2305 #[test]
2306 fn test_runtime_metadata_matches_registry_keys() {
2307 let runtime_meta: std::collections::BTreeSet<&str> =
2308 BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime).map(|m| m.id).collect();
2309 let map_keys: std::collections::BTreeSet<&str> = BUILTIN_TOOLS.keys().copied().collect();
2310 assert_eq!(
2311 runtime_meta, map_keys,
2312 "BUILTIN_TOOLS_DOCS runtime ids must exactly match BUILTIN_TOOLS keys (add/remove the doc entry alongside the registry entry)"
2313 );
2314 }
2315
2316 #[test]
2317 fn test_docs_only_ids_absent_from_registry() {
2318 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
2319 assert!(
2320 !BUILTIN_TOOLS.contains_key(m.id),
2321 "docs-only id {} must not be a runtime registry tool",
2322 m.id
2323 );
2324 }
2325 assert!(
2327 BUILTIN_TOOLS_DOCS
2328 .iter()
2329 .any(|m| !m.runtime && m.id == RUMDL_BUILTIN_TOOL),
2330 "rumdl must be present as a docs-only entry"
2331 );
2332 }
2333
2334 #[test]
2335 fn test_docs_only_requires_display_command() {
2336 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
2337 assert!(
2338 m.display_command.is_some(),
2339 "docs-only id {} needs a display_command (no runtime command to derive)",
2340 m.id
2341 );
2342 }
2343 }
2344
2345 #[test]
2346 fn test_multi_runtime_group_requires_display_command() {
2347 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
2348 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2349 *counts.entry(m.doc_group).or_default() += 1;
2350 }
2351 for m in BUILTIN_TOOLS_DOCS {
2352 if counts.get(m.doc_group).copied().unwrap_or(0) > 1 {
2353 assert!(
2354 m.display_command.is_some(),
2355 "doc_group `{}` has multiple runtime entries; `{}` needs an explicit display_command",
2356 m.doc_group,
2357 m.id
2358 );
2359 }
2360 }
2361 }
2362
2363 #[test]
2364 fn test_doc_group_language_and_kind_consistent() {
2365 let mut groups: std::collections::BTreeMap<&str, (&str, ToolKind)> = std::collections::BTreeMap::new();
2366 for m in BUILTIN_TOOLS_DOCS {
2367 match groups.get(m.doc_group) {
2368 None => {
2369 groups.insert(m.doc_group, (m.language, m.kind));
2370 }
2371 Some((lang, kind)) => {
2372 assert_eq!(
2373 *lang, m.language,
2374 "doc_group `{}` has mismatched languages",
2375 m.doc_group
2376 );
2377 assert_eq!(*kind, m.kind, "doc_group `{}` has mismatched kinds", m.doc_group);
2378 }
2379 }
2380 }
2381 }
2382
2383 fn rendered_rows(table: &str) -> Vec<(String, String, String, String)> {
2390 table
2391 .lines()
2392 .skip(2) .filter(|l| l.starts_with('|'))
2394 .map(|l| {
2395 let cells: Vec<String> = l.trim().trim_matches('|').split('|').map(|c| c.trim().to_string()).collect();
2396 (cells[0].clone(), cells[1].clone(), cells[2].clone(), cells[3].clone())
2397 })
2398 .collect()
2399 }
2400
2401 #[test]
2402 fn test_render_table_golden_rows() {
2403 let table = render_builtin_tools_table();
2404 let rows = rendered_rows(&table);
2405 let has = |id: &str, lang: &str, kind: &str, cmd: &str| {
2406 rows.iter()
2407 .any(|(i, l, k, c)| i == id && l == lang && k == kind && c == cmd)
2408 };
2409 assert!(has("`shellcheck`", "Shell", "Lint", "`shellcheck --shell=bash -`"));
2411 assert!(has("`rustfmt`", "Rust", "Format", "`rustfmt`"));
2412 assert!(has("`jq`", "JSON", "Both", "`jq empty / jq .`"));
2413 assert!(has(
2414 "`prettier`",
2415 "Multi",
2416 "Format",
2417 "`prettier --stdin-filepath=_.EXT`"
2418 ));
2419 assert!(has("`rumdl`", "Markdown", "Lint", "`built-in markdown linting`"));
2420 assert!(
2422 !table.contains("prettier:json"),
2423 "prettier variants must collapse into one row"
2424 );
2425 assert!(!table.contains("oxfmt:ts"), "oxfmt variants must collapse into one row");
2426 }
2427
2428 #[test]
2429 fn test_render_table_one_row_per_group() {
2430 let table = render_builtin_tools_table();
2431 assert_eq!(rendered_rows(&table).len(), builtin_tools_group_count());
2432 }
2433
2434 #[test]
2435 fn test_render_table_command_fallback_from_registry() {
2436 let table = render_builtin_tools_table();
2438 assert!(
2439 rendered_rows(&table)
2440 .iter()
2441 .any(|(i, _, _, c)| i == "`tombi`" && c == "`tombi lint -`")
2442 );
2443 }
2444
2445 #[test]
2446 fn test_render_command_reflects_mode_args() {
2447 let table = render_builtin_tools_table();
2448 let rows = rendered_rows(&table);
2449 let cmd = |id: &str| {
2450 let row = rows
2451 .iter()
2452 .find(|(i, _, _, _)| i == id)
2453 .unwrap_or_else(|| panic!("row {id} not found"));
2454 row.3.clone()
2455 };
2456 assert_eq!(cmd("`yamlfmt`"), "`yamlfmt -`");
2458 assert_eq!(
2460 cmd("`sqlfluff:lint`"),
2461 "`sqlfluff lint --dialect ansi --format github-annotation-native -`"
2462 );
2463 }
2464
2465 #[test]
2466 fn test_runtime_command_for_kind_both_collapses_when_equal() {
2467 assert_eq!(runtime_command_for_kind("jq", ToolKind::Both), "jq .");
2470 }
2471
2472 #[test]
2476 fn test_runtime_command_for_kind_both_shows_both_invocations() {
2477 let rendered = runtime_command_for_kind("djlint", ToolKind::Both);
2478 let (lint, format) = rendered
2479 .split_once(" / ")
2480 .expect("differing invocations render as a pair");
2481 assert_eq!(
2482 lint,
2483 "djlint - --linter-output-format {filename}:{line}: {code} {message}"
2484 );
2485 assert_eq!(format, format!("{lint} --reformat"));
2486 }
2487
2488 #[test]
2494 fn a_curated_display_command_names_the_program_that_runs() {
2495 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2496 let Some(display) = meta.display_command else {
2497 continue;
2498 };
2499 let def = BUILTIN_TOOLS
2500 .get(meta.id)
2501 .expect("runtime metadata matches the registry");
2502 let program = def.command.first().expect("a built-in command names a program");
2503
2504 for invocation in display.split(" / ") {
2505 assert_eq!(
2506 invocation.split_whitespace().next(),
2507 Some(program.as_str()),
2508 "display_command for `{}` starts with a different program than it runs ({program})",
2509 meta.id
2510 );
2511 }
2512 }
2513 }
2514
2515 #[test]
2520 fn test_splice_replaces_region_and_preserves_prose() {
2521 let doc = format!(
2522 "# Title\n\nIntro.\n\n{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\nAfter.\n\n| Built-in tools | 31 | 339 |\n"
2523 );
2524 let out = splice_builtin_tools_docs(&doc).expect("splice");
2525 assert!(out.starts_with("# Title\n\nIntro.\n\n"));
2526 assert!(out.contains("After.\n"));
2527 assert!(!out.contains("stale"));
2528 assert!(
2529 out.contains(&render_builtin_tools_table()),
2530 "generated table is spliced in verbatim"
2531 );
2532 assert!(out.contains(&format!("| Built-in tools | {} | 339 |", builtin_tools_group_count())));
2534 }
2535
2536 #[test]
2537 fn test_splice_missing_marker_errors() {
2538 let doc = "# Title\n\nno markers\n\n| Built-in tools | 31 | 339 |\n";
2539 assert_eq!(splice_builtin_tools_docs(doc), Err(DocsError::MissingMarker));
2540 }
2541
2542 #[test]
2543 fn test_splice_duplicate_marker_errors() {
2544 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n{TABLE_BEGIN}\n\ny\n\n{TABLE_END}\n");
2545 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::DuplicateMarker));
2546 }
2547
2548 #[test]
2549 fn test_splice_marker_order_errors() {
2550 let doc = format!("{TABLE_END}\n\nx\n\n{TABLE_BEGIN}\n\n| Built-in tools | 31 | 339 |\n");
2551 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::MarkerOrder));
2552 }
2553
2554 #[test]
2555 fn test_splice_missing_count_row_errors() {
2556 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n\nno count row here\n");
2557 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::CountRowMissing));
2558 }
2559
2560 #[test]
2561 fn test_splice_is_idempotent() {
2562 let doc = format!("{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\n| Built-in tools | 31 | 339 |\n");
2563 let once = splice_builtin_tools_docs(&doc).expect("first");
2564 let twice = splice_builtin_tools_docs(&once).expect("second");
2565 assert_eq!(once, twice, "splice must be idempotent");
2566 }
2567}
2568
2569#[cfg(test)]
2570mod native_format_check_tests {
2571 use super::*;
2572
2573 #[test]
2574 fn native_checks_use_exact_argv_only_for_the_explicit_builtin_ids() {
2575 let registry = ToolRegistry::new(BTreeMap::new());
2576 for (id, argv) in [
2577 ("shuck:format-check", vec!["shuck", "format", "-", "--check"]),
2578 (
2579 "djlint:html:format-check",
2580 vec!["djlint", "-", "--check", "--profile=html"],
2581 ),
2582 (
2583 "djlint:jinja:format-check",
2584 vec!["djlint", "-", "--check", "--profile=jinja"],
2585 ),
2586 ] {
2587 assert_eq!(registry.native_format_check(id).unwrap().command, argv);
2588 }
2589 for id in [
2590 "oxfmt:lint",
2591 "oxfmt:format",
2592 "shuck:format",
2593 "shuck:lint",
2594 "shuck:lint-fix",
2595 "djlint:html:format",
2596 "djlint:jinja:format",
2597 "djlint:html:lint",
2598 "djlint:reformat",
2599 ] {
2600 assert!(registry.native_format_check(id).is_none(), "{id}");
2601 }
2602 }
2603}