Skip to content

Commit 317bac7

Browse files
committed
feat(lints): Add non_snake_case_features
1 parent 6a63fdc commit 317bac7

5 files changed

Lines changed: 217 additions & 14 deletions

File tree

src/cargo/core/workspace.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::lints::rules::implicit_minimum_version_req;
2828
use crate::lints::rules::non_kebab_case_bins;
2929
use crate::lints::rules::non_kebab_case_features;
3030
use crate::lints::rules::non_kebab_case_packages;
31+
use crate::lints::rules::non_snake_case_features;
3132
use crate::lints::rules::non_snake_case_packages;
3233
use crate::lints::rules::redundant_readme;
3334
use crate::ops;
@@ -1388,6 +1389,13 @@ impl<'gctx> Workspace<'gctx> {
13881389
&mut run_error_count,
13891390
self.gctx,
13901391
)?;
1392+
non_snake_case_features(
1393+
pkg.into(),
1394+
&path,
1395+
&cargo_lints,
1396+
&mut run_error_count,
1397+
self.gctx,
1398+
)?;
13911399
redundant_readme(
13921400
pkg.into(),
13931401
&path,

src/cargo/lints/rules/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod implicit_minimum_version_req;
44
mod non_kebab_case_bins;
55
mod non_kebab_case_features;
66
mod non_kebab_case_packages;
7+
mod non_snake_case_features;
78
mod non_snake_case_packages;
89
mod redundant_readme;
910
mod unknown_lints;
@@ -14,6 +15,7 @@ pub use implicit_minimum_version_req::implicit_minimum_version_req;
1415
pub use non_kebab_case_bins::non_kebab_case_bins;
1516
pub use non_kebab_case_features::non_kebab_case_features;
1617
pub use non_kebab_case_packages::non_kebab_case_packages;
18+
pub use non_snake_case_features::non_snake_case_features;
1719
pub use non_snake_case_packages::non_snake_case_packages;
1820
pub use redundant_readme::redundant_readme;
1921
pub use unknown_lints::output_unknown_lints;
@@ -25,6 +27,7 @@ pub const LINTS: &[crate::lints::Lint] = &[
2527
non_kebab_case_bins::LINT,
2628
non_kebab_case_features::LINT,
2729
non_kebab_case_packages::LINT,
30+
non_snake_case_features::LINT,
2831
non_snake_case_packages::LINT,
2932
redundant_readme::LINT,
3033
unknown_lints::LINT,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use std::path::Path;
2+
3+
use annotate_snippets::AnnotationKind;
4+
use annotate_snippets::Group;
5+
use annotate_snippets::Level;
6+
use annotate_snippets::Origin;
7+
use annotate_snippets::Patch;
8+
use annotate_snippets::Snippet;
9+
use cargo_util_schemas::manifest::TomlToolLints;
10+
11+
use crate::CargoResult;
12+
use crate::GlobalContext;
13+
use crate::core::Package;
14+
use crate::lints::Lint;
15+
use crate::lints::LintLevel;
16+
use crate::lints::LintLevelReason;
17+
use crate::lints::RESTRICTION;
18+
use crate::lints::get_key_value_span;
19+
use crate::lints::rel_cwd_manifest_path;
20+
21+
pub const LINT: Lint = Lint {
22+
name: "non_snake_case_features",
23+
desc: "features should have a snake-case name",
24+
primary_group: &RESTRICTION,
25+
edition_lint_opts: None,
26+
feature_gate: None,
27+
docs: Some(
28+
r#"
29+
### What it does
30+
31+
Detect feature names that are not snake-case.
32+
33+
### Why it is bad
34+
35+
Having multiple naming styles within a workspace can be confusing.
36+
37+
### Drawbacks
38+
39+
Users would expect that a feature tightly coupled to a dependency would match the dependency's name.
40+
41+
### Example
42+
43+
```toml
44+
[features]
45+
foo-bar = []
46+
```
47+
48+
Should be written as:
49+
50+
```toml
51+
[features]
52+
foo_bar = []
53+
```
54+
"#,
55+
),
56+
};
57+
58+
pub fn non_snake_case_features(
59+
pkg: &Package,
60+
manifest_path: &Path,
61+
cargo_lints: &TomlToolLints,
62+
error_count: &mut usize,
63+
gctx: &GlobalContext,
64+
) -> CargoResult<()> {
65+
let (lint_level, reason) = LINT.level(
66+
cargo_lints,
67+
pkg.manifest().edition(),
68+
pkg.manifest().unstable_features(),
69+
);
70+
71+
if lint_level == LintLevel::Allow {
72+
return Ok(());
73+
}
74+
75+
let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
76+
77+
lint_package(pkg, &manifest_path, lint_level, reason, error_count, gctx)
78+
}
79+
80+
pub fn lint_package(
81+
pkg: &Package,
82+
manifest_path: &str,
83+
lint_level: LintLevel,
84+
reason: LintLevelReason,
85+
error_count: &mut usize,
86+
gctx: &GlobalContext,
87+
) -> CargoResult<()> {
88+
for original_name in pkg.summary().features().keys() {
89+
let original_name = &**original_name;
90+
let snake_case = heck::ToSnakeCase::to_snake_case(original_name);
91+
if snake_case == original_name {
92+
continue;
93+
}
94+
95+
let manifest = pkg.manifest();
96+
let document = manifest.document();
97+
let contents = manifest.contents();
98+
let level = lint_level.to_diagnostic_level();
99+
let emitted_source = LINT.emitted_source(lint_level, reason);
100+
101+
let mut primary = Group::with_title(level.primary_title(LINT.desc));
102+
if let Some(document) = document
103+
&& let Some(contents) = contents
104+
&& let Some(span) = get_key_value_span(document, &["features", original_name])
105+
{
106+
primary = primary.element(
107+
Snippet::source(contents)
108+
.path(manifest_path)
109+
.annotation(AnnotationKind::Primary.span(span.key)),
110+
);
111+
} else if let Some(document) = document
112+
&& let Some(contents) = contents
113+
&& let Some(dep_span) = get_key_value_span(document, &["dependencies", original_name])
114+
&& let Some(optional_span) =
115+
get_key_value_span(document, &["dependencies", original_name, "optional"])
116+
{
117+
primary = primary.element(
118+
Snippet::source(contents)
119+
.path(manifest_path)
120+
.annotation(AnnotationKind::Primary.span(dep_span.key))
121+
.annotation(
122+
AnnotationKind::Context
123+
.span(optional_span.key.start..optional_span.value.end)
124+
.label("implicit feature created for optional dependencies"),
125+
),
126+
);
127+
} else {
128+
primary = primary.element(Origin::path(manifest_path));
129+
}
130+
primary = primary.element(Level::NOTE.message(emitted_source));
131+
let mut report = vec![primary];
132+
if let Some(document) = document
133+
&& let Some(contents) = contents
134+
&& let Some(span) = get_key_value_span(document, &["features", original_name])
135+
{
136+
let mut help = Group::with_title(Level::HELP.secondary_title(
137+
"to change the feature name to snake case, convert the `features` key",
138+
));
139+
help = help.element(
140+
Snippet::source(contents)
141+
.path(manifest_path)
142+
.patch(Patch::new(span.key, snake_case.as_str())),
143+
);
144+
report.push(help);
145+
}
146+
147+
if lint_level.is_error() {
148+
*error_count += 1;
149+
}
150+
gctx.shell().print_report(&report, lint_level.force())?;
151+
}
152+
153+
Ok(())
154+
}

src/doc/src/reference/lints.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ These lints are all set to the 'allow' level by default.
2222
- [`implicit_minimum_version_req`](#implicit_minimum_version_req)
2323
- [`non_kebab_case_features`](#non_kebab_case_features)
2424
- [`non_kebab_case_packages`](#non_kebab_case_packages)
25+
- [`non_snake_case_features`](#non_snake_case_features)
2526
- [`non_snake_case_packages`](#non_snake_case_packages)
2627

2728
## Warn-by-default
@@ -213,6 +214,38 @@ name = "foo-bar"
213214
```
214215

215216

217+
## `non_snake_case_features`
218+
Group: `restriction`
219+
220+
Level: `allow`
221+
222+
### What it does
223+
224+
Detect feature names that are not snake-case.
225+
226+
### Why it is bad
227+
228+
Having multiple naming styles within a workspace can be confusing.
229+
230+
### Drawbacks
231+
232+
Users would expect that a feature tightly coupled to a dependency would match the dependency's name.
233+
234+
### Example
235+
236+
```toml
237+
[features]
238+
foo-bar = []
239+
```
240+
241+
Should be written as:
242+
243+
```toml
244+
[features]
245+
foo_bar = []
246+
```
247+
248+
216249
## `non_snake_case_packages`
217250
Group: `restriction`
218251

tests/testsuite/lints/non_snake_case_features.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,18 @@ non_snake_case_features = "warn"
2828
foo.cargo("check -Zcargo-lints")
2929
.masquerade_as_nightly_cargo(&["cargo-lints", "test-dummy-unstable"])
3030
.with_stderr_data(str![[r#"
31-
[WARNING] unknown lint: `non_snake_case_features`
32-
--> Cargo.toml:12:1
33-
|
34-
12 | non_snake_case_features = "warn"
35-
| ^^^^^^^^^^^^^^^^^^^^^^^
36-
|
37-
= [NOTE] `cargo::unknown_lints` is set to `warn` by default
31+
[WARNING] features should have a snake-case name
32+
--> Cargo.toml:9:1
33+
|
34+
9 | foo-bar = []
35+
| ^^^^^^^
36+
|
37+
= [NOTE] `cargo::non_snake_case_features` is set to `warn` in `[lints]`
38+
[HELP] to change the feature name to snake case, convert the `features` key
39+
|
40+
9 - foo-bar = []
41+
9 + foo_bar = []
42+
|
3843
[CHECKING] foo v0.0.1 ([ROOT]/foo)
3944
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
4045
@@ -69,13 +74,13 @@ non_snake_case_features = "warn"
6974
foo.cargo("check -Zcargo-lints")
7075
.masquerade_as_nightly_cargo(&["cargo-lints", "test-dummy-unstable"])
7176
.with_stderr_data(str![[r#"
72-
[WARNING] unknown lint: `non_snake_case_features`
73-
--> Cargo.toml:12:1
74-
|
75-
12 | non_snake_case_features = "warn"
76-
| ^^^^^^^^^^^^^^^^^^^^^^^
77-
|
78-
= [NOTE] `cargo::unknown_lints` is set to `warn` by default
77+
[WARNING] features should have a snake-case name
78+
--> Cargo.toml:9:1
79+
|
80+
9 | foo-bar = { version = "0.0.1", optional = true }
81+
| ^^^^^^^ --------------- implicit feature created for optional dependencies
82+
|
83+
= [NOTE] `cargo::non_snake_case_features` is set to `warn` in `[lints]`
7984
[UPDATING] `dummy-registry` index
8085
[LOCKING] 1 package to latest compatible version
8186
[CHECKING] foo v0.0.1 ([ROOT]/foo)

0 commit comments

Comments
 (0)