diff --git a/src/lints/pub_api_sealed_trait_became_unconditionally_sealed.ron b/src/lints/pub_api_sealed_trait_became_unconditionally_sealed.ron new file mode 100644 index 00000000..c975be3e --- /dev/null +++ b/src/lints/pub_api_sealed_trait_became_unconditionally_sealed.ron @@ -0,0 +1,54 @@ +SemverQuery( + id: "pub_api_sealed_trait_became_unconditionally_sealed", + human_readable_name: "public API sealed trait became unconditionally sealed", + description: "A public API sealed trait has become unconditionally sealed, blocking all downstream implementations including those from first-party crates that rely on the non-public API.", + 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"]) + unconditionally_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 unconditionally sealed, blocking all downstream implementations including those from first-party crates that rely on the non-public API.", + 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 378bb7dc..795d3679 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1287,6 +1287,7 @@ add_lints!( partial_ord_struct_fields_reordered, proc_macro_marked_deprecated, proc_macro_now_doc_hidden, + pub_api_sealed_trait_became_unconditionally_sealed, pub_module_level_const_missing, pub_module_level_const_now_doc_hidden, pub_static_missing, diff --git a/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/Cargo.toml b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/Cargo.toml new file mode 100644 index 00000000..f1dcd7f3 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "pub_api_sealed_trait_became_unconditionally_sealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/src/lib.rs b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/src/lib.rs new file mode 100644 index 00000000..818e3004 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/new/src/lib.rs @@ -0,0 +1,23 @@ +// Traits transitioning from Public API Sealed → Unconditionally Sealed (Lint should detect 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); + } + + pub trait HiddenSealedWithWhereSelfBound + where + Self: hidden::Sealed, + { + } +} diff --git a/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/Cargo.toml b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/Cargo.toml new file mode 100644 index 00000000..f1dcd7f3 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "pub_api_sealed_trait_became_unconditionally_sealed" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/src/lib.rs b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/src/lib.rs new file mode 100644 index 00000000..6f4e07b9 --- /dev/null +++ b/test_crates/pub_api_sealed_trait_became_unconditionally_sealed/old/src/lib.rs @@ -0,0 +1,24 @@ +// Traits transitioning from Public API Sealed → Unconditionally Sealed (Lint should detect 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); + } + + pub trait HiddenSealedWithWhereSelfBound + where + Self: hidden::Sealed, + { + } +} diff --git a/test_outputs/query_execution/pub_api_sealed_trait_became_unconditionally_sealed.snap b/test_outputs/query_execution/pub_api_sealed_trait_became_unconditionally_sealed.snap new file mode 100644 index 00000000..c6878108 --- /dev/null +++ b/test_outputs/query_execution/pub_api_sealed_trait_became_unconditionally_sealed.snap @@ -0,0 +1,107 @@ +--- +source: src/query.rs +expression: "&query_execution_results" +--- +{ + "./test_crates/pub_api_sealed_trait_became_unconditionally_sealed/": [ + { + "name": String("TraitExtendsUnconditionallyHiddenTrait"), + "path": List([ + String("pub_api_sealed_trait_became_unconditionally_sealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("TraitExtendsUnconditionallyHiddenTrait"), + ]), + "span_begin_line": Uint64(8), + "span_end_line": Uint64(8), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodReturningUnconditionallyHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unconditionally_sealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("MethodReturningUnconditionallyHiddenToken"), + ]), + "span_begin_line": Uint64(10), + "span_end_line": Uint64(12), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodTakingUnconditionallyHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unconditionally_sealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("MethodTakingUnconditionallyHiddenToken"), + ]), + "span_begin_line": Uint64(14), + "span_end_line": Uint64(16), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("HiddenSealedWithWhereSelfBound"), + "path": List([ + String("pub_api_sealed_trait_became_unconditionally_sealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("HiddenSealedWithWhereSelfBound"), + ]), + "span_begin_line": Uint64(18), + "span_end_line": Uint64(22), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], + "./test_crates/pub_api_sealed_trait_became_unsealed/": [ + { + "name": String("TraitExtendsUnconditionallyHiddenTrait"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("TraitExtendsUnconditionallyHiddenTrait"), + ]), + "span_begin_line": Uint64(30), + "span_end_line": Uint64(30), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodReturningUnconditionallyHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("MethodReturningUnconditionallyHiddenToken"), + ]), + "span_begin_line": Uint64(32), + "span_end_line": Uint64(34), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "name": String("MethodTakingUnconditionallyHiddenToken"), + "path": List([ + String("pub_api_sealed_trait_became_unsealed"), + String("public_api_sealed_to_unconditionally_sealed"), + String("MethodTakingUnconditionallyHiddenToken"), + ]), + "span_begin_line": Uint64(36), + "span_end_line": Uint64(38), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], + "./test_crates/trait_newly_sealed/": [ + { + "name": String("PublicAPISealed"), + "path": List([ + String("trait_newly_sealed"), + String("PublicAPISealed"), + ]), + "span_begin_line": Uint64(29), + "span_end_line": Uint64(32), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], +}