diff --git a/src/lints/trait_associated_type_added.ron b/src/lints/trait_associated_type_added.ron new file mode 100644 index 00000000..54a41235 --- /dev/null +++ b/src/lints/trait_associated_type_added.ron @@ -0,0 +1,59 @@ +SemverQuery( + id: "trait_associated_type_added", + human_readable_name: "non-sealed public trait added associated type without default value", + description: "A non-sealed trait has gained an associated type without a default value, which breaks downstream implementations of the trait", + required_update: Major, + lint_level: Deny, + reference_link: Some("https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default"), + query: r#" + { + CrateDiff { + current { + item { + ... on Trait { + visibility_limit @filter(op: "=", value: ["$public"]) + + importable_path { + path @output @tag + public_api @filter(op: "=", value: ["$true"]) + } + + associated_type { + associated_type: name @output @tag + has_default @filter(op: "!=", value: ["$true"]) @output + + span_: span @optional { + filename @output + begin_line @output + } + } + } + } + } + baseline { + item { + ... on Trait { + visibility_limit @filter(op: "=", value: ["$public"]) @output + sealed @filter(op: "!=", value: ["$true"]) + + importable_path { + path @filter(op: "=", value: ["%path"]) + public_api @filter(op: "=", value: ["$true"]) + } + + associated_type @fold @transform(op: "count") @filter(op: "=", value: ["$zero"]) { + name @filter(op: "=", value: ["%associated_type"]) + } + } + } + } + } + }"#, + arguments: { + "public": "public", + "true": true, + "zero": 0, + }, + error_message: "A non-sealed trait has gained an associated type without a default value, which breaks downstream implementations of the trait", + per_result_error_template: Some("trait associated type {{join \"::\" path}}::{{associated_type}} in file {{span_filename}}:{{span_begin_line}}"), +) diff --git a/src/query.rs b/src/query.rs index 58c3b420..16bfdbe0 100644 --- a/src/query.rs +++ b/src/query.rs @@ -836,6 +836,7 @@ add_lints!( trait_associated_const_added, trait_associated_const_default_removed, trait_associated_const_now_doc_hidden, + trait_associated_type_added, trait_associated_type_now_doc_hidden, trait_default_impl_removed, trait_method_missing, diff --git a/test_crates/trait_associated_type_added/new/Cargo.toml b/test_crates/trait_associated_type_added/new/Cargo.toml new file mode 100644 index 00000000..08fc1a6f --- /dev/null +++ b/test_crates/trait_associated_type_added/new/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "trait_associated_type_added" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/trait_associated_type_added/new/src/lib.rs b/test_crates/trait_associated_type_added/new/src/lib.rs new file mode 100644 index 00000000..c0167839 --- /dev/null +++ b/test_crates/trait_associated_type_added/new/src/lib.rs @@ -0,0 +1,69 @@ +#![feature(associated_type_defaults)] + +mod sealed { + pub(crate) trait Sealed {} +} + +pub trait WillGainTypeWithoutDefault { + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithoutDefaultSealed: sealed::Sealed { + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithoutDefaultAndSeal: sealed::Sealed { + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithDefault { + type Bar = bool; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithDefaultSealed: sealed::Sealed { + type Bar = bool; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainAnotherTypeWithoutDefault { + type One; + type Two; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainAnotherTypeWithoutDefaultSealed: sealed::Sealed { + type One; + type Two; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainADocHiddenType { + #[doc(hidden)] + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait TypeWithoutDefaultUnchanged { + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait TypeDocHidden { + #[doc(hidden)] + type Bar; + + fn make_me_non_object_safe() -> Self; +} diff --git a/test_crates/trait_associated_type_added/old/Cargo.toml b/test_crates/trait_associated_type_added/old/Cargo.toml new file mode 100644 index 00000000..08fc1a6f --- /dev/null +++ b/test_crates/trait_associated_type_added/old/Cargo.toml @@ -0,0 +1,7 @@ +[package] +publish = false +name = "trait_associated_type_added" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/test_crates/trait_associated_type_added/old/src/lib.rs b/test_crates/trait_associated_type_added/old/src/lib.rs new file mode 100644 index 00000000..70ce4787 --- /dev/null +++ b/test_crates/trait_associated_type_added/old/src/lib.rs @@ -0,0 +1,53 @@ +#![feature(associated_type_defaults)] + +mod sealed { + pub(crate) trait Sealed {} +} + +pub trait WillGainTypeWithoutDefault { + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithoutDefaultSealed: sealed::Sealed { + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithoutDefaultAndSeal { + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainTypeWithDefault { + fn make_me_non_object_safe() -> Self; +} +pub trait WillGainTypeWithDefaultSealed: sealed::Sealed { + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainAnotherTypeWithoutDefault { + type One; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainAnotherTypeWithoutDefaultSealed: sealed::Sealed { + type One; + + fn make_me_non_object_safe() -> Self; +} + +pub trait WillGainADocHiddenType { + fn make_me_non_object_safe() -> Self; +} + +pub trait TypeWithoutDefaultUnchanged { + type Bar; + + fn make_me_non_object_safe() -> Self; +} + +pub trait TypeDocHidden { + #[doc(hidden)] + type Bar; + + fn make_me_non_object_safe() -> Self; +} diff --git a/test_outputs/trait_associated_type_added.output.ron b/test_outputs/trait_associated_type_added.output.ron new file mode 100644 index 00000000..76aeab57 --- /dev/null +++ b/test_outputs/trait_associated_type_added.output.ron @@ -0,0 +1,48 @@ +{ + "./test_crates/trait_associated_type_added/": [ + { + "associated_type": String("Bar"), + "has_default": Boolean(false), + "path": List([ + String("trait_associated_type_added"), + String("WillGainTypeWithoutDefault"), + ]), + "span_begin_line": Uint64(8), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "associated_type": String("Bar"), + "has_default": Boolean(false), + "path": List([ + String("trait_associated_type_added"), + String("WillGainTypeWithoutDefaultAndSeal"), + ]), + "span_begin_line": Uint64(20), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "associated_type": String("Two"), + "has_default": Boolean(false), + "path": List([ + String("trait_associated_type_added"), + String("WillGainAnotherTypeWithoutDefault"), + ]), + "span_begin_line": Uint64(39), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + { + "associated_type": String("Bar"), + "has_default": Boolean(false), + "path": List([ + String("trait_associated_type_added"), + String("WillGainADocHiddenType"), + ]), + "span_begin_line": Uint64(53), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], +} diff --git a/test_outputs/trait_newly_sealed.output.ron b/test_outputs/trait_newly_sealed.output.ron index 44d7fdd6..1363a9d1 100644 --- a/test_outputs/trait_newly_sealed.output.ron +++ b/test_outputs/trait_newly_sealed.output.ron @@ -33,6 +33,18 @@ "visibility_limit": String("public"), }, ], + "./test_crates/trait_associated_type_added/": [ + { + "name": String("WillGainTypeWithoutDefaultAndSeal"), + "path": List([ + String("trait_associated_type_added"), + String("WillGainTypeWithoutDefaultAndSeal"), + ]), + "span_begin_line": Uint64(19), + "span_filename": String("src/lib.rs"), + "visibility_limit": String("public"), + }, + ], "./test_crates/trait_default_impl_removed/": [ { "name": String("TraitE"),