baobao_codegen/pipeline/phases/validate/lints/
command_naming.rs1use baobao_manifest::Manifest;
4
5use super::super::Lint;
6use crate::pipeline::Diagnostic;
7
8pub struct CommandNamingLint;
13
14impl Lint for CommandNamingLint {
15 fn name(&self) -> &'static str {
16 "command-naming"
17 }
18
19 fn description(&self) -> &'static str {
20 "Check command names follow kebab-case conventions"
21 }
22
23 fn check(&self, manifest: &Manifest, diagnostics: &mut Vec<Diagnostic>) {
24 for (name, cmd) in &manifest.commands {
25 check_command_name(name, name, diagnostics);
26 check_subcommand_names(name, cmd, diagnostics);
27 }
28 }
29}
30
31fn is_kebab_case(s: &str) -> bool {
32 if s.is_empty() {
33 return false;
34 }
35 let mut chars = s.chars().peekable();
37 match chars.next() {
38 Some(c) if c.is_ascii_lowercase() => {}
39 _ => return false,
40 }
41 let mut prev_hyphen = false;
43 for c in chars {
44 if c == '-' {
45 if prev_hyphen {
46 return false; }
48 prev_hyphen = true;
49 } else if c.is_ascii_lowercase() || c.is_ascii_digit() {
50 prev_hyphen = false;
51 } else {
52 return false; }
54 }
55 !prev_hyphen
57}
58
59fn check_command_name(name: &str, path: &str, diagnostics: &mut Vec<Diagnostic>) {
60 if !is_kebab_case(name) {
61 diagnostics.push(
62 Diagnostic::warning(
63 "validate",
64 format!(
65 "command '{}' should use kebab-case (e.g., 'my-command' not 'my_command' or 'myCommand')",
66 name
67 ),
68 )
69 .at(format!("commands.{}", path)),
70 );
71 }
72}
73
74fn check_subcommand_names(
75 parent_path: &str,
76 cmd: &baobao_manifest::Command,
77 diagnostics: &mut Vec<Diagnostic>,
78) {
79 for (name, subcmd) in &cmd.commands {
80 let path = format!("{}.{}", parent_path, name);
81 check_command_name(name, &path, diagnostics);
82 check_subcommand_names(&path, subcmd, diagnostics);
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn parse_manifest(content: &str) -> Manifest {
91 toml::from_str(content).expect("Failed to parse test manifest")
92 }
93
94 #[test]
95 fn test_is_kebab_case_valid() {
96 assert!(is_kebab_case("deploy"));
97 assert!(is_kebab_case("run-migrations"));
98 assert!(is_kebab_case("db-migrate"));
99 assert!(is_kebab_case("a"));
100 assert!(is_kebab_case("a1"));
101 assert!(is_kebab_case("foo-bar-baz"));
102 assert!(is_kebab_case("v2-api"));
103 }
104
105 #[test]
106 fn test_is_kebab_case_invalid() {
107 assert!(!is_kebab_case("")); assert!(!is_kebab_case("Deploy")); assert!(!is_kebab_case("runMigrations")); assert!(!is_kebab_case("run_migrations")); assert!(!is_kebab_case("run--migrations")); assert!(!is_kebab_case("-deploy")); assert!(!is_kebab_case("deploy-")); assert!(!is_kebab_case("1deploy")); assert!(!is_kebab_case("de ploy")); }
117
118 #[test]
119 fn test_valid_names() {
120 let manifest = parse_manifest(
121 r#"
122 [cli]
123 name = "test"
124 language = "rust"
125
126 [commands.deploy]
127 description = "Deploy"
128
129 [commands.run-migrations]
130 description = "Run migrations"
131 "#,
132 );
133
134 let mut diagnostics = Vec::new();
135 CommandNamingLint.check(&manifest, &mut diagnostics);
136
137 assert!(diagnostics.is_empty());
138 }
139
140 #[test]
141 fn test_invalid_names() {
142 let manifest = parse_manifest(
143 r#"
144 [cli]
145 name = "test"
146 language = "rust"
147
148 [commands.runMigrations]
149 description = "Run migrations"
150
151 [commands.deploy_now]
152 description = "Deploy now"
153 "#,
154 );
155
156 let mut diagnostics = Vec::new();
157 CommandNamingLint.check(&manifest, &mut diagnostics);
158
159 assert_eq!(diagnostics.len(), 2);
160 assert!(diagnostics.iter().all(|d| d.severity.is_warning()));
161 assert!(
162 diagnostics
163 .iter()
164 .any(|d| d.message.contains("runMigrations"))
165 );
166 assert!(diagnostics.iter().any(|d| d.message.contains("deploy_now")));
167 }
168
169 #[test]
170 fn test_nested_invalid_name() {
171 let manifest = parse_manifest(
172 r#"
173 [cli]
174 name = "test"
175 language = "rust"
176
177 [commands.db]
178 description = "Database commands"
179
180 [commands.db.commands.runMigration]
181 description = "Run migration"
182 "#,
183 );
184
185 let mut diagnostics = Vec::new();
186 CommandNamingLint.check(&manifest, &mut diagnostics);
187
188 assert_eq!(diagnostics.len(), 1);
189 assert!(diagnostics[0].message.contains("runMigration"));
190 }
191}