Skip to content

Commit bf9416a

Browse files
new lint: unconditionally_sealed_trait_became_unsealed (#1164)
Resolves #1121 --------- Co-authored-by: Predrag Gruevski <2348618+obi1kenobi@users.noreply.github.com>
1 parent af24e84 commit bf9416a

File tree

7 files changed

+229
-0
lines changed

7 files changed

+229
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
SemverQuery(
2+
id: "unconditionally_sealed_trait_became_unsealed",
3+
human_readable_name: "unconditionally sealed trait became unsealed",
4+
description: "An unconditionally sealed trait has become unsealed, allowing downstream crates to implement it. Reverting this would be a major breaking change.",
5+
required_update: Minor,
6+
lint_level: Warn,
7+
reference_link: Some("https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed"),
8+
query: r#"
9+
{
10+
CrateDiff {
11+
baseline {
12+
item {
13+
... on Trait {
14+
visibility_limit @filter(op: "=", value: ["$public"]) @output
15+
unconditionally_sealed @filter(op: "=", value: ["$true"])
16+
17+
importable_path {
18+
path @output @tag
19+
public_api @filter(op: "=", value: ["$true"])
20+
}
21+
}
22+
}
23+
}
24+
current {
25+
item {
26+
... on Trait {
27+
visibility_limit @filter(op: "=", value: ["$public"])
28+
unconditionally_sealed @filter(op: "!=", value: ["$true"])
29+
public_api_sealed @filter(op: "!=", value: ["$true"])
30+
name @output
31+
32+
importable_path {
33+
path @filter(op: "=", value: ["%path"])
34+
public_api @filter(op: "=", value: ["$true"])
35+
}
36+
37+
span_: span @optional {
38+
filename @output
39+
begin_line @output
40+
end_line @output
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}"#,
47+
arguments: {
48+
"public": "public",
49+
"true": true,
50+
},
51+
error_message: "An unconditionally sealed trait has become unsealed, allowing downstream crates to implement it. Reverting this would be a major breaking change.",
52+
per_result_error_template: Some("trait {{join \"::\" path}} in file {{span_filename}}:{{span_begin_line}}"),
53+
witness: None,
54+
)

src/query.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,7 @@ add_lints!(
13551355
type_mismatched_generic_lifetimes,
13561356
type_requires_more_const_generic_params,
13571357
type_requires_more_generic_type_params,
1358+
unconditionally_sealed_trait_became_unsealed,
13581359
union_field_added_with_all_pub_fields,
13591360
union_field_added_with_non_pub_fields,
13601361
union_field_missing,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
publish = false
3+
name = "unconditionally_sealed_trait_became_unsealed"
4+
version = "0.1.0"
5+
edition = "2021"
6+
7+
[dependencies]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Traits transitioning from Unconditionally Sealed → Unsealed (Lint should detect these)
2+
pub mod unconditionally_sealed_to_unsealed {
3+
pub mod hidden {
4+
pub trait Sealed {}
5+
pub struct Token;
6+
}
7+
8+
pub trait ExtendsTraitInHiddenModule: hidden::Sealed {}
9+
10+
pub trait TransitivelyTraitSealed: ExtendsTraitInHiddenModule {}
11+
12+
pub trait ReturnsTraitInHiddenModule {
13+
fn method(&self) -> hidden::Token;
14+
}
15+
16+
pub trait AcceptsTraitInHiddenModule {
17+
fn method(&self, token: hidden::Token);
18+
}
19+
20+
pub trait SealedWithWhereSelfBound
21+
where
22+
Self: hidden::Sealed,
23+
{
24+
}
25+
}
26+
27+
// Traits transitioning from Unconditionally Sealed -> Public API Sealed (Lint should not detect these)
28+
pub mod unconditionally_sealed_to_public_api_sealed {
29+
#[doc(hidden)]
30+
pub mod hidden {
31+
pub trait Sealed {}
32+
pub struct Token;
33+
}
34+
35+
pub trait ExtendsTraitInHiddenModule: hidden::Sealed {}
36+
37+
pub trait ReturnsTraitInHiddenModule {
38+
fn method(&self) -> hidden::Token;
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
publish = false
3+
name = "unconditionally_sealed_trait_became_unsealed"
4+
version = "0.1.0"
5+
edition = "2021"
6+
7+
[dependencies]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Traits transitioning from Unconditionally Sealed → Unsealed (Lint should detect these)
2+
pub mod unconditionally_sealed_to_unsealed {
3+
mod hidden {
4+
pub trait Sealed {}
5+
pub struct Token;
6+
}
7+
8+
pub trait ExtendsTraitInHiddenModule: hidden::Sealed {}
9+
10+
pub trait TransitivelyTraitSealed: ExtendsTraitInHiddenModule {}
11+
12+
pub trait ReturnsTraitInHiddenModule {
13+
fn method(&self) -> hidden::Token;
14+
}
15+
16+
pub trait AcceptsTraitInHiddenModule {
17+
fn method(&self, token: hidden::Token);
18+
}
19+
20+
pub trait SealedWithWhereSelfBound
21+
where
22+
Self: hidden::Sealed,
23+
{
24+
}
25+
}
26+
27+
// Traits transitioning from Unconditionally Sealed -> Public API Sealed (Lint should not detect these)
28+
pub mod unconditionally_sealed_to_public_api_sealed {
29+
mod hidden {
30+
pub trait Sealed {}
31+
pub struct Token;
32+
}
33+
34+
pub trait ExtendsTraitInHiddenModule: hidden::Sealed {}
35+
36+
pub trait ReturnsTraitInHiddenModule {
37+
fn method(&self) -> hidden::Token;
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
source: src/query.rs
3+
expression: "&query_execution_results"
4+
---
5+
{
6+
"./test_crates/trait_method_added/": [
7+
{
8+
"name": String("WillGainMethodWithoutDefaultAndLoseSeal"),
9+
"path": List([
10+
String("trait_method_added"),
11+
String("WillGainMethodWithoutDefaultAndLoseSeal"),
12+
]),
13+
"span_begin_line": Uint64(43),
14+
"span_end_line": Uint64(45),
15+
"span_filename": String("src/lib.rs"),
16+
"visibility_limit": String("public"),
17+
},
18+
],
19+
"./test_crates/unconditionally_sealed_trait_became_unsealed/": [
20+
{
21+
"name": String("ExtendsTraitInHiddenModule"),
22+
"path": List([
23+
String("unconditionally_sealed_trait_became_unsealed"),
24+
String("unconditionally_sealed_to_unsealed"),
25+
String("ExtendsTraitInHiddenModule"),
26+
]),
27+
"span_begin_line": Uint64(8),
28+
"span_end_line": Uint64(8),
29+
"span_filename": String("src/lib.rs"),
30+
"visibility_limit": String("public"),
31+
},
32+
{
33+
"name": String("TransitivelyTraitSealed"),
34+
"path": List([
35+
String("unconditionally_sealed_trait_became_unsealed"),
36+
String("unconditionally_sealed_to_unsealed"),
37+
String("TransitivelyTraitSealed"),
38+
]),
39+
"span_begin_line": Uint64(10),
40+
"span_end_line": Uint64(10),
41+
"span_filename": String("src/lib.rs"),
42+
"visibility_limit": String("public"),
43+
},
44+
{
45+
"name": String("ReturnsTraitInHiddenModule"),
46+
"path": List([
47+
String("unconditionally_sealed_trait_became_unsealed"),
48+
String("unconditionally_sealed_to_unsealed"),
49+
String("ReturnsTraitInHiddenModule"),
50+
]),
51+
"span_begin_line": Uint64(12),
52+
"span_end_line": Uint64(14),
53+
"span_filename": String("src/lib.rs"),
54+
"visibility_limit": String("public"),
55+
},
56+
{
57+
"name": String("AcceptsTraitInHiddenModule"),
58+
"path": List([
59+
String("unconditionally_sealed_trait_became_unsealed"),
60+
String("unconditionally_sealed_to_unsealed"),
61+
String("AcceptsTraitInHiddenModule"),
62+
]),
63+
"span_begin_line": Uint64(16),
64+
"span_end_line": Uint64(18),
65+
"span_filename": String("src/lib.rs"),
66+
"visibility_limit": String("public"),
67+
},
68+
{
69+
"name": String("SealedWithWhereSelfBound"),
70+
"path": List([
71+
String("unconditionally_sealed_trait_became_unsealed"),
72+
String("unconditionally_sealed_to_unsealed"),
73+
String("SealedWithWhereSelfBound"),
74+
]),
75+
"span_begin_line": Uint64(20),
76+
"span_end_line": Uint64(24),
77+
"span_filename": String("src/lib.rs"),
78+
"visibility_limit": String("public"),
79+
},
80+
],
81+
}

0 commit comments

Comments
 (0)