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(
496 "jq",
497 ToolDefinition {
498 command: vec!["jq".to_string(), ".".to_string()],
499 stdin: true,
500 stdout: true,
501 lint_args: vec![],
502 format_args: vec![],
503 },
504 );
505
506 m.insert(
508 "yamlfmt",
509 ToolDefinition {
510 command: vec!["yamlfmt".to_string()],
511 stdin: true,
512 stdout: true,
513 lint_args: vec![],
514 format_args: vec!["-".to_string()],
515 },
516 );
517
518 m.insert(
520 "taplo",
521 ToolDefinition {
522 command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
523 stdin: true,
524 stdout: true,
525 lint_args: vec![],
526 format_args: vec![],
527 },
528 );
529
530 let terraform_fmt = || ToolDefinition {
535 command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
536 stdin: true,
537 stdout: true,
538 lint_args: vec![],
539 format_args: vec![],
540 };
541 m.insert("terraform:format", terraform_fmt());
542 m.insert("terraform-fmt", terraform_fmt());
543
544 m.insert(
546 "nixfmt",
547 ToolDefinition {
548 command: vec!["nixfmt".to_string(), "-".to_string()],
549 stdin: true,
550 stdout: true,
551 lint_args: vec![],
552 format_args: vec![],
553 },
554 );
555
556 m.insert(
558 "stylua",
559 ToolDefinition {
560 command: vec!["stylua".to_string(), "-".to_string()],
561 stdin: true,
562 stdout: true,
563 lint_args: vec![],
564 format_args: vec![],
565 },
566 );
567
568 m.insert(
570 "ormolu",
571 ToolDefinition {
572 command: vec!["ormolu".to_string(), "--stdin-input-file=_.hs".to_string()],
573 stdin: true,
574 stdout: true,
575 lint_args: vec![],
576 format_args: vec![],
577 },
578 );
579
580 m.insert(
582 "elm-format",
583 ToolDefinition {
584 command: vec!["elm-format".to_string(), "--stdin".to_string()],
585 stdin: true,
586 stdout: true,
587 lint_args: vec![],
588 format_args: vec![],
589 },
590 );
591
592 m.insert(
594 "swift-format",
595 ToolDefinition {
596 command: vec!["swift-format".to_string(), "format".to_string(), "-".to_string()],
597 stdin: true,
598 stdout: true,
599 lint_args: vec![],
600 format_args: vec![],
601 },
602 );
603
604 m.insert(
606 "ktfmt",
607 ToolDefinition {
608 command: vec!["ktfmt".to_string(), "-".to_string()],
609 stdin: true,
610 stdout: true,
611 lint_args: vec![],
612 format_args: vec![],
613 },
614 );
615
616 const DJLINT_OUTPUT_FORMAT: &str = "{filename}:{line}: {code} {message}";
624
625 m.insert(
626 "djlint",
627 ToolDefinition {
628 command: vec![
629 "djlint".to_string(),
630 "-".to_string(),
631 "--linter-output-format".to_string(),
632 DJLINT_OUTPUT_FORMAT.to_string(),
633 ],
634 stdin: true,
635 stdout: true,
636 lint_args: vec![],
637 format_args: vec!["--reformat".to_string()],
638 },
639 );
640
641 m.insert(
642 "djlint:lint",
643 ToolDefinition {
644 command: vec![
645 "djlint".to_string(),
646 "-".to_string(),
647 "--linter-output-format".to_string(),
648 DJLINT_OUTPUT_FORMAT.to_string(),
649 ],
650 stdin: true,
651 stdout: true,
652 lint_args: vec![],
653 format_args: vec![],
654 },
655 );
656
657 m.insert(
658 "djlint:reformat",
659 ToolDefinition {
660 command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
661 stdin: true,
662 stdout: true,
663 lint_args: vec![],
664 format_args: vec![],
665 },
666 );
667
668 m.insert(
670 "beautysh",
671 ToolDefinition {
672 command: vec!["beautysh".to_string(), "-".to_string()],
673 stdin: true,
674 stdout: true,
675 lint_args: vec![],
676 format_args: vec![],
677 },
678 );
679
680 m.insert(
682 "tombi",
683 ToolDefinition {
684 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
685 stdin: true,
686 stdout: true,
687 lint_args: vec![],
688 format_args: vec![],
689 },
690 );
691
692 m.insert(
693 "tombi:format",
694 ToolDefinition {
695 command: vec!["tombi".to_string(), "format".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:lint",
705 ToolDefinition {
706 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
707 stdin: true,
708 stdout: true,
709 lint_args: vec![],
710 format_args: vec![],
711 },
712 );
713
714 m.insert(
716 "oxfmt",
717 ToolDefinition {
718 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
719 stdin: true,
720 stdout: true,
721 lint_args: vec![],
722 format_args: vec![],
723 },
724 );
725
726 m.insert(
727 "oxfmt:js",
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:ts",
739 ToolDefinition {
740 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
741 stdin: true,
742 stdout: true,
743 lint_args: vec![],
744 format_args: vec![],
745 },
746 );
747
748 m.insert(
749 "oxfmt:jsx",
750 ToolDefinition {
751 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
752 stdin: true,
753 stdout: true,
754 lint_args: vec![],
755 format_args: vec![],
756 },
757 );
758
759 m.insert(
760 "oxfmt:tsx",
761 ToolDefinition {
762 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
763 stdin: true,
764 stdout: true,
765 lint_args: vec![],
766 format_args: vec![],
767 },
768 );
769
770 m.insert(
771 "oxfmt:json",
772 ToolDefinition {
773 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
774 stdin: true,
775 stdout: true,
776 lint_args: vec![],
777 format_args: vec![],
778 },
779 );
780
781 m.insert(
782 "oxfmt:css",
783 ToolDefinition {
784 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
785 stdin: true,
786 stdout: true,
787 lint_args: vec![],
788 format_args: vec![],
789 },
790 );
791
792 for (id, ext) in [
795 ("bun-fmt", "js"),
796 ("bun-fmt:js", "js"),
797 ("bun-fmt:ts", "ts"),
798 ("bun-fmt:jsx", "jsx"),
799 ("bun-fmt:tsx", "tsx"),
800 ("bun-fmt:json", "json"),
801 ("bun-fmt:jsonc", "jsonc"),
802 ("bun-fmt:json5", "json5"),
803 ("bun-fmt:css", "css"),
804 ("bun-fmt:scss", "scss"),
805 ("bun-fmt:less", "less"),
806 ("bun-fmt:html", "html"),
807 ("bun-fmt:vue", "vue"),
808 ("bun-fmt:yaml", "yaml"),
809 ("bun-fmt:toml", "toml"),
810 ("bun-fmt:graphql", "graphql"),
811 ("bun-fmt:markdown", "md"),
812 ("bun-fmt:mdx", "mdx"),
813 ] {
814 m.insert(
815 id,
816 ToolDefinition {
817 command: vec![
818 "bun".to_string(),
819 "fmt".to_string(),
820 format!("--stdin-filepath=_.{ext}"),
821 ],
822 stdin: true,
823 stdout: true,
824 lint_args: vec![],
825 format_args: vec![],
826 },
827 );
828 }
829
830 let deno_fmt = |ext: &str| ToolDefinition {
833 command: vec![
834 "deno".to_string(),
835 "fmt".to_string(),
836 format!("--ext={ext}"),
837 "-".to_string(),
838 ],
839 stdin: true,
840 stdout: true,
841 lint_args: vec![],
842 format_args: vec![],
843 };
844 m.insert("deno-fmt", deno_fmt("ts"));
845 m.insert("deno-fmt:ts", deno_fmt("ts"));
846 m.insert("deno-fmt:js", deno_fmt("js"));
847 m.insert("deno-fmt:json", deno_fmt("json"));
848 m.insert("deno-fmt:jsonc", deno_fmt("jsonc"));
849 m.insert("deno-fmt:md", deno_fmt("md"));
850
851 for (id, existing) in [
854 ("oxfmt:lint", "oxfmt"),
855 ("oxfmt:format", "oxfmt"),
856 ("shuck:lint", "shuck"),
857 ("shuck:format-check", "shuck:format"),
858 ] {
859 m.insert(id, m[existing].clone());
860 }
861 let mut shuck_fix = m["shuck"].clone();
862 shuck_fix.command.push("--fix".to_string());
863 m.insert("shuck:lint-fix", shuck_fix);
866
867 for (profile, lint_id, check_id, format_id) in [
868 (
869 "html",
870 "djlint:html:lint",
871 "djlint:html:format-check",
872 "djlint:html:format",
873 ),
874 (
875 "jinja",
876 "djlint:jinja:lint",
877 "djlint:jinja:format-check",
878 "djlint:jinja:format",
879 ),
880 ] {
881 let mut lint = m["djlint:lint"].clone();
882 lint.command.push(format!("--profile={profile}"));
883 m.insert(lint_id, lint);
884 let mut format = m["djlint:reformat"].clone();
885 format.command.push(format!("--profile={profile}"));
886 m.insert(check_id, format.clone());
887 m.insert(format_id, format);
888 }
889
890 m
891});
892
893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
895enum ToolKind {
896 Lint,
897 FormatCheck,
899 Format,
900 Both,
901}
902
903impl ToolKind {
904 const fn label(self) -> &'static str {
905 match self {
906 ToolKind::Lint | ToolKind::FormatCheck => "Lint",
907 ToolKind::Format => "Format",
908 ToolKind::Both => "Both",
909 }
910 }
911}
912
913#[derive(Debug, Clone, Copy, PartialEq, Eq)]
915pub enum BuiltinLintMode {
916 Diagnostics,
918 FormatCheck,
925}
926
927pub fn builtin_lint_mode(tool_id: &str) -> Option<BuiltinLintMode> {
929 BUILTIN_TOOLS_DOCS
930 .iter()
931 .find(|m| m.id == tool_id && m.runtime)
932 .map(|m| match m.kind {
933 ToolKind::Lint | ToolKind::Both => BuiltinLintMode::Diagnostics,
934 ToolKind::Format | ToolKind::FormatCheck => BuiltinLintMode::FormatCheck,
935 })
936}
937
938pub fn builtin_tool_formats(tool_id: &str) -> Option<bool> {
944 BUILTIN_TOOLS_DOCS
945 .iter()
946 .find(|m| m.id == tool_id && m.runtime)
947 .map(|m| matches!(m.kind, ToolKind::Format | ToolKind::Both))
948}
949
950struct ToolDocMeta {
964 id: &'static str,
965 language: &'static str,
966 kind: ToolKind,
967 doc_group: &'static str,
969 display_command: Option<&'static str>,
971 runtime: bool,
973}
974
975const BUILTIN_TOOLS_DOCS: &[ToolDocMeta] = &[
977 ToolDocMeta {
978 id: "ruff:check",
979 language: "Python",
980 kind: ToolKind::Lint,
981 doc_group: "ruff:check",
982 display_command: Some("ruff check --output-format=concise -"),
983 runtime: true,
984 },
985 ToolDocMeta {
986 id: "ruff:format",
987 language: "Python",
988 kind: ToolKind::Format,
989 doc_group: "ruff:format",
990 display_command: Some("ruff format -"),
991 runtime: true,
992 },
993 ToolDocMeta {
994 id: "black",
995 language: "Python",
996 kind: ToolKind::Format,
997 doc_group: "black",
998 display_command: None,
999 runtime: true,
1000 },
1001 ToolDocMeta {
1002 id: "prettier",
1003 language: "Multi",
1004 kind: ToolKind::Format,
1005 doc_group: "prettier",
1006 display_command: Some("prettier --stdin-filepath=_.EXT"),
1007 runtime: true,
1008 },
1009 ToolDocMeta {
1010 id: "prettier:json",
1011 language: "Multi",
1012 kind: ToolKind::Format,
1013 doc_group: "prettier",
1014 display_command: Some("prettier --stdin-filepath=_.EXT"),
1015 runtime: true,
1016 },
1017 ToolDocMeta {
1018 id: "prettier:yaml",
1019 language: "Multi",
1020 kind: ToolKind::Format,
1021 doc_group: "prettier",
1022 display_command: Some("prettier --stdin-filepath=_.EXT"),
1023 runtime: true,
1024 },
1025 ToolDocMeta {
1026 id: "prettier:html",
1027 language: "Multi",
1028 kind: ToolKind::Format,
1029 doc_group: "prettier",
1030 display_command: Some("prettier --stdin-filepath=_.EXT"),
1031 runtime: true,
1032 },
1033 ToolDocMeta {
1034 id: "prettier:css",
1035 language: "Multi",
1036 kind: ToolKind::Format,
1037 doc_group: "prettier",
1038 display_command: Some("prettier --stdin-filepath=_.EXT"),
1039 runtime: true,
1040 },
1041 ToolDocMeta {
1042 id: "prettier:markdown",
1043 language: "Multi",
1044 kind: ToolKind::Format,
1045 doc_group: "prettier",
1046 display_command: Some("prettier --stdin-filepath=_.EXT"),
1047 runtime: true,
1048 },
1049 ToolDocMeta {
1050 id: "shellcheck",
1051 language: "Shell",
1052 kind: ToolKind::Lint,
1053 doc_group: "shellcheck",
1054 display_command: None,
1055 runtime: true,
1056 },
1057 ToolDocMeta {
1058 id: "shfmt",
1059 language: "Shell",
1060 kind: ToolKind::Format,
1061 doc_group: "shfmt",
1062 display_command: None,
1063 runtime: true,
1064 },
1065 ToolDocMeta {
1066 id: "shuck",
1067 language: "Shell",
1068 kind: ToolKind::Lint,
1069 doc_group: "shuck",
1070 display_command: None,
1071 runtime: true,
1072 },
1073 ToolDocMeta {
1074 id: "shuck:format",
1075 language: "Shell",
1076 kind: ToolKind::Format,
1077 doc_group: "shuck:format",
1078 display_command: None,
1079 runtime: true,
1080 },
1081 ToolDocMeta {
1082 id: "rustfmt",
1083 language: "Rust",
1084 kind: ToolKind::Format,
1085 doc_group: "rustfmt",
1086 display_command: None,
1087 runtime: true,
1088 },
1089 ToolDocMeta {
1090 id: "gofmt",
1091 language: "Go",
1092 kind: ToolKind::Format,
1093 doc_group: "gofmt",
1094 display_command: None,
1095 runtime: true,
1096 },
1097 ToolDocMeta {
1098 id: "goimports",
1099 language: "Go",
1100 kind: ToolKind::Format,
1101 doc_group: "goimports",
1102 display_command: None,
1103 runtime: true,
1104 },
1105 ToolDocMeta {
1106 id: "clang-format",
1107 language: "C/C++",
1108 kind: ToolKind::Format,
1109 doc_group: "clang-format",
1110 display_command: None,
1111 runtime: true,
1112 },
1113 ToolDocMeta {
1114 id: "sqlfluff:lint",
1115 language: "SQL",
1116 kind: ToolKind::Lint,
1117 doc_group: "sqlfluff:lint",
1118 display_command: None,
1119 runtime: true,
1120 },
1121 ToolDocMeta {
1122 id: "sqlfluff:fix",
1123 language: "SQL",
1124 kind: ToolKind::Format,
1125 doc_group: "sqlfluff:fix",
1126 display_command: None,
1127 runtime: true,
1128 },
1129 ToolDocMeta {
1130 id: "jq",
1131 language: "JSON",
1132 kind: ToolKind::Both,
1133 doc_group: "jq",
1134 display_command: None,
1135 runtime: true,
1136 },
1137 ToolDocMeta {
1138 id: "yamlfmt",
1139 language: "YAML",
1140 kind: ToolKind::Format,
1141 doc_group: "yamlfmt",
1142 display_command: None,
1143 runtime: true,
1144 },
1145 ToolDocMeta {
1146 id: "taplo",
1147 language: "TOML",
1148 kind: ToolKind::Format,
1149 doc_group: "taplo",
1150 display_command: None,
1151 runtime: true,
1152 },
1153 ToolDocMeta {
1154 id: "terraform:format",
1155 language: "Terraform",
1156 kind: ToolKind::Format,
1157 doc_group: "terraform:format",
1158 display_command: Some("terraform fmt -"),
1159 runtime: true,
1160 },
1161 ToolDocMeta {
1162 id: "terraform-fmt",
1163 language: "Terraform",
1164 kind: ToolKind::Format,
1165 doc_group: "terraform:format",
1166 display_command: Some("terraform fmt -"),
1167 runtime: true,
1168 },
1169 ToolDocMeta {
1170 id: "nixfmt",
1171 language: "Nix",
1172 kind: ToolKind::Format,
1173 doc_group: "nixfmt",
1174 display_command: None,
1175 runtime: true,
1176 },
1177 ToolDocMeta {
1178 id: "stylua",
1179 language: "Lua",
1180 kind: ToolKind::Format,
1181 doc_group: "stylua",
1182 display_command: None,
1183 runtime: true,
1184 },
1185 ToolDocMeta {
1186 id: "ormolu",
1187 language: "Haskell",
1188 kind: ToolKind::Format,
1189 doc_group: "ormolu",
1190 display_command: None,
1191 runtime: true,
1192 },
1193 ToolDocMeta {
1194 id: "elm-format",
1195 language: "Elm",
1196 kind: ToolKind::Format,
1197 doc_group: "elm-format",
1198 display_command: None,
1199 runtime: true,
1200 },
1201 ToolDocMeta {
1202 id: "swift-format",
1203 language: "Swift",
1204 kind: ToolKind::Format,
1205 doc_group: "swift-format",
1206 display_command: None,
1207 runtime: true,
1208 },
1209 ToolDocMeta {
1210 id: "ktfmt",
1211 language: "Kotlin",
1212 kind: ToolKind::Format,
1213 doc_group: "ktfmt",
1214 display_command: None,
1215 runtime: true,
1216 },
1217 ToolDocMeta {
1220 id: "djlint",
1221 language: "Jinja/HTML",
1222 kind: ToolKind::Both,
1223 doc_group: "djlint",
1224 display_command: Some("djlint - / djlint - --reformat"),
1225 runtime: true,
1226 },
1227 ToolDocMeta {
1228 id: "djlint:lint",
1229 language: "Jinja/HTML",
1230 kind: ToolKind::Lint,
1231 doc_group: "djlint:lint",
1232 display_command: Some("djlint -"),
1233 runtime: true,
1234 },
1235 ToolDocMeta {
1236 id: "djlint:reformat",
1237 language: "Jinja/HTML",
1238 kind: ToolKind::Format,
1239 doc_group: "djlint:reformat",
1240 display_command: None,
1241 runtime: true,
1242 },
1243 ToolDocMeta {
1244 id: "beautysh",
1245 language: "Shell",
1246 kind: ToolKind::Format,
1247 doc_group: "beautysh",
1248 display_command: None,
1249 runtime: true,
1250 },
1251 ToolDocMeta {
1252 id: "tombi",
1253 language: "TOML",
1254 kind: ToolKind::Lint,
1255 doc_group: "tombi",
1256 display_command: None,
1257 runtime: true,
1258 },
1259 ToolDocMeta {
1260 id: "tombi:format",
1261 language: "TOML",
1262 kind: ToolKind::Format,
1263 doc_group: "tombi:format",
1264 display_command: None,
1265 runtime: true,
1266 },
1267 ToolDocMeta {
1268 id: "tombi:lint",
1269 language: "TOML",
1270 kind: ToolKind::Lint,
1271 doc_group: "tombi:lint",
1272 display_command: None,
1273 runtime: true,
1274 },
1275 ToolDocMeta {
1276 id: "oxfmt",
1277 language: "Multi",
1278 kind: ToolKind::Format,
1279 doc_group: "oxfmt",
1280 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1281 runtime: true,
1282 },
1283 ToolDocMeta {
1284 id: "oxfmt:js",
1285 language: "Multi",
1286 kind: ToolKind::Format,
1287 doc_group: "oxfmt",
1288 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1289 runtime: true,
1290 },
1291 ToolDocMeta {
1292 id: "oxfmt:ts",
1293 language: "Multi",
1294 kind: ToolKind::Format,
1295 doc_group: "oxfmt",
1296 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1297 runtime: true,
1298 },
1299 ToolDocMeta {
1300 id: "oxfmt:jsx",
1301 language: "Multi",
1302 kind: ToolKind::Format,
1303 doc_group: "oxfmt",
1304 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1305 runtime: true,
1306 },
1307 ToolDocMeta {
1308 id: "oxfmt:tsx",
1309 language: "Multi",
1310 kind: ToolKind::Format,
1311 doc_group: "oxfmt",
1312 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1313 runtime: true,
1314 },
1315 ToolDocMeta {
1316 id: "oxfmt:json",
1317 language: "Multi",
1318 kind: ToolKind::Format,
1319 doc_group: "oxfmt",
1320 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1321 runtime: true,
1322 },
1323 ToolDocMeta {
1324 id: "oxfmt:css",
1325 language: "Multi",
1326 kind: ToolKind::Format,
1327 doc_group: "oxfmt",
1328 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
1329 runtime: true,
1330 },
1331 ToolDocMeta {
1332 id: "bun-fmt",
1333 language: "Multi",
1334 kind: ToolKind::Format,
1335 doc_group: "bun-fmt",
1336 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1337 runtime: true,
1338 },
1339 ToolDocMeta {
1340 id: "bun-fmt:js",
1341 language: "Multi",
1342 kind: ToolKind::Format,
1343 doc_group: "bun-fmt",
1344 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1345 runtime: true,
1346 },
1347 ToolDocMeta {
1348 id: "bun-fmt:ts",
1349 language: "Multi",
1350 kind: ToolKind::Format,
1351 doc_group: "bun-fmt",
1352 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1353 runtime: true,
1354 },
1355 ToolDocMeta {
1356 id: "bun-fmt:jsx",
1357 language: "Multi",
1358 kind: ToolKind::Format,
1359 doc_group: "bun-fmt",
1360 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1361 runtime: true,
1362 },
1363 ToolDocMeta {
1364 id: "bun-fmt:tsx",
1365 language: "Multi",
1366 kind: ToolKind::Format,
1367 doc_group: "bun-fmt",
1368 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1369 runtime: true,
1370 },
1371 ToolDocMeta {
1372 id: "bun-fmt:json",
1373 language: "Multi",
1374 kind: ToolKind::Format,
1375 doc_group: "bun-fmt",
1376 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1377 runtime: true,
1378 },
1379 ToolDocMeta {
1380 id: "bun-fmt:jsonc",
1381 language: "Multi",
1382 kind: ToolKind::Format,
1383 doc_group: "bun-fmt",
1384 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1385 runtime: true,
1386 },
1387 ToolDocMeta {
1388 id: "bun-fmt:json5",
1389 language: "Multi",
1390 kind: ToolKind::Format,
1391 doc_group: "bun-fmt",
1392 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1393 runtime: true,
1394 },
1395 ToolDocMeta {
1396 id: "bun-fmt:css",
1397 language: "Multi",
1398 kind: ToolKind::Format,
1399 doc_group: "bun-fmt",
1400 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1401 runtime: true,
1402 },
1403 ToolDocMeta {
1404 id: "bun-fmt:scss",
1405 language: "Multi",
1406 kind: ToolKind::Format,
1407 doc_group: "bun-fmt",
1408 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1409 runtime: true,
1410 },
1411 ToolDocMeta {
1412 id: "bun-fmt:less",
1413 language: "Multi",
1414 kind: ToolKind::Format,
1415 doc_group: "bun-fmt",
1416 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1417 runtime: true,
1418 },
1419 ToolDocMeta {
1420 id: "bun-fmt:html",
1421 language: "Multi",
1422 kind: ToolKind::Format,
1423 doc_group: "bun-fmt",
1424 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1425 runtime: true,
1426 },
1427 ToolDocMeta {
1428 id: "bun-fmt:vue",
1429 language: "Multi",
1430 kind: ToolKind::Format,
1431 doc_group: "bun-fmt",
1432 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1433 runtime: true,
1434 },
1435 ToolDocMeta {
1436 id: "bun-fmt:yaml",
1437 language: "Multi",
1438 kind: ToolKind::Format,
1439 doc_group: "bun-fmt",
1440 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1441 runtime: true,
1442 },
1443 ToolDocMeta {
1444 id: "bun-fmt:toml",
1445 language: "Multi",
1446 kind: ToolKind::Format,
1447 doc_group: "bun-fmt",
1448 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1449 runtime: true,
1450 },
1451 ToolDocMeta {
1452 id: "bun-fmt:graphql",
1453 language: "Multi",
1454 kind: ToolKind::Format,
1455 doc_group: "bun-fmt",
1456 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1457 runtime: true,
1458 },
1459 ToolDocMeta {
1460 id: "bun-fmt:markdown",
1461 language: "Multi",
1462 kind: ToolKind::Format,
1463 doc_group: "bun-fmt",
1464 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1465 runtime: true,
1466 },
1467 ToolDocMeta {
1468 id: "bun-fmt:mdx",
1469 language: "Multi",
1470 kind: ToolKind::Format,
1471 doc_group: "bun-fmt",
1472 display_command: Some("bun fmt --stdin-filepath=_.EXT"),
1473 runtime: true,
1474 },
1475 ToolDocMeta {
1476 id: "deno-fmt",
1477 language: "Multi",
1478 kind: ToolKind::Format,
1479 doc_group: "deno-fmt",
1480 display_command: Some("deno fmt --ext=EXT -"),
1481 runtime: true,
1482 },
1483 ToolDocMeta {
1484 id: "deno-fmt:ts",
1485 language: "Multi",
1486 kind: ToolKind::Format,
1487 doc_group: "deno-fmt",
1488 display_command: Some("deno fmt --ext=EXT -"),
1489 runtime: true,
1490 },
1491 ToolDocMeta {
1492 id: "deno-fmt:js",
1493 language: "Multi",
1494 kind: ToolKind::Format,
1495 doc_group: "deno-fmt",
1496 display_command: Some("deno fmt --ext=EXT -"),
1497 runtime: true,
1498 },
1499 ToolDocMeta {
1500 id: "deno-fmt:json",
1501 language: "Multi",
1502 kind: ToolKind::Format,
1503 doc_group: "deno-fmt",
1504 display_command: Some("deno fmt --ext=EXT -"),
1505 runtime: true,
1506 },
1507 ToolDocMeta {
1508 id: "deno-fmt:jsonc",
1509 language: "Multi",
1510 kind: ToolKind::Format,
1511 doc_group: "deno-fmt",
1512 display_command: Some("deno fmt --ext=EXT -"),
1513 runtime: true,
1514 },
1515 ToolDocMeta {
1516 id: "deno-fmt:md",
1517 language: "Multi",
1518 kind: ToolKind::Format,
1519 doc_group: "deno-fmt",
1520 display_command: Some("deno fmt --ext=EXT -"),
1521 runtime: true,
1522 },
1523 ToolDocMeta {
1524 id: "shuck:lint",
1525 language: "Shell",
1526 kind: ToolKind::Lint,
1527 doc_group: "shuck:lint",
1528 display_command: None,
1529 runtime: true,
1530 },
1531 ToolDocMeta {
1532 id: "shuck:lint-fix",
1533 language: "Shell",
1534 kind: ToolKind::Format,
1535 doc_group: "shuck:lint-fix",
1536 display_command: None,
1537 runtime: true,
1538 },
1539 ToolDocMeta {
1540 id: "shuck:format-check",
1541 language: "Shell",
1542 kind: ToolKind::FormatCheck,
1543 doc_group: "shuck:format-check",
1544 display_command: Some("shuck format - --check"),
1545 runtime: true,
1546 },
1547 ToolDocMeta {
1548 id: "oxfmt:lint",
1549 language: "JavaScript",
1550 kind: ToolKind::FormatCheck,
1551 doc_group: "oxfmt:lint",
1552 display_command: None,
1553 runtime: true,
1554 },
1555 ToolDocMeta {
1556 id: "oxfmt:format",
1557 language: "JavaScript",
1558 kind: ToolKind::Format,
1559 doc_group: "oxfmt:format",
1560 display_command: None,
1561 runtime: true,
1562 },
1563 ToolDocMeta {
1564 id: "djlint:html:lint",
1565 language: "HTML",
1566 kind: ToolKind::Lint,
1567 doc_group: "djlint:html:lint",
1568 display_command: Some("djlint - --profile=html"),
1569 runtime: true,
1570 },
1571 ToolDocMeta {
1572 id: "djlint:html:format-check",
1573 language: "HTML",
1574 kind: ToolKind::FormatCheck,
1575 doc_group: "djlint:html:format-check",
1576 display_command: Some("djlint - --check --profile=html"),
1577 runtime: true,
1578 },
1579 ToolDocMeta {
1580 id: "djlint:html:format",
1581 language: "HTML",
1582 kind: ToolKind::Format,
1583 doc_group: "djlint:html:format",
1584 display_command: Some("djlint - --reformat --profile=html"),
1585 runtime: true,
1586 },
1587 ToolDocMeta {
1588 id: "djlint:jinja:lint",
1589 language: "Jinja",
1590 kind: ToolKind::Lint,
1591 doc_group: "djlint:jinja:lint",
1592 display_command: Some("djlint - --profile=jinja"),
1593 runtime: true,
1594 },
1595 ToolDocMeta {
1596 id: "djlint:jinja:format-check",
1597 language: "Jinja",
1598 kind: ToolKind::FormatCheck,
1599 doc_group: "djlint:jinja:format-check",
1600 display_command: Some("djlint - --check --profile=jinja"),
1601 runtime: true,
1602 },
1603 ToolDocMeta {
1604 id: "djlint:jinja:format",
1605 language: "Jinja",
1606 kind: ToolKind::Format,
1607 doc_group: "djlint:jinja:format",
1608 display_command: Some("djlint - --reformat --profile=jinja"),
1609 runtime: true,
1610 },
1611 ToolDocMeta {
1612 id: "rumdl:lint",
1613 language: "Markdown",
1614 kind: ToolKind::Lint,
1615 doc_group: "rumdl:lint",
1616 display_command: Some("built-in markdown linting"),
1617 runtime: false,
1618 },
1619 ToolDocMeta {
1620 id: "rumdl:format",
1621 language: "Markdown",
1622 kind: ToolKind::Format,
1623 doc_group: "rumdl:format",
1624 display_command: Some("built-in markdown formatting"),
1625 runtime: false,
1626 },
1627 ToolDocMeta {
1630 id: RUMDL_BUILTIN_TOOL,
1631 language: "Markdown",
1632 kind: ToolKind::Lint,
1633 doc_group: RUMDL_BUILTIN_TOOL,
1634 display_command: Some("built-in markdown linting"),
1635 runtime: false,
1636 },
1637];
1638
1639const TABLE_BEGIN: &str = "<!-- BEGIN builtin-tools (generated) -->";
1641const TABLE_END: &str = "<!-- END builtin-tools (generated) -->";
1642
1643#[derive(Debug, Clone, PartialEq, Eq)]
1645pub enum DocsError {
1646 MissingMarker,
1648 DuplicateMarker,
1650 MarkerOrder,
1652 CountRowMissing,
1654 CountRowAmbiguous,
1656 CountRowMalformed,
1658}
1659
1660impl std::fmt::Display for DocsError {
1661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1662 let msg = match self {
1663 DocsError::MissingMarker => "missing `<!-- BEGIN/END builtin-tools (generated) -->` marker pair",
1664 DocsError::DuplicateMarker => "duplicate builtin-tools marker",
1665 DocsError::MarkerOrder => "END builtin-tools marker precedes BEGIN",
1666 DocsError::CountRowMissing => "`| Built-in tools` count row not found",
1667 DocsError::CountRowAmbiguous => "multiple `| Built-in tools` count rows found",
1668 DocsError::CountRowMalformed => "`| Built-in tools` count row has an unexpected layout",
1669 };
1670 f.write_str(msg)
1671 }
1672}
1673
1674impl std::error::Error for DocsError {}
1675
1676fn builtin_tools_group_count() -> usize {
1678 let mut seen: Vec<&str> = Vec::new();
1679 for m in BUILTIN_TOOLS_DOCS {
1680 if !seen.contains(&m.doc_group) {
1681 seen.push(m.doc_group);
1682 }
1683 }
1684 seen.len()
1685}
1686
1687pub fn render_builtin_tools_table() -> String {
1693 let headers = ["Tool ID", "Language", "Type", "Command"];
1694 let mut rows: Vec<[String; 4]> = Vec::new();
1695
1696 let mut seen: Vec<&str> = Vec::new();
1697 for m in BUILTIN_TOOLS_DOCS {
1698 if seen.contains(&m.doc_group) {
1699 continue;
1700 }
1701 seen.push(m.doc_group);
1702
1703 let command = match m.display_command {
1706 Some(cmd) => cmd.to_string(),
1707 None if m.runtime => runtime_command_for_kind(m.id, m.kind),
1708 None => String::new(),
1709 };
1710
1711 rows.push([
1712 format!("`{}`", m.doc_group),
1713 m.language.to_string(),
1714 m.kind.label().to_string(),
1715 format!("`{command}`"),
1716 ]);
1717 }
1718
1719 let mut widths = headers.map(str::chars).map(Iterator::count);
1721 for row in &rows {
1722 for (i, cell) in row.iter().enumerate() {
1723 widths[i] = widths[i].max(cell.chars().count());
1724 }
1725 }
1726
1727 let mut out = String::new();
1728 push_table_row(&mut out, &headers.map(String::from), &widths);
1729 let separators = std::array::from_fn(|i| "-".repeat(widths[i]));
1730 push_table_row(&mut out, &separators, &widths);
1731 for row in &rows {
1732 push_table_row(&mut out, row, &widths);
1733 }
1734 out
1735}
1736
1737fn runtime_command_for_kind(id: &str, kind: ToolKind) -> String {
1742 let Some(def) = BUILTIN_TOOLS.get(id) else {
1743 return String::new();
1744 };
1745 let invocation =
1746 |extra: &[String]| -> String { def.command.iter().chain(extra).cloned().collect::<Vec<_>>().join(" ") };
1747 match kind {
1748 ToolKind::Lint => invocation(&def.lint_args),
1749 ToolKind::Format | ToolKind::FormatCheck => invocation(&def.format_args),
1750 ToolKind::Both => {
1751 let lint = invocation(&def.lint_args);
1752 let format = invocation(&def.format_args);
1753 if lint == format {
1754 lint
1755 } else {
1756 format!("{lint} / {format}")
1757 }
1758 }
1759 }
1760}
1761
1762fn push_table_row(out: &mut String, cells: &[String; 4], widths: &[usize; 4]) {
1764 out.push('|');
1765 for (i, cell) in cells.iter().enumerate() {
1766 out.push(' ');
1767 out.push_str(cell);
1768 for _ in cell.chars().count()..widths[i] {
1769 out.push(' ');
1770 }
1771 out.push_str(" |");
1772 }
1773 out.push('\n');
1774}
1775
1776pub fn splice_builtin_tools_docs(existing: &str) -> Result<String, DocsError> {
1782 if existing.matches(TABLE_BEGIN).count() == 0 || existing.matches(TABLE_END).count() == 0 {
1783 return Err(DocsError::MissingMarker);
1784 }
1785 if existing.matches(TABLE_BEGIN).count() > 1 || existing.matches(TABLE_END).count() > 1 {
1786 return Err(DocsError::DuplicateMarker);
1787 }
1788 let begin_pos = existing.find(TABLE_BEGIN).unwrap();
1789 let end_pos = existing.find(TABLE_END).unwrap();
1790 if end_pos < begin_pos {
1791 return Err(DocsError::MarkerOrder);
1792 }
1793
1794 let table = render_builtin_tools_table();
1795 let replacement = format!("{TABLE_BEGIN}\n\n{table}\n{TABLE_END}");
1796
1797 let mut result = String::with_capacity(existing.len() + replacement.len());
1798 result.push_str(&existing[..begin_pos]);
1799 result.push_str(&replacement);
1800 result.push_str(&existing[end_pos + TABLE_END.len()..]);
1801
1802 update_builtin_tools_count(&result, builtin_tools_group_count())
1803}
1804
1805fn update_builtin_tools_count(text: &str, count: usize) -> Result<String, DocsError> {
1808 let lines: Vec<&str> = text.lines().collect();
1809 let mut target: Option<usize> = None;
1810 for (i, line) in lines.iter().enumerate() {
1811 if line.trim_start().starts_with("| Built-in tools") {
1812 if target.is_some() {
1813 return Err(DocsError::CountRowAmbiguous);
1814 }
1815 target = Some(i);
1816 }
1817 }
1818 let i = target.ok_or(DocsError::CountRowMissing)?;
1819 let new_line = replace_count_cell(lines[i], count).ok_or(DocsError::CountRowMalformed)?;
1820
1821 let mut out = String::with_capacity(text.len());
1822 for (j, line) in lines.iter().enumerate() {
1823 out.push_str(if j == i { &new_line } else { line });
1824 out.push('\n');
1825 }
1826 if !text.ends_with('\n') {
1827 out.pop();
1828 }
1829 Ok(out)
1830}
1831
1832fn replace_count_cell(line: &str, count: usize) -> Option<String> {
1834 let pipes: Vec<usize> = line.match_indices('|').map(|(i, _)| i).collect();
1835 if pipes.len() < 3 {
1836 return None;
1837 }
1838 let start = pipes[1] + 1;
1839 let end = pipes[2];
1840 let cell = line.get(start..end)?;
1841 let width = cell.len();
1842 let num = count.to_string();
1843
1844 let mut new_cell = String::with_capacity(width.max(num.len() + 2));
1845 new_cell.push(' ');
1846 new_cell.push_str(&num);
1847 while new_cell.len() < width {
1848 new_cell.push(' ');
1849 }
1850 if new_cell.len() > width {
1851 new_cell = format!(" {num} ");
1853 }
1854
1855 Some(format!("{}{}{}", &line[..start], new_cell, &line[end..]))
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860 use super::*;
1861
1862 #[test]
1863 fn test_get_builtin_tool() {
1864 let registry = ToolRegistry::default();
1865
1866 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1867 assert!(tool.command.contains(&"ruff".to_string()));
1868 assert!(tool.stdin);
1869 assert!(tool.stdout);
1870
1871 let tool = registry.get("shellcheck").expect("Should find shellcheck");
1872 assert!(tool.command.contains(&"shellcheck".to_string()));
1873 }
1874
1875 #[test]
1876 fn test_builtin_yamlfmt_answers_lint_by_formatting() {
1877 let registry = ToolRegistry::default();
1878
1879 assert_eq!(registry.lint_mode("yamlfmt"), Some(BuiltinLintMode::FormatCheck));
1880
1881 let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
1883 let mut argv = tool.command.clone();
1884 argv.extend(tool.format_args.clone());
1885
1886 assert_eq!(argv, vec!["yamlfmt", "-"]);
1887 assert!(tool.lint_args.is_empty());
1888 }
1889
1890 #[test]
1891 fn test_get_user_tool_overrides_builtin() {
1892 let mut user_tools = BTreeMap::new();
1893 user_tools.insert(
1894 "ruff:check".to_string(),
1895 ToolDefinition {
1896 command: vec!["custom-ruff".to_string()],
1897 stdin: false,
1898 stdout: false,
1899 lint_args: vec![],
1900 format_args: vec![],
1901 },
1902 );
1903
1904 let registry = ToolRegistry::new(user_tools);
1905
1906 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1907 assert_eq!(tool.command, vec!["custom-ruff"]);
1908 assert!(!tool.stdin); }
1910
1911 #[test]
1912 fn bun_formatter_variants_select_the_stdin_parser_and_check_by_comparison() {
1913 let registry = ToolRegistry::default();
1914 for (id, ext) in [
1915 ("bun-fmt", "js"),
1916 ("bun-fmt:ts", "ts"),
1917 ("bun-fmt:json", "json"),
1918 ("bun-fmt:mdx", "mdx"),
1919 ] {
1920 let tool = registry.get(id).unwrap();
1921 assert_eq!(tool.command, vec!["bun", "fmt", &format!("--stdin-filepath=_.{ext}")]);
1922 assert!(tool.stdin && tool.stdout);
1923 assert_eq!(registry.lint_mode(id), Some(BuiltinLintMode::FormatCheck));
1924 assert_eq!(builtin_tool_formats(id), Some(true));
1925 assert_eq!(registry.resolve_id(id, ToolSlot::Format).as_deref(), Some(id));
1926 assert_eq!(registry.resolve_id(id, ToolSlot::Lint).as_deref(), Some(id));
1927 }
1928 }
1929
1930 #[test]
1931 fn test_contains() {
1932 let registry = ToolRegistry::default();
1933
1934 assert!(registry.contains("ruff:check"));
1935 assert!(registry.contains("prettier"));
1936 assert!(registry.contains("shellcheck"));
1937 assert!(!registry.contains("nonexistent-tool"));
1938 }
1939
1940 #[test]
1941 fn test_list_tools() {
1942 let registry = ToolRegistry::default();
1943 let tools = registry.list_tools();
1944
1945 assert!(tools.contains(&"ruff:check"));
1946 assert!(tools.contains(&"ruff:format"));
1947 assert!(tools.contains(&"prettier"));
1948 assert!(tools.contains(&"shellcheck"));
1949 assert!(tools.contains(&"shfmt"));
1950 assert!(tools.contains(&"rustfmt"));
1951 assert!(tools.contains(&"gofmt"));
1952 }
1953
1954 #[test]
1955 fn test_user_tools_in_list() {
1956 let mut user_tools = BTreeMap::new();
1957 user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
1958
1959 let registry = ToolRegistry::new(user_tools);
1960 let tools = registry.list_tools();
1961
1962 assert!(tools.contains(&"my-custom-tool"));
1963 assert!(tools.contains(&"ruff:check")); }
1965
1966 #[test]
1967 fn test_new_builtin_tools() {
1968 let registry = ToolRegistry::default();
1969
1970 let tool = registry.get("djlint").expect("Should find djlint");
1972 assert!(tool.command.contains(&"djlint".to_string()));
1973 assert!(tool.stdin);
1974
1975 let tool = registry.get("beautysh").expect("Should find beautysh");
1977 assert!(tool.command.contains(&"beautysh".to_string()));
1978 assert!(tool.stdin);
1979
1980 let tool = registry.get("tombi").expect("Should find tombi");
1982 assert!(tool.command.contains(&"tombi".to_string()));
1983 assert!(tool.stdin);
1984
1985 let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
1986 assert!(tool.command.contains(&"lint".to_string()));
1987
1988 let tool = registry.get("tombi:format").expect("Should find tombi:format");
1989 assert!(
1990 tool.command.contains(&"format".to_string()),
1991 "tombi:format should use 'format' subcommand, got: {:?}",
1992 tool.command
1993 );
1994
1995 let tool = registry.get("oxfmt").expect("Should find oxfmt");
1997 assert!(tool.command.contains(&"oxfmt".to_string()));
1998 assert!(tool.stdin);
1999
2000 let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
2001 assert!(tool.command.iter().any(|s| s.contains("_.ts")));
2002 }
2003
2004 #[test]
2012 fn test_bare_tombi_resolves_to_lint_not_format() {
2013 let registry = ToolRegistry::default();
2014
2015 let bare = registry.get("tombi").expect("Should find bare tombi");
2016 let format = registry.get("tombi:format").expect("Should find tombi:format");
2017
2018 assert!(
2020 bare.command.contains(&"lint".to_string()),
2021 "Bare 'tombi' uses lint subcommand: {:?}",
2022 bare.command
2023 );
2024
2025 assert!(
2027 format.command.contains(&"format".to_string()),
2028 "tombi:format uses format subcommand: {:?}",
2029 format.command
2030 );
2031
2032 assert_ne!(
2034 bare.command, format.command,
2035 "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
2036 );
2037 }
2038
2039 #[test]
2042 fn test_tools_with_lint_format_variants_are_distinct() {
2043 let registry = ToolRegistry::default();
2044
2045 let ruff_check = registry.get("ruff:check").expect("ruff:check");
2047 let ruff_format = registry.get("ruff:format").expect("ruff:format");
2048 assert_ne!(
2049 ruff_check.command, ruff_format.command,
2050 "ruff:check and ruff:format should be distinct"
2051 );
2052
2053 let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
2055 let tombi_format = registry.get("tombi:format").expect("tombi:format");
2056 assert_ne!(
2057 tombi_lint.command, tombi_format.command,
2058 "tombi:lint and tombi:format should be distinct"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_deno_fmt_has_per_extension_variants() {
2064 let registry = ToolRegistry::default();
2067
2068 let deno_json = registry.get("deno-fmt:json").expect("deno-fmt:json");
2069 assert!(deno_json.command.iter().any(|a| a == "--ext=json"));
2070 let deno_md = registry.get("deno-fmt:md").expect("deno-fmt:md");
2071 assert!(deno_md.command.iter().any(|a| a == "--ext=md"));
2072
2073 let deno = registry.get("deno-fmt").expect("deno-fmt");
2075 assert!(deno.command.iter().any(|a| a == "--ext=ts"));
2076 }
2077
2078 #[test]
2087 fn no_builtin_declares_lint_args() {
2088 let offenders: Vec<&str> = BUILTIN_TOOLS
2089 .iter()
2090 .filter(|(_, def)| !def.lint_args.is_empty())
2091 .map(|(id, _)| *id)
2092 .collect();
2093
2094 assert!(
2095 offenders.is_empty(),
2096 "built-in tools must not declare lint_args (a formatter is lint-checked by \
2097 formatting and comparing; a linter's args belong in `command`): {offenders:?}"
2098 );
2099 }
2100
2101 #[test]
2105 fn builtin_lint_mode_follows_documented_kind() {
2106 let registry = ToolRegistry::default();
2107
2108 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2109 let def = registry.get(meta.id).expect("runtime metadata matches the registry");
2110 let mode = registry.lint_mode(meta.id);
2111
2112 match meta.kind {
2113 ToolKind::Format | ToolKind::FormatCheck => {
2114 assert_eq!(
2115 mode,
2116 Some(BuiltinLintMode::FormatCheck),
2117 "{} is documented as a formatter",
2118 meta.id
2119 );
2120 assert!(
2121 def.stdin && def.stdout,
2122 "{} is lint-checked by comparing its output, so it must read stdin \
2123 and write stdout",
2124 meta.id
2125 );
2126 }
2127 ToolKind::Lint | ToolKind::Both => {
2128 assert_eq!(
2129 mode,
2130 Some(BuiltinLintMode::Diagnostics),
2131 "{} is documented as a linter",
2132 meta.id
2133 );
2134 }
2135 }
2136 }
2137 }
2138
2139 #[test]
2149 fn format_slot_never_resolves_to_a_linter() {
2150 let registry = ToolRegistry::default();
2151 let documented_kind = |id: &str| {
2152 BUILTIN_TOOLS_DOCS
2153 .iter()
2154 .find(|m| m.id == id && m.runtime)
2155 .map(|m| m.kind)
2156 };
2157
2158 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2159 let fills = registry.fills_format_slot(meta.id);
2160 let resolved = registry.resolve_id(meta.id, ToolSlot::Format);
2161
2162 match fills {
2163 Some(true) => {
2164 let resolved = resolved.expect("a tool that fills the slot resolves in it");
2165 assert!(
2166 !matches!(documented_kind(&resolved), Some(ToolKind::Lint | ToolKind::FormatCheck)),
2167 "{} fills a format slot by running {resolved}, which only lints",
2168 meta.id
2169 );
2170 }
2171 Some(false) => assert!(
2172 matches!(documented_kind(meta.id), Some(ToolKind::Lint | ToolKind::FormatCheck)),
2173 "{} declines the format slot, so it must be documented as a linter",
2174 meta.id
2175 ),
2176 None => panic!("{} is in the registry, so it resolves somewhere", meta.id),
2177 }
2178 }
2179
2180 assert_eq!(registry.fills_format_slot("ruff:check"), Some(false));
2183 assert_eq!(registry.fills_format_slot("shellcheck"), Some(false));
2184 assert_eq!(registry.fills_format_slot("sqlfluff:lint"), Some(false));
2185 assert_eq!(registry.fills_format_slot("tombi"), Some(true));
2186 assert_eq!(
2187 registry.resolve_id("tombi", ToolSlot::Format).as_deref(),
2188 Some("tombi:format")
2189 );
2190 }
2191
2192 #[test]
2194 fn user_tool_is_never_lint_checked_by_formatting() {
2195 let mut user_tools = BTreeMap::new();
2196 user_tools.insert(
2197 "yamlfmt".to_string(),
2198 ToolDefinition {
2199 command: vec!["my-yaml-linter".to_string()],
2200 stdin: true,
2201 stdout: true,
2202 lint_args: vec!["--check".to_string()],
2203 format_args: vec![],
2204 },
2205 );
2206 let registry = ToolRegistry::new(user_tools);
2207
2208 assert_eq!(registry.lint_mode("yamlfmt"), None);
2209 assert_eq!(
2211 ToolRegistry::default().lint_mode("yamlfmt"),
2212 Some(BuiltinLintMode::FormatCheck)
2213 );
2214 }
2215
2216 #[test]
2221 fn explicit_mode_aliases_preserve_custom_bare_definitions() {
2222 for id in ["oxfmt", "shuck"] {
2223 let custom = ToolDefinition {
2224 command: vec!["custom-tool".to_string()],
2225 stdin: true,
2226 stdout: true,
2227 lint_args: vec![],
2228 format_args: vec![],
2229 };
2230 let registry = ToolRegistry::new(BTreeMap::from([(id.to_string(), custom.clone())]));
2231 for slot in [ToolSlot::Lint, ToolSlot::Format] {
2232 assert_eq!(registry.resolve(id, slot), Some(&custom));
2233 }
2234 }
2235 }
2236
2237 #[test]
2238 fn explicit_check_variants_compare_output_but_cannot_format() {
2239 let registry = ToolRegistry::default();
2240 for id in [
2241 "oxfmt:lint",
2242 "djlint:html:format-check",
2243 "djlint:jinja:format-check",
2244 "shuck:format-check",
2245 ] {
2246 assert_eq!(registry.lint_mode(id), Some(BuiltinLintMode::FormatCheck));
2247 assert_eq!(registry.fills_format_slot(id), Some(false));
2248 }
2249 for profile in ["html", "jinja"] {
2250 for mode in ["lint", "format-check", "format"] {
2251 let id = format!("djlint:{profile}:{mode}");
2252 assert!(
2253 registry
2254 .get(&id)
2255 .unwrap()
2256 .command
2257 .contains(&format!("--profile={profile}"))
2258 );
2259 }
2260 }
2261 }
2262
2263 #[test]
2264 fn test_docs_metadata_ids_unique() {
2265 let mut seen = std::collections::BTreeSet::new();
2266 for m in BUILTIN_TOOLS_DOCS {
2267 assert!(seen.insert(m.id), "duplicate metadata id: {}", m.id);
2268 }
2269 }
2270
2271 #[test]
2272 fn test_runtime_metadata_matches_registry_keys() {
2273 let runtime_meta: std::collections::BTreeSet<&str> =
2274 BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime).map(|m| m.id).collect();
2275 let map_keys: std::collections::BTreeSet<&str> = BUILTIN_TOOLS.keys().copied().collect();
2276 assert_eq!(
2277 runtime_meta, map_keys,
2278 "BUILTIN_TOOLS_DOCS runtime ids must exactly match BUILTIN_TOOLS keys (add/remove the doc entry alongside the registry entry)"
2279 );
2280 }
2281
2282 #[test]
2283 fn test_docs_only_ids_absent_from_registry() {
2284 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
2285 assert!(
2286 !BUILTIN_TOOLS.contains_key(m.id),
2287 "docs-only id {} must not be a runtime registry tool",
2288 m.id
2289 );
2290 }
2291 assert!(
2293 BUILTIN_TOOLS_DOCS
2294 .iter()
2295 .any(|m| !m.runtime && m.id == RUMDL_BUILTIN_TOOL),
2296 "rumdl must be present as a docs-only entry"
2297 );
2298 }
2299
2300 #[test]
2301 fn test_docs_only_requires_display_command() {
2302 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
2303 assert!(
2304 m.display_command.is_some(),
2305 "docs-only id {} needs a display_command (no runtime command to derive)",
2306 m.id
2307 );
2308 }
2309 }
2310
2311 #[test]
2312 fn test_multi_runtime_group_requires_display_command() {
2313 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
2314 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2315 *counts.entry(m.doc_group).or_default() += 1;
2316 }
2317 for m in BUILTIN_TOOLS_DOCS {
2318 if counts.get(m.doc_group).copied().unwrap_or(0) > 1 {
2319 assert!(
2320 m.display_command.is_some(),
2321 "doc_group `{}` has multiple runtime entries; `{}` needs an explicit display_command",
2322 m.doc_group,
2323 m.id
2324 );
2325 }
2326 }
2327 }
2328
2329 #[test]
2330 fn test_doc_group_language_and_kind_consistent() {
2331 let mut groups: std::collections::BTreeMap<&str, (&str, ToolKind)> = std::collections::BTreeMap::new();
2332 for m in BUILTIN_TOOLS_DOCS {
2333 match groups.get(m.doc_group) {
2334 None => {
2335 groups.insert(m.doc_group, (m.language, m.kind));
2336 }
2337 Some((lang, kind)) => {
2338 assert_eq!(
2339 *lang, m.language,
2340 "doc_group `{}` has mismatched languages",
2341 m.doc_group
2342 );
2343 assert_eq!(*kind, m.kind, "doc_group `{}` has mismatched kinds", m.doc_group);
2344 }
2345 }
2346 }
2347 }
2348
2349 fn rendered_rows(table: &str) -> Vec<(String, String, String, String)> {
2356 table
2357 .lines()
2358 .skip(2) .filter(|l| l.starts_with('|'))
2360 .map(|l| {
2361 let cells: Vec<String> = l.trim().trim_matches('|').split('|').map(|c| c.trim().to_string()).collect();
2362 (cells[0].clone(), cells[1].clone(), cells[2].clone(), cells[3].clone())
2363 })
2364 .collect()
2365 }
2366
2367 #[test]
2368 fn test_render_table_golden_rows() {
2369 let table = render_builtin_tools_table();
2370 let rows = rendered_rows(&table);
2371 let has = |id: &str, lang: &str, kind: &str, cmd: &str| {
2372 rows.iter()
2373 .any(|(i, l, k, c)| i == id && l == lang && k == kind && c == cmd)
2374 };
2375 assert!(has("`shellcheck`", "Shell", "Lint", "`shellcheck --shell=bash -`"));
2377 assert!(has("`rustfmt`", "Rust", "Format", "`rustfmt`"));
2378 assert!(has("`jq`", "JSON", "Both", "`jq .`"));
2379 assert!(has(
2380 "`prettier`",
2381 "Multi",
2382 "Format",
2383 "`prettier --stdin-filepath=_.EXT`"
2384 ));
2385 assert!(has("`rumdl`", "Markdown", "Lint", "`built-in markdown linting`"));
2386 assert!(
2388 !table.contains("prettier:json"),
2389 "prettier variants must collapse into one row"
2390 );
2391 assert!(!table.contains("oxfmt:ts"), "oxfmt variants must collapse into one row");
2392 }
2393
2394 #[test]
2395 fn test_render_table_one_row_per_group() {
2396 let table = render_builtin_tools_table();
2397 assert_eq!(rendered_rows(&table).len(), builtin_tools_group_count());
2398 }
2399
2400 #[test]
2401 fn test_render_table_command_fallback_from_registry() {
2402 let table = render_builtin_tools_table();
2404 assert!(
2405 rendered_rows(&table)
2406 .iter()
2407 .any(|(i, _, _, c)| i == "`tombi`" && c == "`tombi lint -`")
2408 );
2409 }
2410
2411 #[test]
2412 fn test_render_command_reflects_mode_args() {
2413 let table = render_builtin_tools_table();
2414 let rows = rendered_rows(&table);
2415 let cmd = |id: &str| {
2416 let row = rows
2417 .iter()
2418 .find(|(i, _, _, _)| i == id)
2419 .unwrap_or_else(|| panic!("row {id} not found"));
2420 row.3.clone()
2421 };
2422 assert_eq!(cmd("`yamlfmt`"), "`yamlfmt -`");
2424 assert_eq!(
2426 cmd("`sqlfluff:lint`"),
2427 "`sqlfluff lint --dialect ansi --format github-annotation-native -`"
2428 );
2429 }
2430
2431 #[test]
2432 fn test_runtime_command_for_kind_both_collapses_when_equal() {
2433 assert_eq!(runtime_command_for_kind("jq", ToolKind::Both), "jq .");
2435 }
2436
2437 #[test]
2442 fn test_runtime_command_for_kind_both_shows_both_invocations() {
2443 let rendered = runtime_command_for_kind("djlint", ToolKind::Both);
2444 let (lint, format) = rendered
2445 .split_once(" / ")
2446 .expect("differing invocations render as a pair");
2447 assert_eq!(
2448 lint,
2449 "djlint - --linter-output-format {filename}:{line}: {code} {message}"
2450 );
2451 assert_eq!(format, format!("{lint} --reformat"));
2452 }
2453
2454 #[test]
2460 fn a_curated_display_command_names_the_program_that_runs() {
2461 for meta in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
2462 let Some(display) = meta.display_command else {
2463 continue;
2464 };
2465 let def = BUILTIN_TOOLS
2466 .get(meta.id)
2467 .expect("runtime metadata matches the registry");
2468 let program = def.command.first().expect("a built-in command names a program");
2469
2470 for invocation in display.split(" / ") {
2471 assert_eq!(
2472 invocation.split_whitespace().next(),
2473 Some(program.as_str()),
2474 "display_command for `{}` starts with a different program than it runs ({program})",
2475 meta.id
2476 );
2477 }
2478 }
2479 }
2480
2481 #[test]
2486 fn test_splice_replaces_region_and_preserves_prose() {
2487 let doc = format!(
2488 "# Title\n\nIntro.\n\n{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\nAfter.\n\n| Built-in tools | 31 | 339 |\n"
2489 );
2490 let out = splice_builtin_tools_docs(&doc).expect("splice");
2491 assert!(out.starts_with("# Title\n\nIntro.\n\n"));
2492 assert!(out.contains("After.\n"));
2493 assert!(!out.contains("stale"));
2494 assert!(
2495 out.contains(&render_builtin_tools_table()),
2496 "generated table is spliced in verbatim"
2497 );
2498 assert!(out.contains(&format!("| Built-in tools | {} | 339 |", builtin_tools_group_count())));
2500 }
2501
2502 #[test]
2503 fn test_splice_missing_marker_errors() {
2504 let doc = "# Title\n\nno markers\n\n| Built-in tools | 31 | 339 |\n";
2505 assert_eq!(splice_builtin_tools_docs(doc), Err(DocsError::MissingMarker));
2506 }
2507
2508 #[test]
2509 fn test_splice_duplicate_marker_errors() {
2510 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n{TABLE_BEGIN}\n\ny\n\n{TABLE_END}\n");
2511 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::DuplicateMarker));
2512 }
2513
2514 #[test]
2515 fn test_splice_marker_order_errors() {
2516 let doc = format!("{TABLE_END}\n\nx\n\n{TABLE_BEGIN}\n\n| Built-in tools | 31 | 339 |\n");
2517 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::MarkerOrder));
2518 }
2519
2520 #[test]
2521 fn test_splice_missing_count_row_errors() {
2522 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n\nno count row here\n");
2523 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::CountRowMissing));
2524 }
2525
2526 #[test]
2527 fn test_splice_is_idempotent() {
2528 let doc = format!("{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\n| Built-in tools | 31 | 339 |\n");
2529 let once = splice_builtin_tools_docs(&doc).expect("first");
2530 let twice = splice_builtin_tools_docs(&once).expect("second");
2531 assert_eq!(once, twice, "splice must be idempotent");
2532 }
2533}
2534
2535#[cfg(test)]
2536mod native_format_check_tests {
2537 use super::*;
2538
2539 #[test]
2540 fn native_checks_use_exact_argv_only_for_the_explicit_builtin_ids() {
2541 let registry = ToolRegistry::new(BTreeMap::new());
2542 for (id, argv) in [
2543 ("shuck:format-check", vec!["shuck", "format", "-", "--check"]),
2544 (
2545 "djlint:html:format-check",
2546 vec!["djlint", "-", "--check", "--profile=html"],
2547 ),
2548 (
2549 "djlint:jinja:format-check",
2550 vec!["djlint", "-", "--check", "--profile=jinja"],
2551 ),
2552 ] {
2553 assert_eq!(registry.native_format_check(id).unwrap().command, argv);
2554 }
2555 for id in [
2556 "oxfmt:lint",
2557 "oxfmt:format",
2558 "shuck:format",
2559 "shuck:lint",
2560 "shuck:lint-fix",
2561 "djlint:html:format",
2562 "djlint:jinja:format",
2563 "djlint:html:lint",
2564 "djlint:reformat",
2565 ] {
2566 assert!(registry.native_format_check(id).is_none(), "{id}");
2567 }
2568 }
2569}