cargo/lints/rules/
non_kebab_case_bins.rs1use std::path::Path;
2
3use annotate_snippets::AnnotationKind;
4use annotate_snippets::Group;
5use annotate_snippets::Level;
6use annotate_snippets::Origin;
7use annotate_snippets::Patch;
8use annotate_snippets::Snippet;
9use cargo_util_schemas::manifest::TomlToolLints;
10
11use crate::CargoResult;
12use crate::GlobalContext;
13use crate::core::Package;
14use crate::core::Workspace;
15use crate::lints::AsIndex;
16use crate::lints::Lint;
17use crate::lints::LintLevel;
18use crate::lints::LintLevelReason;
19use crate::lints::STYLE;
20use crate::lints::get_key_value_span;
21use crate::lints::rel_cwd_manifest_path;
22
23pub static LINT: &Lint = &Lint {
24 name: "non_kebab_case_bins",
25 desc: "binaries should have a kebab-case name",
26 primary_group: &STYLE,
27 msrv: Some(super::CARGO_LINTS_MSRV),
28 edition_lint_opts: None,
29 feature_gate: None,
30 docs: Some(
31 r#"
32### What it does
33
34Detect binary names, explicit and implicit, that are not kebab-case
35
36### Why it is bad
37
38Kebab-case binary names is a common convention among command line tools.
39
40### Drawbacks
41
42It would be disruptive to existing users to change the binary name.
43
44A binary may need to conform to externally controlled conventions which can include a different naming convention.
45
46GUI applications may wish to choose a more user focused naming convention, like "Title Case" or "Sentence case".
47
48### Example
49
50```toml
51[[bin]]
52name = "foo_bar"
53```
54
55Should be written as:
56
57```toml
58[[bin]]
59name = "foo-bar"
60```
61"#,
62 ),
63};
64
65pub fn non_kebab_case_bins(
66 ws: &Workspace<'_>,
67 pkg: &Package,
68 manifest_path: &Path,
69 cargo_lints: &TomlToolLints,
70 error_count: &mut usize,
71 gctx: &GlobalContext,
72) -> CargoResult<()> {
73 let (lint_level, reason) = LINT.level(
74 cargo_lints,
75 pkg.rust_version(),
76 pkg.manifest().edition(),
77 pkg.manifest().unstable_features(),
78 );
79
80 if lint_level == LintLevel::Allow {
81 return Ok(());
82 }
83
84 let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
85
86 lint_package(
87 ws,
88 pkg,
89 &manifest_path,
90 lint_level,
91 reason,
92 error_count,
93 gctx,
94 )
95}
96
97pub fn lint_package(
98 ws: &Workspace<'_>,
99 pkg: &Package,
100 manifest_path: &str,
101 lint_level: LintLevel,
102 reason: LintLevelReason,
103 error_count: &mut usize,
104 gctx: &GlobalContext,
105) -> CargoResult<()> {
106 let manifest = pkg.manifest();
107
108 for (i, bin) in manifest.normalized_toml().bin.iter().flatten().enumerate() {
109 let Some(original_name) = bin.name.as_deref() else {
110 continue;
111 };
112 let kebab_case = heck::ToKebabCase::to_kebab_case(original_name);
113 if kebab_case == original_name {
114 continue;
115 }
116
117 let document = manifest.document();
118 let contents = manifest.contents();
119 let level = lint_level.to_diagnostic_level();
120 let emitted_source = LINT.emitted_source(lint_level, reason);
121
122 let mut primary_source = ws.target_dir().as_path_unlocked().to_owned();
123 primary_source.push("...");
125 primary_source.push("");
126 let mut primary_source = primary_source.display().to_string();
127 let primary_span_start = primary_source.len();
128 let primary_span_end = primary_span_start + original_name.len();
129 primary_source.push_str(original_name);
130 primary_source.push_str(std::env::consts::EXE_SUFFIX);
131 let mut primary_group =
132 level
133 .primary_title(LINT.desc)
134 .element(Snippet::source(&primary_source).annotation(
135 AnnotationKind::Primary.span(primary_span_start..primary_span_end),
136 ));
137 if i == 0 {
138 primary_group = primary_group.element(Level::NOTE.message(emitted_source));
139 }
140 let mut report = vec![primary_group];
141
142 if let Some((i, _target)) = manifest
143 .original_toml()
144 .iter()
145 .flat_map(|m| m.bin.iter().flatten())
146 .enumerate()
147 .find(|(_i, t)| t.name.as_deref() == Some(original_name))
148 {
149 let mut help = Group::with_title(
150 Level::HELP
151 .secondary_title("to change the binary name to kebab case, convert `bin.name`"),
152 );
153 if let Some(document) = document
154 && let Some(contents) = contents
155 && let Some(span) = get_key_value_span(
156 document,
157 &["bin".as_index(), i.as_index(), "name".as_index()],
158 )
159 {
160 help = help.element(
161 Snippet::source(contents)
162 .path(manifest_path)
163 .patch(Patch::new(span.value, format!("\"{kebab_case}\""))),
164 );
165 } else {
166 help = help.element(Origin::path(manifest_path));
167 }
168 report.push(help);
169 } else if is_default_main(bin.path.as_ref())
170 && manifest
171 .original_toml()
172 .iter()
173 .flat_map(|m| m.bin.iter().flatten())
174 .all(|t| t.path != bin.path)
175 && manifest
176 .original_toml()
177 .and_then(|t| t.package.as_ref())
178 .map(|p| p.name.is_some())
179 .unwrap_or(false)
180 {
181 let help_package_name =
184 "to change the binary name to kebab case, convert `package.name`";
185 let help_bin_table = "to change the binary name to kebab case, specify `bin.name`";
189 if let Some(document) = document
190 && let Some(contents) = contents
191 && let Some(span) = get_key_value_span(document, &["package", "name"])
192 {
193 report.push(
194 Level::HELP.secondary_title(help_package_name).element(
195 Snippet::source(contents)
196 .path(manifest_path)
197 .patch(Patch::new(span.value, format!("\"{kebab_case}\""))),
198 ),
199 );
200 report.push(
201 Level::HELP.secondary_title(help_bin_table).element(
202 Snippet::source(contents)
203 .path(manifest_path)
204 .patch(Patch::new(
205 contents.len()..contents.len(),
206 format!(
207 r#"
208[[bin]]
209name = "{kebab_case}"
210path = "src/main.rs""#
211 ),
212 )),
213 ),
214 );
215 } else {
216 report.push(
217 Level::HELP
218 .secondary_title(help_package_name)
219 .element(Origin::path(manifest_path)),
220 );
221 report.push(
222 Level::HELP
223 .secondary_title(help_bin_table)
224 .element(Origin::path(manifest_path)),
225 );
226 }
227 } else {
228 let path = bin
229 .path
230 .as_ref()
231 .expect("normalized have a path")
232 .0
233 .as_path();
234 let display_path = path.as_os_str().to_string_lossy();
235 let end = display_path.len() - if display_path.ends_with(".rs") { 3 } else { 0 };
236 let start = path
237 .parent()
238 .map(|p| {
239 let p = p.as_os_str().to_string_lossy();
240 p.len() + if p.is_empty() { 0 } else { 1 }
242 })
243 .unwrap_or(0);
244 let help = Level::HELP
245 .secondary_title("to change the binary name to kebab case, convert the file stem")
246 .element(Snippet::source(display_path).patch(Patch::new(start..end, kebab_case)));
247 report.push(help);
248 }
249
250 if lint_level.is_error() {
251 *error_count += 1;
252 }
253 gctx.shell().print_report(&report, lint_level.force())?;
254 }
255
256 Ok(())
257}
258
259fn is_default_main(path: Option<&cargo_util_schemas::manifest::PathValue>) -> bool {
260 let Some(path) = path else {
261 return false;
262 };
263 path.0 == std::path::Path::new("src/main.rs")
264}