diff --git a/src/lints/unconditionally_sealed_trait_became_unsealed.ron b/src/lints/unconditionally_sealed_trait_became_unsealed.ron new file mode 100644 index 00000000..7f3fdd7d --- /dev/null +++ b/src/lints/unconditionally_sealed_trait_became_unsealed.ron @@ -0,0 +1,54 @@ +SemverQuery( + id: "unconditionally_sealed_trait_became_unsealed", + human_readable_name: "unconditionally sealed trait became unsealed", + description: "An unconditionally 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 + 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"]) + unconditionally_sealed @filter(op: "!=", value: ["$true"]) + 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: "An unconditionally 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 b5bab28d..378bb7dc 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1355,6 +1355,7 @@ add_lints!( type_mismatched_generic_lifetimes, type_requires_more_const_generic_params, type_requires_more_generic_type_params, + unconditionally_sealed_trait_became_unsealed, union_field_added_with_all_pub_fields, union_field_added_with_non_pub_fields, union_field_missing, diff --git a/test_crates/unconditionally_sealed_trait_became_unsealed/new/Cargo.toml b/test_crates/unconditionally_sealed_trait_became_unsealed/new/Cargo.toml new file mode 100644 index 00000000..f46baa74 --- /dev/null +++ b/test_crates/unconditionally_sealed_trait_became_unsealed/new/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "unconditionally_sealed_trait_became_unsealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/unconditionally_sealed_trait_became_unsealed/new/src/lib.rs b/test_crates/unconditionally_sealed_trait_became_unsealed/new/src/lib.rs new file mode 100644 index 00000000..8b92e78c --- /dev/null +++ b/test_crates/unconditionally_sealed_trait_became_unsealed/new/src/lib.rs @@ -0,0 +1,40 @@ +// Traits transitioning from Unconditionally Sealed → Unsealed (Lint should detect these) +pub mod unconditionally_sealed_to_unsealed { + pub mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait ExtendsTraitInHiddenModule: hidden::Sealed {} + + pub trait TransitivelyTraitSealed: ExtendsTraitInHiddenModule {} + + pub trait ReturnsTraitInHiddenModule { + fn method(&self) -> hidden::Token; + } + + pub trait AcceptsTraitInHiddenModule { + fn method(&self, token: hidden::Token); + } + + pub trait SealedWithWhereSelfBound + where + Self: hidden::Sealed, + { + } +} + +// Traits transitioning from Unconditionally Sealed -> Public API Sealed (Lint should not detect these) +pub mod unconditionally_sealed_to_public_api_sealed { + #[doc(hidden)] + pub mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait ExtendsTraitInHiddenModule: hidden::Sealed {} + + pub trait ReturnsTraitInHiddenModule { + fn method(&self) -> hidden::Token; + } +} diff --git a/test_crates/unconditionally_sealed_trait_became_unsealed/old/Cargo.toml b/test_crates/unconditionally_sealed_trait_became_unsealed/old/Cargo.toml new file mode 100644 index 00000000..f46baa74 --- /dev/null +++ b/test_crates/unconditionally_sealed_trait_became_unsealed/old/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "unconditionally_sealed_trait_became_unsealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/unconditionally_sealed_trait_became_unsealed/old/src/lib.rs b/test_crates/unconditionally_sealed_trait_became_unsealed/old/src/lib.rs new file mode 100644 index 00000000..f0584834 --- /dev/null +++ b/test_crates/unconditionally_sealed_trait_became_unsealed/old/src/lib.rs @@ -0,0 +1,39 @@ +// Traits transitioning from Unconditionally Sealed → Unsealed (Lint should detect these) +pub mod unconditionally_sealed_to_unsealed { + mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait ExtendsTraitInHiddenModule: hidden::Sealed {} + + pub trait TransitivelyTraitSealed: ExtendsTraitInHiddenModule {} + + pub trait ReturnsTraitInHiddenModule { + fn method(&self) -> hidden::Token; + } + + pub trait AcceptsTraitInHiddenModule { + fn method(&self, token: hidden::Token); + } + + pub trait SealedWithWhereSelfBound + where + Self: hidden::Sealed, + { + } +} + +// Traits transitioning from Unconditionally Sealed -> Public API Sealed (Lint should not detect these) +pub mod unconditionally_sealed_to_public_api_sealed { + mod hidden { + pub trait Sealed {} + pub struct Token; + } + + pub trait ExtendsTraitInHiddenModule: hidden::Sealed {} + + pub trait ReturnsTraitInHiddenModule { + fn method(&self) -> hidden::Token; + } +} diff --git a/test_outputs/query_execution/unconditionally_sealed_trait_became_unsealed.snap b/test_outputs/query_execution/unconditionally_sealed_trait_became_unsealed.snap new file mode 100644 index 00000000..2844716f --- /dev/null +++ b/test_outputs/query_execution/unconditionally_sealed_trait_became_unsealed.snap @@ -0,0 +1,81 @@ +--- +source: src/query.rs +expression: "&query_execution_results" +--- +{ + "./test_crates/trait_method_added/": [ + { + "name": String("WillGainMethodWithoutDefaultAndLoseSeal"), + "path": List([ + String("trait_method_added"), + String("WillGainMethodWithoutDefaultAndLoseSeal"), + ]), + "span_begin_line": Uint64(43), + "span_end_line": Uint64(45), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], + "./test_crates/unconditionally_sealed_trait_became_unsealed/": [ + { + "name": String("ExtendsTraitInHiddenModule"), + "path": List([ + String("unconditionally_sealed_trait_became_unsealed"), + String("unconditionally_sealed_to_unsealed"), + String("ExtendsTraitInHiddenModule"), + ]), + "span_begin_line": Uint64(8), + "span_end_line": Uint64(8), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("TransitivelyTraitSealed"), + "path": List([ + String("unconditionally_sealed_trait_became_unsealed"), + String("unconditionally_sealed_to_unsealed"), + String("TransitivelyTraitSealed"), + ]), + "span_begin_line": Uint64(10), + "span_end_line": Uint64(10), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("ReturnsTraitInHiddenModule"), + "path": List([ + String("unconditionally_sealed_trait_became_unsealed"), + String("unconditionally_sealed_to_unsealed"), + String("ReturnsTraitInHiddenModule"), + ]), + "span_begin_line": Uint64(12), + "span_end_line": Uint64(14), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("AcceptsTraitInHiddenModule"), + "path": List([ + String("unconditionally_sealed_trait_became_unsealed"), + String("unconditionally_sealed_to_unsealed"), + String("AcceptsTraitInHiddenModule"), + ]), + "span_begin_line": Uint64(16), + "span_end_line": Uint64(18), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("SealedWithWhereSelfBound"), + "path": List([ + String("unconditionally_sealed_trait_became_unsealed"), + String("unconditionally_sealed_to_unsealed"), + String("SealedWithWhereSelfBound"), + ]), + "span_begin_line": Uint64(20), + "span_end_line": Uint64(24), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], +}