Skip to content

Commit 490f802

Browse files
committed
Add internal lint derive_deserialize_allowing_unknown
`cargo dev dogfood` fails at this commit. The next commit addresses the errors that are produced.
1 parent 2440f51 commit 490f802

11 files changed

+278
-0
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_errors;
1718
extern crate rustc_hir;

clippy_lints/src/declared_lints.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub static LINTS: &[&crate::LintInfo] = &[
88
#[cfg(feature = "internal")]
99
crate::utils::internal_lints::collapsible_calls::COLLAPSIBLE_SPAN_LINT_CALLS_INFO,
1010
#[cfg(feature = "internal")]
11+
crate::utils::internal_lints::derive_deserialize_allowing_unknown::DERIVE_DESERIALIZE_ALLOWING_UNKNOWN_INFO,
12+
#[cfg(feature = "internal")]
1113
crate::utils::internal_lints::interning_defined_symbol::INTERNING_DEFINED_SYMBOL_INFO,
1214
#[cfg(feature = "internal")]
1315
crate::utils::internal_lints::interning_defined_symbol::UNNECESSARY_SYMBOL_STR_INFO,

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,9 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
609609
});
610610
store.register_early_pass(|| Box::new(utils::internal_lints::produce_ice::ProduceIce));
611611
store.register_late_pass(|_| Box::new(utils::internal_lints::collapsible_calls::CollapsibleCalls));
612+
store.register_late_pass(|_| {
613+
Box::new(utils::internal_lints::derive_deserialize_allowing_unknown::DeriveDeserializeAllowingUnknown)
614+
});
612615
store.register_late_pass(|_| Box::new(utils::internal_lints::invalid_paths::InvalidPaths));
613616
store.register_late_pass(|_| {
614617
Box::<utils::internal_lints::interning_defined_symbol::InterningDefinedSymbol>::default()

clippy_lints/src/utils/internal_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod almost_standard_lint_formulation;
22
pub mod collapsible_calls;
3+
pub mod derive_deserialize_allowing_unknown;
34
pub mod interning_defined_symbol;
45
pub mod invalid_paths;
56
pub mod lint_without_lint_pass;
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::{def_path_res, 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_middle::ty::TyCtxt;
12+
use rustc_session::declare_lint_pass;
13+
use rustc_span::sym;
14+
use std::sync::OnceLock;
15+
16+
declare_clippy_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(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(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 DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
47+
internal,
48+
"`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`"
49+
}
50+
51+
declare_lint_pass!(DeriveDeserializeAllowingUnknown => [DERIVE_DESERIALIZE_ALLOWING_UNKNOWN]);
52+
53+
impl<'tcx> LateLintPass<'tcx> for DeriveDeserializeAllowingUnknown {
54+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
55+
// Is this an `impl` (of a certain form)?
56+
let ItemKind::Impl(Impl {
57+
of_trait: Some(TraitRef {
58+
path: Path { res, .. }, ..
59+
}),
60+
self_ty:
61+
Ty {
62+
kind:
63+
TyKind::Path(QPath::Resolved(
64+
None,
65+
Path {
66+
res: Res::Def(_, self_ty_def_id),
67+
..
68+
},
69+
)),
70+
..
71+
},
72+
..
73+
}) = item.kind
74+
else {
75+
return;
76+
};
77+
78+
// Is it an `impl` of the trait `serde::Deserialize`?
79+
if !is_serde_deserialize_res(cx.tcx, res) {
80+
return;
81+
}
82+
83+
// Is it derived?
84+
if !cx.tcx.has_attr(item.owner_id, sym::automatically_derived) {
85+
return;
86+
}
87+
88+
// Is `self_ty` local?
89+
let Some(local_def_id) = self_ty_def_id.as_local() else {
90+
return;
91+
};
92+
93+
// Does `self_ty` have a variant with named fields?
94+
if !has_variant_with_named_fields(cx.tcx, local_def_id) {
95+
return;
96+
}
97+
98+
let hir_id = cx.tcx.local_def_id_to_hir_id(local_def_id);
99+
100+
// Does `self_ty` have `#[serde(deny_unknown_fields)]`?
101+
if let Some(tokens) = find_serde_attr_item(cx.tcx, hir_id)
102+
&& tokens.iter().any(is_deny_unknown_fields_token)
103+
{
104+
return;
105+
}
106+
107+
span_lint(
108+
cx,
109+
DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
110+
item.span,
111+
"`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`",
112+
);
113+
}
114+
}
115+
116+
fn is_serde_deserialize_res(tcx: TyCtxt<'_>, res: &Res) -> bool {
117+
static SERDE_DESERIALIZE_RESES: OnceLock<Vec<Res>> = OnceLock::new();
118+
119+
let serde_deserialize_reses = SERDE_DESERIALIZE_RESES.get_or_init(|| def_path_res(tcx, &paths::SERDE_DESERIALIZE));
120+
121+
serde_deserialize_reses.contains(res)
122+
}
123+
124+
// Determines whether `def_id` corresponds to an ADT with at least one variant with named fields. A
125+
// variant has named fields if its `ctor` field is `None`.
126+
fn has_variant_with_named_fields(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
127+
let ty = tcx.type_of(def_id).skip_binder();
128+
129+
let rustc_middle::ty::Adt(adt_def, _) = ty.kind() else {
130+
return false;
131+
};
132+
133+
adt_def.variants().iter().any(|variant_def| variant_def.ctor.is_none())
134+
}
135+
136+
fn find_serde_attr_item(tcx: TyCtxt<'_>, hir_id: HirId) -> Option<&TokenStream> {
137+
tcx.hir().attrs(hir_id).iter().find_map(|attribute| {
138+
if let Attribute::Unparsed(attr_item) = attribute
139+
&& let AttrItem {
140+
path: AttrPath { segments, .. },
141+
args: AttrArgs::Delimited(DelimArgs { tokens, .. }),
142+
style: AttrStyle::Outer,
143+
..
144+
} = &**attr_item
145+
&& segments.len() == 1
146+
&& segments[0].as_str() == "serde"
147+
{
148+
Some(tokens)
149+
} else {
150+
None
151+
}
152+
})
153+
}
154+
155+
fn is_deny_unknown_fields_token(tt: &TokenTree) -> bool {
156+
if let TokenTree::Token(token, _) = tt
157+
&& token
158+
.ident()
159+
.is_some_and(|(token, _)| token.as_str() == "deny_unknown_fields")
160+
{
161+
true
162+
} else {
163+
false
164+
}
165+
}

tests/dogfood.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ fn run_clippy_for_package(project: &str, args: &[&str]) -> bool {
8484
if cfg!(feature = "internal") {
8585
// internal lints only exist if we build with the internal feature
8686
command.args(["-D", "clippy::internal"]);
87+
// `derive_deserialize_allowing_unknown` is crate-wide denied in `clippy_config`.
88+
command.args(["-A", "clippy::derive_deserialize_allowing_unknown"]);
8789
} else {
8890
// running a clippy built without internal lints on the clippy source
8991
// that contains e.g. `allow(clippy::invalid_paths)`
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#![deny(clippy::internal)]
2+
3+
use serde::{Deserialize, Deserializer};
4+
5+
#[derive(Deserialize)]
6+
struct Struct {
7+
flag: bool,
8+
limit: u64,
9+
}
10+
11+
#[derive(Deserialize)]
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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::internal)]
11+
| ^^^^^^^^^^^^^^^^
12+
= note: `#[deny(clippy::derive_deserialize_allowing_unknown)]` implied by `#[deny(clippy::internal)]`
13+
= note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
14+
15+
error: `#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`
16+
--> tests/ui-internal/derive_deserialize_allowing_unknown.rs:11:10
17+
|
18+
LL | #[derive(Deserialize)]
19+
| ^^^^^^^^^^^
20+
|
21+
= note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
22+
23+
error: aborting due to 2 previous errors
24+
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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:2:21
3+
|
4+
LL | disallowed-macros = [
5+
| _____________________^
6+
LL | | { path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" },
7+
LL | | ]
8+
| |_^
9+
10+
error: aborting due to 1 previous error
11+

0 commit comments

Comments
 (0)