diff --git a/src/lints/pub_api_sealed_trait_became_unsealed.ron b/src/lints/pub_api_sealed_trait_became_unsealed.ron new file mode 100644 index 00000000..61242b3b --- /dev/null +++ b/src/lints/pub_api_sealed_trait_became_unsealed.ron @@ -0,0 +1,54 @@ +SemverQuery( + id: "pub_api_sealed_trait_became_unsealed", + human_readable_name: "public API sealed trait became unsealed", + description: "A public API sealed trait has become unsealed, allowing downstream crates to implement it. Reverting this would be a major breaking change.", + required_update: Minor, + lint_level: Warn, + reference_link: Some("https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed"), + query: r#" + { + CrateDiff { + baseline { + item { + ... on Trait { + visibility_limit @filter(op: "=", value: ["$public"]) @output + public_api_sealed @filter(op: "=", value: ["$true"]) + unconditionally_sealed @filter(op: "!=", value: ["$true"]) + + importable_path { + path @output @tag + public_api @filter(op: "=", value: ["$true"]) + } + } + } + } + current { + item { + ... on Trait { + visibility_limit @filter(op: "=", value: ["$public"]) + public_api_sealed @filter(op: "!=", value: ["$true"]) + name @output + + importable_path { + path @filter(op: "=", value: ["%path"]) + public_api @filter(op: "=", value: ["$true"]) + } + + span_: span @optional { + filename @output + begin_line @output + end_line @output + } + } + } + } + } + }"#, + arguments: { + "public": "public", + "true": true, + }, + error_message: "A public API sealed trait has become unsealed, allowing downstream crates to implement it. Reverting this would be a major breaking change.", + per_result_error_template: Some("trait {{join \"::\" path}} in file {{span_filename}}:{{span_begin_line}}"), + witness: None, +) diff --git a/src/query.rs b/src/query.rs index 1e8026ac..396bc79e 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1340,6 +1340,7 @@ add_lints!( trait_removed_supertrait, trait_requires_more_const_generic_params, trait_requires_more_generic_type_params, + pub_api_sealed_trait_became_unsealed, trait_unsafe_added, trait_unsafe_removed, tuple_struct_to_plain_struct, diff --git a/test_crates/pub_api_sealed_trait_became_unsealed/new/Cargo.toml b/test_crates/pub_api_sealed_trait_became_unsealed/new/Cargo.toml new file mode 100644 index 00000000..8a7a0dca --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unsealed/new/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "pub_api_sealed_trait_became_unsealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/pub_api_sealed_trait_became_unsealed/new/src/lib.rs b/test_crates/pub_api_sealed_trait_became_unsealed/new/src/lib.rs new file mode 100644 index 00000000..c3ac8d82 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unsealed/new/src/lib.rs @@ -0,0 +1,39 @@ +// Traits transitioning from Public API Sealed → Unsealed (Lint should detect these) +pub mod public_api_sealed_to_unsealed { + pub mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait PublicAPIToBeUnsealed { + type Hidden; + } + + pub trait TraitExtendsHiddenPublicAPITrait: hidden::Sealed {} + + pub trait MethodReturnPublicAPIHiddenToken { + fn method(&self) -> hidden::Token; + } + + pub trait MethodTakingPublicAPIHiddenToken { + fn method(&self, token: hidden::Token); + } +} + +// Traits transitioning from Public API Sealed → Unconditionally Sealed (Lint should ignore these) +pub mod public_api_sealed_to_unconditionally_sealed { + mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait TraitExtendsUnconditionallyHiddenTrait: hidden::Sealed {} + + pub trait MethodReturningUnconditionallyHiddenToken { + fn method(&self) -> hidden::Token; + } + + pub trait MethodTakingUnconditionallyHiddenToken { + fn method(&self, token: hidden::Token); + } +} diff --git a/test_crates/pub_api_sealed_trait_became_unsealed/old/Cargo.toml b/test_crates/pub_api_sealed_trait_became_unsealed/old/Cargo.toml new file mode 100644 index 00000000..8a7a0dca --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unsealed/old/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "pub_api_sealed_trait_became_unsealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/pub_api_sealed_trait_became_unsealed/old/src/lib.rs b/test_crates/pub_api_sealed_trait_became_unsealed/old/src/lib.rs new file mode 100644 index 00000000..c8506d29 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unsealed/old/src/lib.rs @@ -0,0 +1,42 @@ +// Traits transitioning from Public API Sealed → Unsealed (Lint should detect these) +pub mod public_api_sealed_to_unsealed { + #[doc(hidden)] + pub mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait PublicAPIToBeUnsealed { + #[doc(hidden)] + type Hidden; + } + + pub trait TraitExtendsHiddenPublicAPITrait: hidden::Sealed {} + + pub trait MethodReturnPublicAPIHiddenToken { + fn method(&self) -> hidden::Token; + } + + pub trait MethodTakingPublicAPIHiddenToken { + fn method(&self, token: hidden::Token); + } +} + +// Traits transitioning from Public API Sealed → Unconditionally Sealed (Lint should ignore these) +pub mod public_api_sealed_to_unconditionally_sealed { + #[doc(hidden)] + pub mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait TraitExtendsUnconditionallyHiddenTrait: hidden::Sealed {} + + pub trait MethodReturningUnconditionallyHiddenToken { + fn method(&self) -> hidden::Token; + } + + pub trait MethodTakingUnconditionallyHiddenToken { + fn method(&self, token: hidden::Token); + } +} diff --git a/test_outputs/query_execution/pub_api_sealed_trait_became_unsealed.snap b/test_outputs/query_execution/pub_api_sealed_trait_became_unsealed.snap new file mode 100644 index 00000000..fe591aed --- /dev/null +++ b/test_outputs/query_execution/pub_api_sealed_trait_became_unsealed.snap @@ -0,0 +1,82 @@ +--- +source: src/query.rs +expression: "&query_execution_results" +--- +{ + "./test_crates/pub_api_sealed_trait_became_unsealed/": [ + { + "name": String("PublicAPIToBeUnsealed"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unsealed"), + String("PublicAPIToBeUnsealed"), + ]), + "span_begin_line": Uint64(8), + "span_end_line": Uint64(10), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("TraitExtendsHiddenPublicAPITrait"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unsealed"), + String("TraitExtendsHiddenPublicAPITrait"), + ]), + "span_begin_line": Uint64(12), + "span_end_line": Uint64(12), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodReturnPublicAPIHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unsealed"), + String("MethodReturnPublicAPIHiddenToken"), + ]), + "span_begin_line": Uint64(14), + "span_end_line": Uint64(16), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodTakingPublicAPIHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unsealed"), + String("MethodTakingPublicAPIHiddenToken"), + ]), + "span_begin_line": Uint64(18), + "span_end_line": Uint64(20), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], + "./test_crates/trait_associated_type_marked_deprecated/": [ + { + "name": String("PublicTraitWithHiddenType"), + "path": List([ + String("trait_associated_type_marked_deprecated"), + String("PublicTraitWithHiddenType"), + ]), + "span_begin_line": Uint64(40), + "span_end_line": Uint64(44), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], + "./test_crates/trait_method_marked_deprecated/": [ + { + "name": String("PublicTraitWithHiddenMethod"), + "path": List([ + String("trait_method_marked_deprecated"), + String("PublicTraitWithHiddenMethod"), + ]), + "span_begin_line": Uint64(47), + "span_end_line": Uint64(51), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], +}