Skip to content

Commit 17f2a87

Browse files
authored
Add internal lint derive_deserialize_allowing_unknown (#14360)
Adds an internal lint to check for `#[derive(serde::Deserialize)]` without [`#[serde(deny_unknown_fields)]`](https://serde.rs/container-attrs.html#deny_unknown_fields). Today, if you run Clippy with the following clippy.toml, Clippy will produce a warning, but there will be no accompanying note: ```toml # In the following configuration, "recommendation" should be "reason" or "replacement". disallowed-macros = [ { path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" }, ] ``` ```sh $ cargo clippy Checking a v0.1.0 (/home/smoelius/tmp/a) warning: use of a disallowed macro `std::panic` --> src/lib.rs:2:5 | 2 | panic!(); | ^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros = note: `#[warn(clippy::disallowed_macros)]` on by default ``` The underlying problem is: the enum that derives `serde::Deserialize` ([`DisallowedPathEnum`](https://github.com/rust-lang/rust-clippy/blob/81643e297cf44ce3c7648b8443fc4d6592fa81eb/clippy_config/src/types.rs#L47)) does not have the attribute `#[serde(deny_unknown_fields)]`. This lint identifies such problems by checking trait `impl`s. An alternative I considered was to walk `clippy_config::conf::Conf` directly. However, that would not catch the `DisallowedPathEnum` case because it [is not used in `Conf` directly](https://github.com/rust-lang/rust-clippy/blob/81643e297cf44ce3c7648b8443fc4d6592fa81eb/clippy_config/src/types.rs#L31). Just to be clear, no one asked for this. So I hope the maintainers do not mind. changelog: none
2 parents 7bac114 + 9f4ecea commit 17f2a87

File tree

9 files changed

+270
-1
lines changed

9 files changed

+270
-1
lines changed

clippy_config/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
rustc::diagnostic_outside_of_impl,
1313
rustc::untranslatable_diagnostic
1414
)]
15+
#![deny(clippy::derive_deserialize_allowing_unknown)]
1516

1617
extern crate rustc_data_structures;
1718
extern crate rustc_errors;

clippy_config/src/types.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::collections::HashMap;
1212
use std::fmt;
1313

1414
#[derive(Debug, Deserialize)]
15+
#[serde(deny_unknown_fields)]
1516
pub struct Rename {
1617
pub path: String,
1718
pub rename: String,
@@ -59,7 +60,7 @@ impl<'de, const REPLACEMENT_ALLOWED: bool> Deserialize<'de> for DisallowedPath<R
5960
// `DisallowedPathEnum` is an implementation detail to enable the `Deserialize` implementation just
6061
// above. `DisallowedPathEnum` is not meant to be used outside of this file.
6162
#[derive(Debug, Deserialize, Serialize)]
62-
#[serde(untagged)]
63+
#[serde(untagged, deny_unknown_fields)]
6364
enum DisallowedPathEnum {
6465
Simple(String),
6566
WithReason {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::paths;
3+
use rustc_ast::tokenstream::{TokenStream, TokenTree};
4+
use rustc_ast::{AttrStyle, DelimArgs};
5+
use rustc_hir::def::Res;
6+
use rustc_hir::def_id::LocalDefId;
7+
use rustc_hir::{
8+
AttrArgs, AttrItem, AttrPath, Attribute, HirId, Impl, Item, ItemKind, Path, QPath, TraitRef, Ty, TyKind,
9+
};
10+
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_lint_defs::declare_tool_lint;
12+
use rustc_middle::ty::TyCtxt;
13+
use rustc_session::declare_lint_pass;
14+
use rustc_span::sym;
15+
16+
declare_tool_lint! {
17+
/// ### What it does
18+
/// Checks for structs or enums that derive `serde::Deserialize` and that
19+
/// do not have a `#[serde(deny_unknown_fields)]` attribute.
20+
///
21+
/// ### Why is this bad?
22+
/// If the struct or enum is used in [`clippy_config::conf::Conf`] and a
23+
/// user inserts an unknown field by mistake, the user's error will be
24+
/// silently ignored.
25+
///
26+
/// ### Example
27+
/// ```rust
28+
/// #[derive(serde::Deserialize)]
29+
/// pub struct DisallowedPath {
30+
/// path: String,
31+
/// reason: Option<String>,
32+
/// replacement: Option<String>,
33+
/// }
34+
/// ```
35+
///
36+
/// Use instead:
37+
/// ```rust
38+
/// #[derive(serde::Deserialize)]
39+
/// #[serde(deny_unknown_fields)]
40+
/// pub struct DisallowedPath {
41+
/// path: String,
42+
/// reason: Option<String>,
43+
/// replacement: Option<String>,
44+
/// }
45+
/// ```
46+
pub clippy::DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
47+
Allow,
48+
"`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`",
49+
report_in_external_macro: true
50+
}
51+
52+
declare_lint_pass!(DeriveDeserializeAllowingUnknown => [DERIVE_DESERIALIZE_ALLOWING_UNKNOWN]);
53+
54+
impl<'tcx> LateLintPass<'tcx> for DeriveDeserializeAllowingUnknown {
55+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
56+
// Is this an `impl` (of a certain form)?
57+
let ItemKind::Impl(Impl {
58+
of_trait:
59+
Some(TraitRef {
60+
path:
61+
Path {
62+
res: Res::Def(_, trait_def_id),
63+
..
64+
},
65+
..
66+
}),
67+
self_ty:
68+
Ty {
69+
kind:
70+
TyKind::Path(QPath::Resolved(
71+
None,
72+
Path {
73+
res: Res::Def(_, self_ty_def_id),
74+
..
75+
},
76+
)),
77+
..
78+
},
79+
..
80+
}) = item.kind
81+
else {
82+
return;
83+
};
84+
85+
// Is it an `impl` of the trait `serde::Deserialize`?
86+
if !paths::SERDE_DESERIALIZE.get(cx).contains(trait_def_id) {
87+
return;
88+
}
89+
90+
// Is it derived?
91+
if !cx.tcx.has_attr(item.owner_id, sym::automatically_derived) {
92+
return;
93+
}
94+
95+
// Is `self_ty` local?
96+
let Some(local_def_id) = self_ty_def_id.as_local() else {
97+
return;
98+
};
99+
100+
// Does `self_ty` have a variant with named fields?
101+
if !has_variant_with_named_fields(cx.tcx, local_def_id) {
102+
return;
103+
}
104+
105+
let hir_id = cx.tcx.local_def_id_to_hir_id(local_def_id);
106+
107+
// Does `self_ty` have `#[serde(deny_unknown_fields)]`?
108+
if let Some(tokens) = find_serde_attr_item(cx.tcx, hir_id)
109+
&& tokens.iter().any(is_deny_unknown_fields_token)
110+
{
111+
return;
112+
}
113+
114+
span_lint(
115+
cx,
116+
DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
117+
item.span,
118+
"`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`",
119+
);
120+
}
121+
}
122+
123+
// Determines whether `def_id` corresponds to an ADT with at least one variant with named fields. A
124+
// variant has named fields if its `ctor` field is `None`.
125+
fn has_variant_with_named_fields(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
126+
let ty = tcx.type_of(def_id).skip_binder();
127+
128+
let rustc_middle::ty::Adt(adt_def, _) = ty.kind() else {
129+
return false;
130+
};
131+
132+
adt_def.variants().iter().any(|variant_def| variant_def.ctor.is_none())
133+
}
134+
135+
fn find_serde_attr_item(tcx: TyCtxt<'_>, hir_id: HirId) -> Option<&TokenStream> {
136+
tcx.hir_attrs(hir_id).iter().find_map(|attribute| {
137+
if let Attribute::Unparsed(attr_item) = attribute
138+
&& let AttrItem {
139+
path: AttrPath { segments, .. },
140+
args: AttrArgs::Delimited(DelimArgs { tokens, .. }),
141+
style: AttrStyle::Outer,
142+
..
143+
} = &**attr_item
144+
&& segments.len() == 1
145+
&& segments[0].as_str() == "serde"
146+
{
147+
Some(tokens)
148+
} else {
149+
None
150+
}
151+
})
152+
}
153+
154+
fn is_deny_unknown_fields_token(tt: &TokenTree) -> bool {
155+
if let TokenTree::Token(token, _) = tt
156+
&& token
157+
.ident()
158+
.is_some_and(|(token, _)| token.as_str() == "deny_unknown_fields")
159+
{
160+
true
161+
} else {
162+
false
163+
}
164+
}

clippy_lints_internal/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ extern crate rustc_span;
3232

3333
mod almost_standard_lint_formulation;
3434
mod collapsible_calls;
35+
mod derive_deserialize_allowing_unknown;
3536
mod internal_paths;
3637
mod lint_without_lint_pass;
3738
mod msrv_attr_impl;
@@ -46,6 +47,7 @@ use rustc_lint::{Lint, LintStore};
4647
static LINTS: &[&Lint] = &[
4748
almost_standard_lint_formulation::ALMOST_STANDARD_LINT_FORMULATION,
4849
collapsible_calls::COLLAPSIBLE_SPAN_LINT_CALLS,
50+
derive_deserialize_allowing_unknown::DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
4951
lint_without_lint_pass::DEFAULT_LINT,
5052
lint_without_lint_pass::INVALID_CLIPPY_VERSION_ATTRIBUTE,
5153
lint_without_lint_pass::LINT_WITHOUT_LINT_PASS,
@@ -65,6 +67,7 @@ pub fn register_lints(store: &mut LintStore) {
6567
store.register_early_pass(|| Box::new(unsorted_clippy_utils_paths::UnsortedClippyUtilsPaths));
6668
store.register_early_pass(|| Box::new(produce_ice::ProduceIce));
6769
store.register_late_pass(|_| Box::new(collapsible_calls::CollapsibleCalls));
70+
store.register_late_pass(|_| Box::new(derive_deserialize_allowing_unknown::DeriveDeserializeAllowingUnknown));
6871
store.register_late_pass(|_| Box::<symbols::Symbols>::default());
6972
store.register_late_pass(|_| Box::<lint_without_lint_pass::LintWithoutLintPass>::default());
7073
store.register_late_pass(|_| Box::new(unnecessary_def_path::UnnecessaryDefPath));
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#![deny(clippy::derive_deserialize_allowing_unknown)]
2+
3+
use serde::{Deserialize, Deserializer};
4+
5+
#[derive(Deserialize)] //~ derive_deserialize_allowing_unknown
6+
struct Struct {
7+
flag: bool,
8+
limit: u64,
9+
}
10+
11+
#[derive(Deserialize)] //~ derive_deserialize_allowing_unknown
12+
enum Enum {
13+
A(bool),
14+
B { limit: u64 },
15+
}
16+
17+
// negative tests
18+
19+
#[derive(Deserialize)]
20+
#[serde(deny_unknown_fields)]
21+
struct StructWithDenyUnknownFields {
22+
flag: bool,
23+
limit: u64,
24+
}
25+
26+
#[derive(Deserialize)]
27+
#[serde(deny_unknown_fields)]
28+
enum EnumWithDenyUnknownFields {
29+
A(bool),
30+
B { limit: u64 },
31+
}
32+
33+
#[derive(Deserialize)]
34+
#[serde(untagged, deny_unknown_fields)]
35+
enum MultipleSerdeAttributes {
36+
A(bool),
37+
B { limit: u64 },
38+
}
39+
40+
#[derive(Deserialize)]
41+
struct TupleStruct(u64, bool);
42+
43+
#[derive(Deserialize)]
44+
#[serde(deny_unknown_fields)]
45+
enum EnumWithOnlyTupleVariants {
46+
A(bool),
47+
B(u64),
48+
}
49+
50+
struct ManualSerdeImplementation;
51+
52+
impl<'de> Deserialize<'de> for ManualSerdeImplementation {
53+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54+
where
55+
D: Deserializer<'de>,
56+
{
57+
let () = <() as Deserialize>::deserialize(deserializer)?;
58+
Ok(ManualSerdeImplementation)
59+
}
60+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: `#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`
2+
--> tests/ui-internal/derive_deserialize_allowing_unknown.rs:5:10
3+
|
4+
LL | #[derive(Deserialize)]
5+
| ^^^^^^^^^^^
6+
|
7+
note: the lint level is defined here
8+
--> tests/ui-internal/derive_deserialize_allowing_unknown.rs:1:9
9+
|
10+
LL | #![deny(clippy::derive_deserialize_allowing_unknown)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
= note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
13+
14+
error: `#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`
15+
--> tests/ui-internal/derive_deserialize_allowing_unknown.rs:11:10
16+
|
17+
LL | #[derive(Deserialize)]
18+
| ^^^^^^^^^^^
19+
|
20+
= note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
21+
22+
error: aborting due to 2 previous errors
23+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# In the following configuration, "recommendation" should be "reason" or "replacement".
2+
disallowed-macros = [
3+
{ path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" },
4+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#[rustfmt::skip]
2+
//@error-in-other-file: error reading Clippy's configuration file: data did not match any variant of untagged enum DisallowedPathEnum
3+
fn main() {
4+
panic!();
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
error: error reading Clippy's configuration file: data did not match any variant of untagged enum DisallowedPathEnum
2+
--> $DIR/tests/ui-toml/toml_unknown_config_struct_field/clippy.toml:3:5
3+
|
4+
LL | { path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" },
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
7+
error: aborting due to 1 previous error
8+

0 commit comments

Comments
 (0)