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
18impl ToolRegistry {
19 pub fn new(user_tools: BTreeMap<String, ToolDefinition>) -> Self {
21 Self { user_tools }
22 }
23
24 pub fn get(&self, tool_id: &str) -> Option<&ToolDefinition> {
28 self.user_tools.get(tool_id).or_else(|| BUILTIN_TOOLS.get(tool_id))
29 }
30
31 pub fn contains(&self, tool_id: &str) -> bool {
33 self.user_tools.contains_key(tool_id) || BUILTIN_TOOLS.contains_key(tool_id)
34 }
35
36 pub fn list_tools(&self) -> Vec<&str> {
38 let mut tools: Vec<&str> = self.user_tools.keys().map(std::string::String::as_str).collect();
39 for key in BUILTIN_TOOLS.keys() {
40 if !self.user_tools.contains_key(*key) {
41 tools.push(key);
42 }
43 }
44 tools.sort_unstable();
45 tools
46 }
47}
48
49pub fn builtin_tool_ids() -> Vec<&'static str> {
56 let mut ids: Vec<&'static str> = BUILTIN_TOOLS.keys().copied().collect();
57 ids.sort_unstable();
58 ids
59}
60
61impl Default for ToolRegistry {
62 fn default() -> Self {
63 Self::new(BTreeMap::new())
64 }
65}
66
67static BUILTIN_TOOLS: LazyLock<HashMap<&'static str, ToolDefinition>> = LazyLock::new(|| {
71 let mut m = HashMap::new();
72
73 m.insert(
75 "ruff:check",
76 ToolDefinition {
77 command: vec![
78 "ruff".to_string(),
79 "check".to_string(),
80 "--output-format=concise".to_string(),
81 "--stdin-filename=_.py".to_string(),
82 "-".to_string(),
83 ],
84 stdin: true,
85 stdout: true,
86 lint_args: vec![],
87 format_args: vec![],
88 },
89 );
90
91 m.insert(
92 "ruff:format",
93 ToolDefinition {
94 command: vec![
95 "ruff".to_string(),
96 "format".to_string(),
97 "--stdin-filename=_.py".to_string(),
98 "-".to_string(),
99 ],
100 stdin: true,
101 stdout: true,
102 lint_args: vec![],
103 format_args: vec![],
104 },
105 );
106
107 m.insert(
109 "black",
110 ToolDefinition {
111 command: vec!["black".to_string(), "--quiet".to_string(), "-".to_string()],
112 stdin: true,
113 stdout: true,
114 lint_args: vec!["--check".to_string()],
115 format_args: vec![],
116 },
117 );
118
119 m.insert(
121 "prettier",
122 ToolDefinition {
123 command: vec!["prettier".to_string(), "--stdin-filepath=_.js".to_string()],
124 stdin: true,
125 stdout: true,
126 lint_args: vec!["--check".to_string()],
127 format_args: vec![],
128 },
129 );
130
131 m.insert(
132 "prettier:json",
133 ToolDefinition {
134 command: vec!["prettier".to_string(), "--stdin-filepath=_.json".to_string()],
135 stdin: true,
136 stdout: true,
137 lint_args: vec!["--check".to_string()],
138 format_args: vec![],
139 },
140 );
141
142 m.insert(
143 "prettier:yaml",
144 ToolDefinition {
145 command: vec!["prettier".to_string(), "--stdin-filepath=_.yaml".to_string()],
146 stdin: true,
147 stdout: true,
148 lint_args: vec!["--check".to_string()],
149 format_args: vec![],
150 },
151 );
152
153 m.insert(
154 "prettier:html",
155 ToolDefinition {
156 command: vec!["prettier".to_string(), "--stdin-filepath=_.html".to_string()],
157 stdin: true,
158 stdout: true,
159 lint_args: vec!["--check".to_string()],
160 format_args: vec![],
161 },
162 );
163
164 m.insert(
165 "prettier:css",
166 ToolDefinition {
167 command: vec!["prettier".to_string(), "--stdin-filepath=_.css".to_string()],
168 stdin: true,
169 stdout: true,
170 lint_args: vec!["--check".to_string()],
171 format_args: vec![],
172 },
173 );
174
175 m.insert(
176 "prettier:markdown",
177 ToolDefinition {
178 command: vec!["prettier".to_string(), "--stdin-filepath=_.md".to_string()],
179 stdin: true,
180 stdout: true,
181 lint_args: vec!["--check".to_string()],
182 format_args: vec![],
183 },
184 );
185
186 m.insert(
191 "shellcheck",
192 ToolDefinition {
193 command: vec!["shellcheck".to_string(), "--shell=bash".to_string(), "-".to_string()],
194 stdin: true,
195 stdout: true,
196 lint_args: vec![],
197 format_args: vec![],
198 },
199 );
200
201 m.insert(
203 "shfmt",
204 ToolDefinition {
205 command: vec!["shfmt".to_string()],
206 stdin: true,
207 stdout: true,
208 lint_args: vec!["-d".to_string()], format_args: vec![],
210 },
211 );
212
213 m.insert(
215 "rustfmt",
216 ToolDefinition {
217 command: vec!["rustfmt".to_string()],
218 stdin: true,
219 stdout: true,
220 lint_args: vec!["--check".to_string()],
221 format_args: vec![],
222 },
223 );
224
225 m.insert(
227 "gofmt",
228 ToolDefinition {
229 command: vec!["gofmt".to_string()],
230 stdin: true,
231 stdout: true,
232 lint_args: vec!["-d".to_string()], format_args: vec![],
234 },
235 );
236
237 m.insert(
239 "goimports",
240 ToolDefinition {
241 command: vec!["goimports".to_string()],
242 stdin: true,
243 stdout: true,
244 lint_args: vec!["-d".to_string()],
245 format_args: vec![],
246 },
247 );
248
249 m.insert(
251 "clang-format",
252 ToolDefinition {
253 command: vec!["clang-format".to_string()],
254 stdin: true,
255 stdout: true,
256 lint_args: vec!["--dry-run".to_string(), "--Werror".to_string()],
257 format_args: vec![],
258 },
259 );
260
261 m.insert(
264 "sqlfluff:lint",
265 ToolDefinition {
266 command: vec![
267 "sqlfluff".to_string(),
268 "lint".to_string(),
269 "--dialect".to_string(),
270 "ansi".to_string(),
271 "-".to_string(),
272 ],
273 stdin: true,
274 stdout: true,
275 lint_args: vec![],
276 format_args: vec![],
277 },
278 );
279
280 m.insert(
281 "sqlfluff:fix",
282 ToolDefinition {
283 command: vec![
284 "sqlfluff".to_string(),
285 "fix".to_string(),
286 "--dialect".to_string(),
287 "ansi".to_string(),
288 "-".to_string(),
289 ],
290 stdin: true,
291 stdout: true,
292 lint_args: vec![],
293 format_args: vec![],
294 },
295 );
296
297 m.insert(
299 "jq",
300 ToolDefinition {
301 command: vec!["jq".to_string(), ".".to_string()],
302 stdin: true,
303 stdout: true,
304 lint_args: vec![],
305 format_args: vec![],
306 },
307 );
308
309 m.insert(
311 "yamlfmt",
312 ToolDefinition {
313 command: vec!["yamlfmt".to_string()],
314 stdin: true,
315 stdout: true,
316 lint_args: vec!["-lint".to_string(), "-".to_string()],
317 format_args: vec!["-".to_string()],
318 },
319 );
320
321 m.insert(
323 "taplo",
324 ToolDefinition {
325 command: vec!["taplo".to_string(), "fmt".to_string(), "-".to_string()],
326 stdin: true,
327 stdout: true,
328 lint_args: vec!["--check".to_string()],
329 format_args: vec![],
330 },
331 );
332
333 m.insert(
335 "terraform-fmt",
336 ToolDefinition {
337 command: vec!["terraform".to_string(), "fmt".to_string(), "-".to_string()],
338 stdin: true,
339 stdout: true,
340 lint_args: vec!["-check".to_string()],
341 format_args: vec![],
342 },
343 );
344
345 m.insert(
347 "nixfmt",
348 ToolDefinition {
349 command: vec!["nixfmt".to_string(), "-".to_string()],
350 stdin: true,
351 stdout: true,
352 lint_args: vec!["--check".to_string()],
353 format_args: vec![],
354 },
355 );
356
357 m.insert(
359 "stylua",
360 ToolDefinition {
361 command: vec!["stylua".to_string(), "-".to_string()],
362 stdin: true,
363 stdout: true,
364 lint_args: vec!["--check".to_string()],
365 format_args: vec![],
366 },
367 );
368
369 m.insert(
371 "ormolu",
372 ToolDefinition {
373 command: vec!["ormolu".to_string(), "--stdin-input-file=_.hs".to_string()],
374 stdin: true,
375 stdout: true,
376 lint_args: vec!["--check-idempotence".to_string()],
377 format_args: vec![],
378 },
379 );
380
381 m.insert(
383 "elm-format",
384 ToolDefinition {
385 command: vec!["elm-format".to_string(), "--stdin".to_string()],
386 stdin: true,
387 stdout: true,
388 lint_args: vec!["--validate".to_string()],
389 format_args: vec![],
390 },
391 );
392
393 m.insert(
395 "swift-format",
396 ToolDefinition {
397 command: vec!["swift-format".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 "ktfmt",
408 ToolDefinition {
409 command: vec!["ktfmt".to_string(), "-".to_string()],
410 stdin: true,
411 stdout: true,
412 lint_args: vec![],
413 format_args: vec![],
414 },
415 );
416
417 m.insert(
419 "djlint",
420 ToolDefinition {
421 command: vec!["djlint".to_string(), "-".to_string()],
422 stdin: true,
423 stdout: true,
424 lint_args: vec![],
425 format_args: vec!["--reformat".to_string()],
426 },
427 );
428
429 m.insert(
430 "djlint:lint",
431 ToolDefinition {
432 command: vec!["djlint".to_string(), "-".to_string()],
433 stdin: true,
434 stdout: true,
435 lint_args: vec![],
436 format_args: vec![],
437 },
438 );
439
440 m.insert(
441 "djlint:reformat",
442 ToolDefinition {
443 command: vec!["djlint".to_string(), "-".to_string(), "--reformat".to_string()],
444 stdin: true,
445 stdout: true,
446 lint_args: vec![],
447 format_args: vec![],
448 },
449 );
450
451 m.insert(
453 "beautysh",
454 ToolDefinition {
455 command: vec!["beautysh".to_string(), "-".to_string()],
456 stdin: true,
457 stdout: true,
458 lint_args: vec!["--check".to_string()],
459 format_args: vec![],
460 },
461 );
462
463 m.insert(
465 "tombi",
466 ToolDefinition {
467 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
468 stdin: true,
469 stdout: true,
470 lint_args: vec![],
471 format_args: vec![],
472 },
473 );
474
475 m.insert(
476 "tombi:format",
477 ToolDefinition {
478 command: vec!["tombi".to_string(), "format".to_string(), "-".to_string()],
479 stdin: true,
480 stdout: true,
481 lint_args: vec![],
482 format_args: vec![],
483 },
484 );
485
486 m.insert(
487 "tombi:lint",
488 ToolDefinition {
489 command: vec!["tombi".to_string(), "lint".to_string(), "-".to_string()],
490 stdin: true,
491 stdout: true,
492 lint_args: vec![],
493 format_args: vec![],
494 },
495 );
496
497 m.insert(
499 "oxfmt",
500 ToolDefinition {
501 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
502 stdin: true,
503 stdout: true,
504 lint_args: vec!["--check".to_string()],
505 format_args: vec![],
506 },
507 );
508
509 m.insert(
510 "oxfmt:js",
511 ToolDefinition {
512 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.js".to_string()],
513 stdin: true,
514 stdout: true,
515 lint_args: vec!["--check".to_string()],
516 format_args: vec![],
517 },
518 );
519
520 m.insert(
521 "oxfmt:ts",
522 ToolDefinition {
523 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.ts".to_string()],
524 stdin: true,
525 stdout: true,
526 lint_args: vec!["--check".to_string()],
527 format_args: vec![],
528 },
529 );
530
531 m.insert(
532 "oxfmt:jsx",
533 ToolDefinition {
534 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.jsx".to_string()],
535 stdin: true,
536 stdout: true,
537 lint_args: vec!["--check".to_string()],
538 format_args: vec![],
539 },
540 );
541
542 m.insert(
543 "oxfmt:tsx",
544 ToolDefinition {
545 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.tsx".to_string()],
546 stdin: true,
547 stdout: true,
548 lint_args: vec!["--check".to_string()],
549 format_args: vec![],
550 },
551 );
552
553 m.insert(
554 "oxfmt:json",
555 ToolDefinition {
556 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.json".to_string()],
557 stdin: true,
558 stdout: true,
559 lint_args: vec!["--check".to_string()],
560 format_args: vec![],
561 },
562 );
563
564 m.insert(
565 "oxfmt:css",
566 ToolDefinition {
567 command: vec!["oxfmt".to_string(), "--stdin-filepath=_.css".to_string()],
568 stdin: true,
569 stdout: true,
570 lint_args: vec!["--check".to_string()],
571 format_args: vec![],
572 },
573 );
574
575 let deno_fmt = |ext: &str| ToolDefinition {
578 command: vec![
579 "deno".to_string(),
580 "fmt".to_string(),
581 format!("--ext={ext}"),
582 "-".to_string(),
583 ],
584 stdin: true,
585 stdout: true,
586 lint_args: vec!["--check".to_string()],
587 format_args: vec![],
588 };
589 m.insert("deno-fmt", deno_fmt("ts"));
590 m.insert("deno-fmt:ts", deno_fmt("ts"));
591 m.insert("deno-fmt:js", deno_fmt("js"));
592 m.insert("deno-fmt:json", deno_fmt("json"));
593 m.insert("deno-fmt:jsonc", deno_fmt("jsonc"));
594 m.insert("deno-fmt:md", deno_fmt("md"));
595
596 m
597});
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601enum ToolKind {
602 Lint,
603 Format,
604 Both,
605}
606
607impl ToolKind {
608 const fn label(self) -> &'static str {
609 match self {
610 ToolKind::Lint => "Lint",
611 ToolKind::Format => "Format",
612 ToolKind::Both => "Both",
613 }
614 }
615}
616
617struct ToolDocMeta {
631 id: &'static str,
632 language: &'static str,
633 kind: ToolKind,
634 doc_group: &'static str,
636 display_command: Option<&'static str>,
638 runtime: bool,
640}
641
642const BUILTIN_TOOLS_DOCS: &[ToolDocMeta] = &[
644 ToolDocMeta {
645 id: "ruff:check",
646 language: "Python",
647 kind: ToolKind::Lint,
648 doc_group: "ruff:check",
649 display_command: Some("ruff check --output-format=concise -"),
650 runtime: true,
651 },
652 ToolDocMeta {
653 id: "ruff:format",
654 language: "Python",
655 kind: ToolKind::Format,
656 doc_group: "ruff:format",
657 display_command: Some("ruff format -"),
658 runtime: true,
659 },
660 ToolDocMeta {
661 id: "black",
662 language: "Python",
663 kind: ToolKind::Format,
664 doc_group: "black",
665 display_command: None,
666 runtime: true,
667 },
668 ToolDocMeta {
669 id: "prettier",
670 language: "Multi",
671 kind: ToolKind::Format,
672 doc_group: "prettier",
673 display_command: Some("prettier --stdin-filepath=_.EXT"),
674 runtime: true,
675 },
676 ToolDocMeta {
677 id: "prettier:json",
678 language: "Multi",
679 kind: ToolKind::Format,
680 doc_group: "prettier",
681 display_command: Some("prettier --stdin-filepath=_.EXT"),
682 runtime: true,
683 },
684 ToolDocMeta {
685 id: "prettier:yaml",
686 language: "Multi",
687 kind: ToolKind::Format,
688 doc_group: "prettier",
689 display_command: Some("prettier --stdin-filepath=_.EXT"),
690 runtime: true,
691 },
692 ToolDocMeta {
693 id: "prettier:html",
694 language: "Multi",
695 kind: ToolKind::Format,
696 doc_group: "prettier",
697 display_command: Some("prettier --stdin-filepath=_.EXT"),
698 runtime: true,
699 },
700 ToolDocMeta {
701 id: "prettier:css",
702 language: "Multi",
703 kind: ToolKind::Format,
704 doc_group: "prettier",
705 display_command: Some("prettier --stdin-filepath=_.EXT"),
706 runtime: true,
707 },
708 ToolDocMeta {
709 id: "prettier:markdown",
710 language: "Multi",
711 kind: ToolKind::Format,
712 doc_group: "prettier",
713 display_command: Some("prettier --stdin-filepath=_.EXT"),
714 runtime: true,
715 },
716 ToolDocMeta {
717 id: "shellcheck",
718 language: "Shell",
719 kind: ToolKind::Lint,
720 doc_group: "shellcheck",
721 display_command: None,
722 runtime: true,
723 },
724 ToolDocMeta {
725 id: "shfmt",
726 language: "Shell",
727 kind: ToolKind::Format,
728 doc_group: "shfmt",
729 display_command: None,
730 runtime: true,
731 },
732 ToolDocMeta {
733 id: "rustfmt",
734 language: "Rust",
735 kind: ToolKind::Format,
736 doc_group: "rustfmt",
737 display_command: None,
738 runtime: true,
739 },
740 ToolDocMeta {
741 id: "gofmt",
742 language: "Go",
743 kind: ToolKind::Format,
744 doc_group: "gofmt",
745 display_command: None,
746 runtime: true,
747 },
748 ToolDocMeta {
749 id: "goimports",
750 language: "Go",
751 kind: ToolKind::Format,
752 doc_group: "goimports",
753 display_command: None,
754 runtime: true,
755 },
756 ToolDocMeta {
757 id: "clang-format",
758 language: "C/C++",
759 kind: ToolKind::Format,
760 doc_group: "clang-format",
761 display_command: None,
762 runtime: true,
763 },
764 ToolDocMeta {
765 id: "sqlfluff:lint",
766 language: "SQL",
767 kind: ToolKind::Lint,
768 doc_group: "sqlfluff:lint",
769 display_command: None,
770 runtime: true,
771 },
772 ToolDocMeta {
773 id: "sqlfluff:fix",
774 language: "SQL",
775 kind: ToolKind::Format,
776 doc_group: "sqlfluff:fix",
777 display_command: None,
778 runtime: true,
779 },
780 ToolDocMeta {
781 id: "jq",
782 language: "JSON",
783 kind: ToolKind::Both,
784 doc_group: "jq",
785 display_command: None,
786 runtime: true,
787 },
788 ToolDocMeta {
789 id: "yamlfmt",
790 language: "YAML",
791 kind: ToolKind::Format,
792 doc_group: "yamlfmt",
793 display_command: None,
794 runtime: true,
795 },
796 ToolDocMeta {
797 id: "taplo",
798 language: "TOML",
799 kind: ToolKind::Format,
800 doc_group: "taplo",
801 display_command: None,
802 runtime: true,
803 },
804 ToolDocMeta {
805 id: "terraform-fmt",
806 language: "Terraform",
807 kind: ToolKind::Format,
808 doc_group: "terraform-fmt",
809 display_command: None,
810 runtime: true,
811 },
812 ToolDocMeta {
813 id: "nixfmt",
814 language: "Nix",
815 kind: ToolKind::Format,
816 doc_group: "nixfmt",
817 display_command: None,
818 runtime: true,
819 },
820 ToolDocMeta {
821 id: "stylua",
822 language: "Lua",
823 kind: ToolKind::Format,
824 doc_group: "stylua",
825 display_command: None,
826 runtime: true,
827 },
828 ToolDocMeta {
829 id: "ormolu",
830 language: "Haskell",
831 kind: ToolKind::Format,
832 doc_group: "ormolu",
833 display_command: None,
834 runtime: true,
835 },
836 ToolDocMeta {
837 id: "elm-format",
838 language: "Elm",
839 kind: ToolKind::Format,
840 doc_group: "elm-format",
841 display_command: None,
842 runtime: true,
843 },
844 ToolDocMeta {
845 id: "swift-format",
846 language: "Swift",
847 kind: ToolKind::Format,
848 doc_group: "swift-format",
849 display_command: None,
850 runtime: true,
851 },
852 ToolDocMeta {
853 id: "ktfmt",
854 language: "Kotlin",
855 kind: ToolKind::Format,
856 doc_group: "ktfmt",
857 display_command: None,
858 runtime: true,
859 },
860 ToolDocMeta {
861 id: "djlint",
862 language: "Jinja/HTML",
863 kind: ToolKind::Both,
864 doc_group: "djlint",
865 display_command: None,
866 runtime: true,
867 },
868 ToolDocMeta {
869 id: "djlint:lint",
870 language: "Jinja/HTML",
871 kind: ToolKind::Lint,
872 doc_group: "djlint:lint",
873 display_command: None,
874 runtime: true,
875 },
876 ToolDocMeta {
877 id: "djlint:reformat",
878 language: "Jinja/HTML",
879 kind: ToolKind::Format,
880 doc_group: "djlint:reformat",
881 display_command: None,
882 runtime: true,
883 },
884 ToolDocMeta {
885 id: "beautysh",
886 language: "Shell",
887 kind: ToolKind::Both,
888 doc_group: "beautysh",
889 display_command: None,
890 runtime: true,
891 },
892 ToolDocMeta {
893 id: "tombi",
894 language: "TOML",
895 kind: ToolKind::Lint,
896 doc_group: "tombi",
897 display_command: None,
898 runtime: true,
899 },
900 ToolDocMeta {
901 id: "tombi:format",
902 language: "TOML",
903 kind: ToolKind::Format,
904 doc_group: "tombi:format",
905 display_command: None,
906 runtime: true,
907 },
908 ToolDocMeta {
909 id: "tombi:lint",
910 language: "TOML",
911 kind: ToolKind::Lint,
912 doc_group: "tombi:lint",
913 display_command: None,
914 runtime: true,
915 },
916 ToolDocMeta {
917 id: "oxfmt",
918 language: "Multi",
919 kind: ToolKind::Format,
920 doc_group: "oxfmt",
921 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
922 runtime: true,
923 },
924 ToolDocMeta {
925 id: "oxfmt:js",
926 language: "Multi",
927 kind: ToolKind::Format,
928 doc_group: "oxfmt",
929 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
930 runtime: true,
931 },
932 ToolDocMeta {
933 id: "oxfmt:ts",
934 language: "Multi",
935 kind: ToolKind::Format,
936 doc_group: "oxfmt",
937 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
938 runtime: true,
939 },
940 ToolDocMeta {
941 id: "oxfmt:jsx",
942 language: "Multi",
943 kind: ToolKind::Format,
944 doc_group: "oxfmt",
945 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
946 runtime: true,
947 },
948 ToolDocMeta {
949 id: "oxfmt:tsx",
950 language: "Multi",
951 kind: ToolKind::Format,
952 doc_group: "oxfmt",
953 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
954 runtime: true,
955 },
956 ToolDocMeta {
957 id: "oxfmt:json",
958 language: "Multi",
959 kind: ToolKind::Format,
960 doc_group: "oxfmt",
961 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
962 runtime: true,
963 },
964 ToolDocMeta {
965 id: "oxfmt:css",
966 language: "Multi",
967 kind: ToolKind::Format,
968 doc_group: "oxfmt",
969 display_command: Some("oxfmt --stdin-filepath=_.EXT"),
970 runtime: true,
971 },
972 ToolDocMeta {
973 id: "deno-fmt",
974 language: "Multi",
975 kind: ToolKind::Format,
976 doc_group: "deno-fmt",
977 display_command: Some("deno fmt --ext=EXT -"),
978 runtime: true,
979 },
980 ToolDocMeta {
981 id: "deno-fmt:ts",
982 language: "Multi",
983 kind: ToolKind::Format,
984 doc_group: "deno-fmt",
985 display_command: Some("deno fmt --ext=EXT -"),
986 runtime: true,
987 },
988 ToolDocMeta {
989 id: "deno-fmt:js",
990 language: "Multi",
991 kind: ToolKind::Format,
992 doc_group: "deno-fmt",
993 display_command: Some("deno fmt --ext=EXT -"),
994 runtime: true,
995 },
996 ToolDocMeta {
997 id: "deno-fmt:json",
998 language: "Multi",
999 kind: ToolKind::Format,
1000 doc_group: "deno-fmt",
1001 display_command: Some("deno fmt --ext=EXT -"),
1002 runtime: true,
1003 },
1004 ToolDocMeta {
1005 id: "deno-fmt:jsonc",
1006 language: "Multi",
1007 kind: ToolKind::Format,
1008 doc_group: "deno-fmt",
1009 display_command: Some("deno fmt --ext=EXT -"),
1010 runtime: true,
1011 },
1012 ToolDocMeta {
1013 id: "deno-fmt:md",
1014 language: "Multi",
1015 kind: ToolKind::Format,
1016 doc_group: "deno-fmt",
1017 display_command: Some("deno fmt --ext=EXT -"),
1018 runtime: true,
1019 },
1020 ToolDocMeta {
1023 id: RUMDL_BUILTIN_TOOL,
1024 language: "Markdown",
1025 kind: ToolKind::Lint,
1026 doc_group: RUMDL_BUILTIN_TOOL,
1027 display_command: Some("built-in markdown linting"),
1028 runtime: false,
1029 },
1030];
1031
1032const TABLE_BEGIN: &str = "<!-- BEGIN builtin-tools (generated) -->";
1034const TABLE_END: &str = "<!-- END builtin-tools (generated) -->";
1035
1036#[derive(Debug, Clone, PartialEq, Eq)]
1038pub enum DocsError {
1039 MissingMarker,
1041 DuplicateMarker,
1043 MarkerOrder,
1045 CountRowMissing,
1047 CountRowAmbiguous,
1049 CountRowMalformed,
1051}
1052
1053impl std::fmt::Display for DocsError {
1054 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1055 let msg = match self {
1056 DocsError::MissingMarker => "missing `<!-- BEGIN/END builtin-tools (generated) -->` marker pair",
1057 DocsError::DuplicateMarker => "duplicate builtin-tools marker",
1058 DocsError::MarkerOrder => "END builtin-tools marker precedes BEGIN",
1059 DocsError::CountRowMissing => "`| Built-in tools` count row not found",
1060 DocsError::CountRowAmbiguous => "multiple `| Built-in tools` count rows found",
1061 DocsError::CountRowMalformed => "`| Built-in tools` count row has an unexpected layout",
1062 };
1063 f.write_str(msg)
1064 }
1065}
1066
1067impl std::error::Error for DocsError {}
1068
1069fn builtin_tools_group_count() -> usize {
1071 let mut seen: Vec<&str> = Vec::new();
1072 for m in BUILTIN_TOOLS_DOCS {
1073 if !seen.contains(&m.doc_group) {
1074 seen.push(m.doc_group);
1075 }
1076 }
1077 seen.len()
1078}
1079
1080pub fn render_builtin_tools_table() -> String {
1086 let headers = ["Tool ID", "Language", "Type", "Command"];
1087 let mut rows: Vec<[String; 4]> = Vec::new();
1088
1089 let mut seen: Vec<&str> = Vec::new();
1090 for m in BUILTIN_TOOLS_DOCS {
1091 if seen.contains(&m.doc_group) {
1092 continue;
1093 }
1094 seen.push(m.doc_group);
1095
1096 let command = match m.display_command {
1099 Some(cmd) => cmd.to_string(),
1100 None if m.runtime => runtime_command_for_kind(m.id, m.kind),
1101 None => String::new(),
1102 };
1103
1104 rows.push([
1105 format!("`{}`", m.doc_group),
1106 m.language.to_string(),
1107 m.kind.label().to_string(),
1108 format!("`{command}`"),
1109 ]);
1110 }
1111
1112 let mut widths = headers.map(str::chars).map(Iterator::count);
1114 for row in &rows {
1115 for (i, cell) in row.iter().enumerate() {
1116 widths[i] = widths[i].max(cell.chars().count());
1117 }
1118 }
1119
1120 let mut out = String::new();
1121 push_table_row(&mut out, &headers.map(String::from), &widths);
1122 let separators = std::array::from_fn(|i| "-".repeat(widths[i]));
1123 push_table_row(&mut out, &separators, &widths);
1124 for row in &rows {
1125 push_table_row(&mut out, row, &widths);
1126 }
1127 out
1128}
1129
1130fn runtime_command_for_kind(id: &str, kind: ToolKind) -> String {
1135 let Some(def) = BUILTIN_TOOLS.get(id) else {
1136 return String::new();
1137 };
1138 let invocation =
1139 |extra: &[String]| -> String { def.command.iter().chain(extra).cloned().collect::<Vec<_>>().join(" ") };
1140 match kind {
1141 ToolKind::Lint => invocation(&def.lint_args),
1142 ToolKind::Format => invocation(&def.format_args),
1143 ToolKind::Both => {
1144 let lint = invocation(&def.lint_args);
1145 let format = invocation(&def.format_args);
1146 if lint == format {
1147 lint
1148 } else {
1149 format!("{lint} / {format}")
1150 }
1151 }
1152 }
1153}
1154
1155fn push_table_row(out: &mut String, cells: &[String; 4], widths: &[usize; 4]) {
1157 out.push('|');
1158 for (i, cell) in cells.iter().enumerate() {
1159 out.push(' ');
1160 out.push_str(cell);
1161 for _ in cell.chars().count()..widths[i] {
1162 out.push(' ');
1163 }
1164 out.push_str(" |");
1165 }
1166 out.push('\n');
1167}
1168
1169pub fn splice_builtin_tools_docs(existing: &str) -> Result<String, DocsError> {
1175 if existing.matches(TABLE_BEGIN).count() == 0 || existing.matches(TABLE_END).count() == 0 {
1176 return Err(DocsError::MissingMarker);
1177 }
1178 if existing.matches(TABLE_BEGIN).count() > 1 || existing.matches(TABLE_END).count() > 1 {
1179 return Err(DocsError::DuplicateMarker);
1180 }
1181 let begin_pos = existing.find(TABLE_BEGIN).unwrap();
1182 let end_pos = existing.find(TABLE_END).unwrap();
1183 if end_pos < begin_pos {
1184 return Err(DocsError::MarkerOrder);
1185 }
1186
1187 let table = render_builtin_tools_table();
1188 let replacement = format!("{TABLE_BEGIN}\n\n{table}\n{TABLE_END}");
1189
1190 let mut result = String::with_capacity(existing.len() + replacement.len());
1191 result.push_str(&existing[..begin_pos]);
1192 result.push_str(&replacement);
1193 result.push_str(&existing[end_pos + TABLE_END.len()..]);
1194
1195 update_builtin_tools_count(&result, builtin_tools_group_count())
1196}
1197
1198fn update_builtin_tools_count(text: &str, count: usize) -> Result<String, DocsError> {
1201 let lines: Vec<&str> = text.lines().collect();
1202 let mut target: Option<usize> = None;
1203 for (i, line) in lines.iter().enumerate() {
1204 if line.trim_start().starts_with("| Built-in tools") {
1205 if target.is_some() {
1206 return Err(DocsError::CountRowAmbiguous);
1207 }
1208 target = Some(i);
1209 }
1210 }
1211 let i = target.ok_or(DocsError::CountRowMissing)?;
1212 let new_line = replace_count_cell(lines[i], count).ok_or(DocsError::CountRowMalformed)?;
1213
1214 let mut out = String::with_capacity(text.len());
1215 for (j, line) in lines.iter().enumerate() {
1216 out.push_str(if j == i { &new_line } else { line });
1217 out.push('\n');
1218 }
1219 if !text.ends_with('\n') {
1220 out.pop();
1221 }
1222 Ok(out)
1223}
1224
1225fn replace_count_cell(line: &str, count: usize) -> Option<String> {
1227 let pipes: Vec<usize> = line.match_indices('|').map(|(i, _)| i).collect();
1228 if pipes.len() < 3 {
1229 return None;
1230 }
1231 let start = pipes[1] + 1;
1232 let end = pipes[2];
1233 let cell = line.get(start..end)?;
1234 let width = cell.len();
1235 let num = count.to_string();
1236
1237 let mut new_cell = String::with_capacity(width.max(num.len() + 2));
1238 new_cell.push(' ');
1239 new_cell.push_str(&num);
1240 while new_cell.len() < width {
1241 new_cell.push(' ');
1242 }
1243 if new_cell.len() > width {
1244 new_cell = format!(" {num} ");
1246 }
1247
1248 Some(format!("{}{}{}", &line[..start], new_cell, &line[end..]))
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253 use super::*;
1254
1255 #[test]
1256 fn test_get_builtin_tool() {
1257 let registry = ToolRegistry::default();
1258
1259 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1260 assert!(tool.command.contains(&"ruff".to_string()));
1261 assert!(tool.stdin);
1262 assert!(tool.stdout);
1263
1264 let tool = registry.get("shellcheck").expect("Should find shellcheck");
1265 assert!(tool.command.contains(&"shellcheck".to_string()));
1266 }
1267
1268 #[test]
1269 fn test_builtin_yamlfmt_lint_command_validates_stdin() {
1270 let registry = ToolRegistry::default();
1271
1272 let tool = registry.get("yamlfmt").expect("Should find yamlfmt");
1273 let mut argv = tool.command.clone();
1274 argv.extend(tool.lint_args.clone());
1275
1276 assert_eq!(argv, vec!["yamlfmt", "-lint", "-"]);
1277 }
1278
1279 #[test]
1280 fn test_get_user_tool_overrides_builtin() {
1281 let mut user_tools = BTreeMap::new();
1282 user_tools.insert(
1283 "ruff:check".to_string(),
1284 ToolDefinition {
1285 command: vec!["custom-ruff".to_string()],
1286 stdin: false,
1287 stdout: false,
1288 lint_args: vec![],
1289 format_args: vec![],
1290 },
1291 );
1292
1293 let registry = ToolRegistry::new(user_tools);
1294
1295 let tool = registry.get("ruff:check").expect("Should find ruff:check");
1296 assert_eq!(tool.command, vec!["custom-ruff"]);
1297 assert!(!tool.stdin); }
1299
1300 #[test]
1301 fn test_contains() {
1302 let registry = ToolRegistry::default();
1303
1304 assert!(registry.contains("ruff:check"));
1305 assert!(registry.contains("prettier"));
1306 assert!(registry.contains("shellcheck"));
1307 assert!(!registry.contains("nonexistent-tool"));
1308 }
1309
1310 #[test]
1311 fn test_list_tools() {
1312 let registry = ToolRegistry::default();
1313 let tools = registry.list_tools();
1314
1315 assert!(tools.contains(&"ruff:check"));
1316 assert!(tools.contains(&"ruff:format"));
1317 assert!(tools.contains(&"prettier"));
1318 assert!(tools.contains(&"shellcheck"));
1319 assert!(tools.contains(&"shfmt"));
1320 assert!(tools.contains(&"rustfmt"));
1321 assert!(tools.contains(&"gofmt"));
1322 }
1323
1324 #[test]
1325 fn test_user_tools_in_list() {
1326 let mut user_tools = BTreeMap::new();
1327 user_tools.insert("my-custom-tool".to_string(), ToolDefinition::default());
1328
1329 let registry = ToolRegistry::new(user_tools);
1330 let tools = registry.list_tools();
1331
1332 assert!(tools.contains(&"my-custom-tool"));
1333 assert!(tools.contains(&"ruff:check")); }
1335
1336 #[test]
1337 fn test_new_builtin_tools() {
1338 let registry = ToolRegistry::default();
1339
1340 let tool = registry.get("djlint").expect("Should find djlint");
1342 assert!(tool.command.contains(&"djlint".to_string()));
1343 assert!(tool.stdin);
1344
1345 let tool = registry.get("beautysh").expect("Should find beautysh");
1347 assert!(tool.command.contains(&"beautysh".to_string()));
1348 assert!(tool.stdin);
1349
1350 let tool = registry.get("tombi").expect("Should find tombi");
1352 assert!(tool.command.contains(&"tombi".to_string()));
1353 assert!(tool.stdin);
1354
1355 let tool = registry.get("tombi:lint").expect("Should find tombi:lint");
1356 assert!(tool.command.contains(&"lint".to_string()));
1357
1358 let tool = registry.get("tombi:format").expect("Should find tombi:format");
1359 assert!(
1360 tool.command.contains(&"format".to_string()),
1361 "tombi:format should use 'format' subcommand, got: {:?}",
1362 tool.command
1363 );
1364
1365 let tool = registry.get("oxfmt").expect("Should find oxfmt");
1367 assert!(tool.command.contains(&"oxfmt".to_string()));
1368 assert!(tool.stdin);
1369
1370 let tool = registry.get("oxfmt:ts").expect("Should find oxfmt:ts");
1371 assert!(tool.command.iter().any(|s| s.contains("_.ts")));
1372 }
1373
1374 #[test]
1382 fn test_bare_tombi_resolves_to_lint_not_format() {
1383 let registry = ToolRegistry::default();
1384
1385 let bare = registry.get("tombi").expect("Should find bare tombi");
1386 let format = registry.get("tombi:format").expect("Should find tombi:format");
1387
1388 assert!(
1390 bare.command.contains(&"lint".to_string()),
1391 "Bare 'tombi' uses lint subcommand: {:?}",
1392 bare.command
1393 );
1394
1395 assert!(
1397 format.command.contains(&"format".to_string()),
1398 "tombi:format uses format subcommand: {:?}",
1399 format.command
1400 );
1401
1402 assert_ne!(
1404 bare.command, format.command,
1405 "Bare 'tombi' and 'tombi:format' should have different commands (this is the root cause of #527)"
1406 );
1407 }
1408
1409 #[test]
1412 fn test_tools_with_lint_format_variants_are_distinct() {
1413 let registry = ToolRegistry::default();
1414
1415 let ruff_check = registry.get("ruff:check").expect("ruff:check");
1417 let ruff_format = registry.get("ruff:format").expect("ruff:format");
1418 assert_ne!(
1419 ruff_check.command, ruff_format.command,
1420 "ruff:check and ruff:format should be distinct"
1421 );
1422
1423 let tombi_lint = registry.get("tombi:lint").expect("tombi:lint");
1425 let tombi_format = registry.get("tombi:format").expect("tombi:format");
1426 assert_ne!(
1427 tombi_lint.command, tombi_format.command,
1428 "tombi:lint and tombi:format should be distinct"
1429 );
1430 }
1431
1432 #[test]
1433 fn test_deno_fmt_has_per_extension_variants() {
1434 let registry = ToolRegistry::default();
1437
1438 let deno_json = registry.get("deno-fmt:json").expect("deno-fmt:json");
1439 assert!(deno_json.command.iter().any(|a| a == "--ext=json"));
1440 let deno_md = registry.get("deno-fmt:md").expect("deno-fmt:md");
1441 assert!(deno_md.command.iter().any(|a| a == "--ext=md"));
1442
1443 let deno = registry.get("deno-fmt").expect("deno-fmt");
1445 assert!(deno.command.iter().any(|a| a == "--ext=ts"));
1446 }
1447
1448 #[test]
1453 fn test_docs_metadata_ids_unique() {
1454 let mut seen = std::collections::BTreeSet::new();
1455 for m in BUILTIN_TOOLS_DOCS {
1456 assert!(seen.insert(m.id), "duplicate metadata id: {}", m.id);
1457 }
1458 }
1459
1460 #[test]
1461 fn test_runtime_metadata_matches_registry_keys() {
1462 let runtime_meta: std::collections::BTreeSet<&str> =
1463 BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime).map(|m| m.id).collect();
1464 let map_keys: std::collections::BTreeSet<&str> = BUILTIN_TOOLS.keys().copied().collect();
1465 assert_eq!(
1466 runtime_meta, map_keys,
1467 "BUILTIN_TOOLS_DOCS runtime ids must exactly match BUILTIN_TOOLS keys (add/remove the doc entry alongside the registry entry)"
1468 );
1469 }
1470
1471 #[test]
1472 fn test_docs_only_ids_absent_from_registry() {
1473 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
1474 assert!(
1475 !BUILTIN_TOOLS.contains_key(m.id),
1476 "docs-only id {} must not be a runtime registry tool",
1477 m.id
1478 );
1479 }
1480 assert!(
1482 BUILTIN_TOOLS_DOCS
1483 .iter()
1484 .any(|m| !m.runtime && m.id == RUMDL_BUILTIN_TOOL),
1485 "rumdl must be present as a docs-only entry"
1486 );
1487 }
1488
1489 #[test]
1490 fn test_docs_only_requires_display_command() {
1491 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| !m.runtime) {
1492 assert!(
1493 m.display_command.is_some(),
1494 "docs-only id {} needs a display_command (no runtime command to derive)",
1495 m.id
1496 );
1497 }
1498 }
1499
1500 #[test]
1501 fn test_multi_runtime_group_requires_display_command() {
1502 let mut counts: std::collections::BTreeMap<&str, usize> = std::collections::BTreeMap::new();
1503 for m in BUILTIN_TOOLS_DOCS.iter().filter(|m| m.runtime) {
1504 *counts.entry(m.doc_group).or_default() += 1;
1505 }
1506 for m in BUILTIN_TOOLS_DOCS {
1507 if counts.get(m.doc_group).copied().unwrap_or(0) > 1 {
1508 assert!(
1509 m.display_command.is_some(),
1510 "doc_group `{}` has multiple runtime entries; `{}` needs an explicit display_command",
1511 m.doc_group,
1512 m.id
1513 );
1514 }
1515 }
1516 }
1517
1518 #[test]
1519 fn test_doc_group_language_and_kind_consistent() {
1520 let mut groups: std::collections::BTreeMap<&str, (&str, ToolKind)> = std::collections::BTreeMap::new();
1521 for m in BUILTIN_TOOLS_DOCS {
1522 match groups.get(m.doc_group) {
1523 None => {
1524 groups.insert(m.doc_group, (m.language, m.kind));
1525 }
1526 Some((lang, kind)) => {
1527 assert_eq!(
1528 *lang, m.language,
1529 "doc_group `{}` has mismatched languages",
1530 m.doc_group
1531 );
1532 assert_eq!(*kind, m.kind, "doc_group `{}` has mismatched kinds", m.doc_group);
1533 }
1534 }
1535 }
1536 }
1537
1538 fn rendered_rows(table: &str) -> Vec<(String, String, String, String)> {
1545 table
1546 .lines()
1547 .skip(2) .filter(|l| l.starts_with('|'))
1549 .map(|l| {
1550 let cells: Vec<String> = l.trim().trim_matches('|').split('|').map(|c| c.trim().to_string()).collect();
1551 (cells[0].clone(), cells[1].clone(), cells[2].clone(), cells[3].clone())
1552 })
1553 .collect()
1554 }
1555
1556 #[test]
1557 fn test_render_table_golden_rows() {
1558 let table = render_builtin_tools_table();
1559 let rows = rendered_rows(&table);
1560 let has = |id: &str, lang: &str, kind: &str, cmd: &str| {
1561 rows.iter()
1562 .any(|(i, l, k, c)| i == id && l == lang && k == kind && c == cmd)
1563 };
1564 assert!(has("`shellcheck`", "Shell", "Lint", "`shellcheck --shell=bash -`"));
1566 assert!(has("`rustfmt`", "Rust", "Format", "`rustfmt`"));
1567 assert!(has("`jq`", "JSON", "Both", "`jq .`"));
1568 assert!(has(
1569 "`prettier`",
1570 "Multi",
1571 "Format",
1572 "`prettier --stdin-filepath=_.EXT`"
1573 ));
1574 assert!(has("`rumdl`", "Markdown", "Lint", "`built-in markdown linting`"));
1575 assert!(
1577 !table.contains("prettier:json"),
1578 "prettier variants must collapse into one row"
1579 );
1580 assert!(!table.contains("oxfmt:ts"), "oxfmt variants must collapse into one row");
1581 }
1582
1583 #[test]
1584 fn test_render_table_one_row_per_group() {
1585 let table = render_builtin_tools_table();
1586 assert_eq!(rendered_rows(&table).len(), builtin_tools_group_count());
1587 }
1588
1589 #[test]
1590 fn test_render_table_command_fallback_from_registry() {
1591 let table = render_builtin_tools_table();
1593 assert!(
1594 rendered_rows(&table)
1595 .iter()
1596 .any(|(i, _, _, c)| i == "`tombi`" && c == "`tombi lint -`")
1597 );
1598 }
1599
1600 #[test]
1601 fn test_render_command_reflects_mode_args() {
1602 let table = render_builtin_tools_table();
1603 let rows = rendered_rows(&table);
1604 let cmd = |id: &str| {
1605 let row = rows
1606 .iter()
1607 .find(|(i, _, _, _)| i == id)
1608 .unwrap_or_else(|| panic!("row {id} not found"));
1609 row.3.clone()
1610 };
1611 assert_eq!(cmd("`yamlfmt`"), "`yamlfmt -`");
1613 assert_eq!(cmd("`djlint`"), "`djlint - / djlint - --reformat`");
1615 assert_eq!(cmd("`sqlfluff:lint`"), "`sqlfluff lint --dialect ansi -`");
1617 }
1618
1619 #[test]
1620 fn test_runtime_command_for_kind_both_collapses_when_equal() {
1621 assert_eq!(runtime_command_for_kind("jq", ToolKind::Both), "jq .");
1623 }
1624
1625 #[test]
1630 fn test_splice_replaces_region_and_preserves_prose() {
1631 let doc = format!(
1632 "# Title\n\nIntro.\n\n{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\nAfter.\n\n| Built-in tools | 31 | 339 |\n"
1633 );
1634 let out = splice_builtin_tools_docs(&doc).expect("splice");
1635 assert!(out.starts_with("# Title\n\nIntro.\n\n"));
1636 assert!(out.contains("After.\n"));
1637 assert!(!out.contains("stale"));
1638 assert!(
1639 out.contains(&render_builtin_tools_table()),
1640 "generated table is spliced in verbatim"
1641 );
1642 assert!(out.contains(&format!("| Built-in tools | {} | 339 |", builtin_tools_group_count())));
1644 }
1645
1646 #[test]
1647 fn test_splice_missing_marker_errors() {
1648 let doc = "# Title\n\nno markers\n\n| Built-in tools | 31 | 339 |\n";
1649 assert_eq!(splice_builtin_tools_docs(doc), Err(DocsError::MissingMarker));
1650 }
1651
1652 #[test]
1653 fn test_splice_duplicate_marker_errors() {
1654 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n{TABLE_BEGIN}\n\ny\n\n{TABLE_END}\n");
1655 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::DuplicateMarker));
1656 }
1657
1658 #[test]
1659 fn test_splice_marker_order_errors() {
1660 let doc = format!("{TABLE_END}\n\nx\n\n{TABLE_BEGIN}\n\n| Built-in tools | 31 | 339 |\n");
1661 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::MarkerOrder));
1662 }
1663
1664 #[test]
1665 fn test_splice_missing_count_row_errors() {
1666 let doc = format!("{TABLE_BEGIN}\n\nx\n\n{TABLE_END}\n\nno count row here\n");
1667 assert_eq!(splice_builtin_tools_docs(&doc), Err(DocsError::CountRowMissing));
1668 }
1669
1670 #[test]
1671 fn test_splice_is_idempotent() {
1672 let doc = format!("{TABLE_BEGIN}\n\nstale\n\n{TABLE_END}\n\n| Built-in tools | 31 | 339 |\n");
1673 let once = splice_builtin_tools_docs(&doc).expect("first");
1674 let twice = splice_builtin_tools_docs(&once).expect("second");
1675 assert_eq!(once, twice, "splice must be idempotent");
1676 }
1677}