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