about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--clippy_config/src/lib.rs1
-rw-r--r--clippy_config/src/types.rs3
-rw-r--r--clippy_lints_internal/src/derive_deserialize_allowing_unknown.rs164
-rw-r--r--clippy_lints_internal/src/lib.rs3
-rw-r--r--tests/ui-internal/derive_deserialize_allowing_unknown.rs60
-rw-r--r--tests/ui-internal/derive_deserialize_allowing_unknown.stderr23
-rw-r--r--tests/ui-toml/toml_unknown_config_struct_field/clippy.toml4
-rw-r--r--tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.rs5
-rw-r--r--tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.stderr8
9 files changed, 270 insertions, 1 deletions
diff --git a/clippy_config/src/lib.rs b/clippy_config/src/lib.rs
index 33608591fc7..67904b4fcdc 100644
--- a/clippy_config/src/lib.rs
+++ b/clippy_config/src/lib.rs
@@ -12,6 +12,7 @@
     rustc::diagnostic_outside_of_impl,
     rustc::untranslatable_diagnostic
 )]
+#![deny(clippy::derive_deserialize_allowing_unknown)]
 
 extern crate rustc_data_structures;
 extern crate rustc_errors;
diff --git a/clippy_config/src/types.rs b/clippy_config/src/types.rs
index 2cb5493f1a9..f64eefa0c23 100644
--- a/clippy_config/src/types.rs
+++ b/clippy_config/src/types.rs
@@ -12,6 +12,7 @@ use std::collections::HashMap;
 use std::fmt;
 
 #[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
 pub struct Rename {
     pub path: String,
     pub rename: String,
@@ -59,7 +60,7 @@ impl<'de, const REPLACEMENT_ALLOWED: bool> Deserialize<'de> for DisallowedPath<R
 // `DisallowedPathEnum` is an implementation detail to enable the `Deserialize` implementation just
 // above. `DisallowedPathEnum` is not meant to be used outside of this file.
 #[derive(Debug, Deserialize, Serialize)]
-#[serde(untagged)]
+#[serde(untagged, deny_unknown_fields)]
 enum DisallowedPathEnum {
     Simple(String),
     WithReason {
diff --git a/clippy_lints_internal/src/derive_deserialize_allowing_unknown.rs b/clippy_lints_internal/src/derive_deserialize_allowing_unknown.rs
new file mode 100644
index 00000000000..a1dacd359a0
--- /dev/null
+++ b/clippy_lints_internal/src/derive_deserialize_allowing_unknown.rs
@@ -0,0 +1,164 @@
+use clippy_utils::diagnostics::span_lint;
+use clippy_utils::paths;
+use rustc_ast::tokenstream::{TokenStream, TokenTree};
+use rustc_ast::{AttrStyle, DelimArgs};
+use rustc_hir::def::Res;
+use rustc_hir::def_id::LocalDefId;
+use rustc_hir::{
+    AttrArgs, AttrItem, AttrPath, Attribute, HirId, Impl, Item, ItemKind, Path, QPath, TraitRef, Ty, TyKind,
+};
+use rustc_lint::{LateContext, LateLintPass};
+use rustc_lint_defs::declare_tool_lint;
+use rustc_middle::ty::TyCtxt;
+use rustc_session::declare_lint_pass;
+use rustc_span::sym;
+
+declare_tool_lint! {
+    /// ### What it does
+    /// Checks for structs or enums that derive `serde::Deserialize` and that
+    /// do not have a `#[serde(deny_unknown_fields)]` attribute.
+    ///
+    /// ### Why is this bad?
+    /// If the struct or enum is used in [`clippy_config::conf::Conf`] and a
+    /// user inserts an unknown field by mistake, the user's error will be
+    /// silently ignored.
+    ///
+    /// ### Example
+    /// ```rust
+    /// #[derive(serde::Deserialize)]
+    /// pub struct DisallowedPath {
+    ///     path: String,
+    ///     reason: Option<String>,
+    ///     replacement: Option<String>,
+    /// }
+    /// ```
+    ///
+    /// Use instead:
+    /// ```rust
+    /// #[derive(serde::Deserialize)]
+    /// #[serde(deny_unknown_fields)]
+    /// pub struct DisallowedPath {
+    ///     path: String,
+    ///     reason: Option<String>,
+    ///     replacement: Option<String>,
+    /// }
+    /// ```
+    pub clippy::DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
+    Allow,
+    "`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`",
+    report_in_external_macro: true
+}
+
+declare_lint_pass!(DeriveDeserializeAllowingUnknown => [DERIVE_DESERIALIZE_ALLOWING_UNKNOWN]);
+
+impl<'tcx> LateLintPass<'tcx> for DeriveDeserializeAllowingUnknown {
+    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
+        // Is this an `impl` (of a certain form)?
+        let ItemKind::Impl(Impl {
+            of_trait:
+                Some(TraitRef {
+                    path:
+                        Path {
+                            res: Res::Def(_, trait_def_id),
+                            ..
+                        },
+                    ..
+                }),
+            self_ty:
+                Ty {
+                    kind:
+                        TyKind::Path(QPath::Resolved(
+                            None,
+                            Path {
+                                res: Res::Def(_, self_ty_def_id),
+                                ..
+                            },
+                        )),
+                    ..
+                },
+            ..
+        }) = item.kind
+        else {
+            return;
+        };
+
+        // Is it an `impl` of the trait `serde::Deserialize`?
+        if !paths::SERDE_DESERIALIZE.get(cx).contains(trait_def_id) {
+            return;
+        }
+
+        // Is it derived?
+        if !cx.tcx.has_attr(item.owner_id, sym::automatically_derived) {
+            return;
+        }
+
+        // Is `self_ty` local?
+        let Some(local_def_id) = self_ty_def_id.as_local() else {
+            return;
+        };
+
+        // Does `self_ty` have a variant with named fields?
+        if !has_variant_with_named_fields(cx.tcx, local_def_id) {
+            return;
+        }
+
+        let hir_id = cx.tcx.local_def_id_to_hir_id(local_def_id);
+
+        // Does `self_ty` have `#[serde(deny_unknown_fields)]`?
+        if let Some(tokens) = find_serde_attr_item(cx.tcx, hir_id)
+            && tokens.iter().any(is_deny_unknown_fields_token)
+        {
+            return;
+        }
+
+        span_lint(
+            cx,
+            DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
+            item.span,
+            "`#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`",
+        );
+    }
+}
+
+// Determines whether `def_id` corresponds to an ADT with at least one variant with named fields. A
+// variant has named fields if its `ctor` field is `None`.
+fn has_variant_with_named_fields(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
+    let ty = tcx.type_of(def_id).skip_binder();
+
+    let rustc_middle::ty::Adt(adt_def, _) = ty.kind() else {
+        return false;
+    };
+
+    adt_def.variants().iter().any(|variant_def| variant_def.ctor.is_none())
+}
+
+fn find_serde_attr_item(tcx: TyCtxt<'_>, hir_id: HirId) -> Option<&TokenStream> {
+    tcx.hir_attrs(hir_id).iter().find_map(|attribute| {
+        if let Attribute::Unparsed(attr_item) = attribute
+            && let AttrItem {
+                path: AttrPath { segments, .. },
+                args: AttrArgs::Delimited(DelimArgs { tokens, .. }),
+                style: AttrStyle::Outer,
+                ..
+            } = &**attr_item
+            && segments.len() == 1
+            && segments[0].as_str() == "serde"
+        {
+            Some(tokens)
+        } else {
+            None
+        }
+    })
+}
+
+fn is_deny_unknown_fields_token(tt: &TokenTree) -> bool {
+    if let TokenTree::Token(token, _) = tt
+        && token
+            .ident()
+            .is_some_and(|(token, _)| token.as_str() == "deny_unknown_fields")
+    {
+        true
+    } else {
+        false
+    }
+}
diff --git a/clippy_lints_internal/src/lib.rs b/clippy_lints_internal/src/lib.rs
index 308d161b9d6..43cde86504f 100644
--- a/clippy_lints_internal/src/lib.rs
+++ b/clippy_lints_internal/src/lib.rs
@@ -32,6 +32,7 @@ extern crate rustc_span;
 
 mod almost_standard_lint_formulation;
 mod collapsible_calls;
+mod derive_deserialize_allowing_unknown;
 mod internal_paths;
 mod lint_without_lint_pass;
 mod msrv_attr_impl;
@@ -46,6 +47,7 @@ use rustc_lint::{Lint, LintStore};
 static LINTS: &[&Lint] = &[
     almost_standard_lint_formulation::ALMOST_STANDARD_LINT_FORMULATION,
     collapsible_calls::COLLAPSIBLE_SPAN_LINT_CALLS,
+    derive_deserialize_allowing_unknown::DERIVE_DESERIALIZE_ALLOWING_UNKNOWN,
     lint_without_lint_pass::DEFAULT_LINT,
     lint_without_lint_pass::INVALID_CLIPPY_VERSION_ATTRIBUTE,
     lint_without_lint_pass::LINT_WITHOUT_LINT_PASS,
@@ -65,6 +67,7 @@ pub fn register_lints(store: &mut LintStore) {
     store.register_early_pass(|| Box::new(unsorted_clippy_utils_paths::UnsortedClippyUtilsPaths));
     store.register_early_pass(|| Box::new(produce_ice::ProduceIce));
     store.register_late_pass(|_| Box::new(collapsible_calls::CollapsibleCalls));
+    store.register_late_pass(|_| Box::new(derive_deserialize_allowing_unknown::DeriveDeserializeAllowingUnknown));
     store.register_late_pass(|_| Box::<symbols::Symbols>::default());
     store.register_late_pass(|_| Box::<lint_without_lint_pass::LintWithoutLintPass>::default());
     store.register_late_pass(|_| Box::new(unnecessary_def_path::UnnecessaryDefPath));
diff --git a/tests/ui-internal/derive_deserialize_allowing_unknown.rs b/tests/ui-internal/derive_deserialize_allowing_unknown.rs
new file mode 100644
index 00000000000..9dc8e9e8f4c
--- /dev/null
+++ b/tests/ui-internal/derive_deserialize_allowing_unknown.rs
@@ -0,0 +1,60 @@
+#![deny(clippy::derive_deserialize_allowing_unknown)]
+
+use serde::{Deserialize, Deserializer};
+
+#[derive(Deserialize)] //~ derive_deserialize_allowing_unknown
+struct Struct {
+    flag: bool,
+    limit: u64,
+}
+
+#[derive(Deserialize)] //~ derive_deserialize_allowing_unknown
+enum Enum {
+    A(bool),
+    B { limit: u64 },
+}
+
+// negative tests
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct StructWithDenyUnknownFields {
+    flag: bool,
+    limit: u64,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+enum EnumWithDenyUnknownFields {
+    A(bool),
+    B { limit: u64 },
+}
+
+#[derive(Deserialize)]
+#[serde(untagged, deny_unknown_fields)]
+enum MultipleSerdeAttributes {
+    A(bool),
+    B { limit: u64 },
+}
+
+#[derive(Deserialize)]
+struct TupleStruct(u64, bool);
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+enum EnumWithOnlyTupleVariants {
+    A(bool),
+    B(u64),
+}
+
+struct ManualSerdeImplementation;
+
+impl<'de> Deserialize<'de> for ManualSerdeImplementation {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let () = <() as Deserialize>::deserialize(deserializer)?;
+        Ok(ManualSerdeImplementation)
+    }
+}
diff --git a/tests/ui-internal/derive_deserialize_allowing_unknown.stderr b/tests/ui-internal/derive_deserialize_allowing_unknown.stderr
new file mode 100644
index 00000000000..93d64826c99
--- /dev/null
+++ b/tests/ui-internal/derive_deserialize_allowing_unknown.stderr
@@ -0,0 +1,23 @@
+error: `#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`
+  --> tests/ui-internal/derive_deserialize_allowing_unknown.rs:5:10
+   |
+LL | #[derive(Deserialize)]
+   |          ^^^^^^^^^^^
+   |
+note: the lint level is defined here
+  --> tests/ui-internal/derive_deserialize_allowing_unknown.rs:1:9
+   |
+LL | #![deny(clippy::derive_deserialize_allowing_unknown)]
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   = note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: `#[derive(serde::Deserialize)]` without `#[serde(deny_unknown_fields)]`
+  --> tests/ui-internal/derive_deserialize_allowing_unknown.rs:11:10
+   |
+LL | #[derive(Deserialize)]
+   |          ^^^^^^^^^^^
+   |
+   = note: this error originates in the derive macro `Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui-toml/toml_unknown_config_struct_field/clippy.toml b/tests/ui-toml/toml_unknown_config_struct_field/clippy.toml
new file mode 100644
index 00000000000..82560cfd5e2
--- /dev/null
+++ b/tests/ui-toml/toml_unknown_config_struct_field/clippy.toml
@@ -0,0 +1,4 @@
+# In the following configuration, "recommendation" should be "reason" or "replacement".
+disallowed-macros = [
+    { path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" },
+]
diff --git a/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.rs b/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.rs
new file mode 100644
index 00000000000..9c770c31f6f
--- /dev/null
+++ b/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.rs
@@ -0,0 +1,5 @@
+#[rustfmt::skip]
+//@error-in-other-file: error reading Clippy's configuration file: data did not match any variant of untagged enum DisallowedPathEnum
+fn main() {
+    panic!();
+}
diff --git a/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.stderr b/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.stderr
new file mode 100644
index 00000000000..b564709721d
--- /dev/null
+++ b/tests/ui-toml/toml_unknown_config_struct_field/toml_unknown_config_struct_field.stderr
@@ -0,0 +1,8 @@
+error: error reading Clippy's configuration file: data did not match any variant of untagged enum DisallowedPathEnum
+  --> $DIR/tests/ui-toml/toml_unknown_config_struct_field/clippy.toml:3:5
+   |
+LL |     { path = "std::panic", recommendation = "return a `std::result::Result::Error` instead" },
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: aborting due to 1 previous error
+