baobao_codegen/pipeline/phases/validate/lints/
duplicate_command.rs1use std::collections::HashMap;
4
5use baobao_manifest::Manifest;
6
7use super::super::Lint;
8use crate::pipeline::Diagnostic;
9
10pub struct DuplicateCommandLint;
12
13impl Lint for DuplicateCommandLint {
14 fn name(&self) -> &'static str {
15 "duplicate-command"
16 }
17
18 fn description(&self) -> &'static str {
19 "Detect duplicate command names and paths"
20 }
21
22 fn check(&self, manifest: &Manifest, diagnostics: &mut Vec<Diagnostic>) {
23 let mut seen: HashMap<String, String> = HashMap::new();
24
25 for (name, cmd) in &manifest.commands {
26 let normalized = name.to_lowercase();
28 if let Some(first) = seen.get(&normalized) {
29 diagnostics.push(
30 Diagnostic::error(
31 "validate",
32 format!("duplicate command '{}' (conflicts with '{}')", name, first),
33 )
34 .at(format!("commands.{}", name)),
35 );
36 } else {
37 seen.insert(normalized, name.clone());
38 }
39
40 collect_subcommand_paths(name, cmd, &mut seen, diagnostics);
41 }
42 }
43}
44
45fn collect_subcommand_paths(
46 parent_path: &str,
47 cmd: &baobao_manifest::Command,
48 seen: &mut HashMap<String, String>,
49 diagnostics: &mut Vec<Diagnostic>,
50) {
51 for (name, subcmd) in &cmd.commands {
52 let path = format!("{}/{}", parent_path, name);
53 let normalized = path.to_lowercase();
54
55 if let Some(first) = seen.get(&normalized) {
56 diagnostics.push(
57 Diagnostic::error(
58 "validate",
59 format!(
60 "duplicate command path '{}' (conflicts with '{}')",
61 path, first
62 ),
63 )
64 .at(format!("commands.{}", path.replace('/', "."))),
65 );
66 } else {
67 seen.insert(normalized, path.clone());
68 }
69
70 collect_subcommand_paths(&path, subcmd, seen, diagnostics);
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn parse_manifest(content: &str) -> Manifest {
79 toml::from_str(content).expect("Failed to parse test manifest")
80 }
81
82 #[test]
83 fn test_no_duplicates() {
84 let manifest = parse_manifest(
85 r#"
86 [cli]
87 name = "test"
88 language = "rust"
89
90 [commands.deploy]
91 description = "Deploy"
92
93 [commands.build]
94 description = "Build"
95 "#,
96 );
97
98 let mut diagnostics = Vec::new();
99 DuplicateCommandLint.check(&manifest, &mut diagnostics);
100
101 assert!(diagnostics.is_empty());
102 }
103
104 #[test]
105 fn test_nested_paths_distinct() {
106 let manifest = parse_manifest(
107 r#"
108 [cli]
109 name = "test"
110 language = "rust"
111
112 [commands.db]
113 description = "Database commands"
114
115 [commands.db.commands.migrate]
116 description = "Migrate"
117
118 [commands.api]
119 description = "API commands"
120
121 [commands.api.commands.migrate]
122 description = "Another migrate"
123 "#,
124 );
125
126 let mut diagnostics = Vec::new();
127 DuplicateCommandLint.check(&manifest, &mut diagnostics);
128
129 assert!(diagnostics.is_empty());
131 }
132}