Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for adding #[non_exhaustive] causing breakage. #4

Merged
merged 6 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/regenerate_test_rustdocs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mv "$RUSTDOC_OUTPUT" "$TARGET_DIR/baseline.json"

# For each feature, re-run rustdoc with it enabled.
features=(
'struct_marked_non_exhaustive'
'struct_missing'
'struct_pub_field_missing'
'enum_missing'
Expand Down
1 change: 1 addition & 0 deletions semver_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
[dependencies]

[features]
struct_marked_non_exhaustive = []
struct_missing = []
struct_pub_field_missing = []
enum_missing = []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod non_exhaustive;

/// Testing: <https://doc.rust-lang.org/cargo/reference/semver.html#item-remove>
#[cfg(not(feature = "struct_missing"))]
pub struct WillBeRemovedStruct;
Expand Down
62 changes: 62 additions & 0 deletions semver_tests/src/test_cases/non_exhaustive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Adding `non_exhaustive` to a struct or enum variant is breaking because those items
//! cannot be constructed outside of their defining crate:
//!
//! """
/// Non-exhaustive types cannot be constructed outside of the defining crate:
/// - Non-exhaustive variants (struct or enum variant) cannot be constructed with
/// a StructExpression (including with functional update syntax).
/// """
/// From: <https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute>

#[cfg(not(feature = "struct_marked_non_exhaustive"))]
pub struct UnitStruct;

#[cfg(feature = "struct_marked_non_exhaustive")]
#[non_exhaustive]
pub struct UnitStruct;

#[cfg(not(feature = "struct_marked_non_exhaustive"))]
pub struct TupleStruct(pub u64);

#[cfg(feature = "struct_marked_non_exhaustive")]
#[non_exhaustive]
pub struct TupleStruct(pub u64);

#[cfg(not(feature = "struct_marked_non_exhaustive"))]
pub struct ExternallyConstructibleStruct {
pub foo: u64,
}

#[cfg(feature = "struct_marked_non_exhaustive")]
#[non_exhaustive]
pub struct ExternallyConstructibleStruct {
pub foo: u64,
}

// The private field within means this struct cannot be constructed
// outside this crate, so #[non_exhaustive] won't change anything here.
#[cfg(not(feature = "struct_marked_non_exhaustive"))]
pub struct NonExternallyConstructibleTupleStruct(u64);

#[cfg(feature = "struct_marked_non_exhaustive")]
#[non_exhaustive]
pub struct NonExternallyConstructibleTupleStruct(u64);

#[cfg(not(feature = "struct_marked_non_exhaustive"))]
pub struct NonExternallyConstructibleStruct {
pub foo: u64,

// This private field means this struct cannot be constructed with a struct literal
// from outside of this crate.
bar: u64,
}

#[cfg(feature = "struct_marked_non_exhaustive")]
#[non_exhaustive]
pub struct NonExternallyConstructibleStruct {
pub foo: u64,

// This private field means this struct cannot be constructed with a struct literal
// from outside of this crate.
bar: u64,
}
16 changes: 14 additions & 2 deletions src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ mod tests {
std::fs::read_to_string(&format!("./src/test_data/{}.output.ron", query_name))
.with_context(|| format!("Could not load src/test_data/{}.output.ron expected-outputs file, did you forget to add it?", query_name))
.expect("failed to load expected outputs");
let expected_results: Vec<BTreeMap<String, FieldValue>> =
let mut expected_results: Vec<BTreeMap<String, FieldValue>> =
ron::from_str(&expected_result_text)
.expect("could not parse expected outputs as ron format");

Expand All @@ -714,10 +714,21 @@ mod tests {
);
let results_iter = interpret_ir(adapter.clone(), parsed_query, args).unwrap();

let actual_results: Vec<BTreeMap<_, _>> = results_iter
let mut actual_results: Vec<BTreeMap<_, _>> = results_iter
.map(|res| res.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
.collect();

// Reorder both vectors of results into a deterministic order that will compensate for
// nondeterminism in how the results are ordered.
let key_func = |elem: &BTreeMap<String, FieldValue>| {
(
elem["span_filename"].as_str().unwrap().to_owned(),
elem["span_begin_line"].as_usize().unwrap(),
)
};
expected_results.sort_unstable_by_key(key_func);
actual_results.sort_unstable_by_key(key_func);

assert_eq!(expected_results, actual_results);
}

Expand All @@ -737,5 +748,6 @@ mod tests {
enum_variant_missing,
struct_missing,
struct_pub_field_missing,
struct_marked_non_exhaustive,
);
}
66 changes: 66 additions & 0 deletions src/queries/struct_marked_non_exhaustive.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
SemverQuery(
id: "struct_marked_non_exhaustive",
human_readable_name: "struct marked #[non_exhaustive]",
description: "A publicly-visible struct has been marked #[non_exhaustive], but it was previously constructible using a struct literal outside its crate. The #[non_exhaustive] attribute disables that, so this is a major breaking change for code that depends on it.",
required_update: Major,

// TODO: Change the reference link once this cargo docs PR merges:
// https://github.com/rust-lang/cargo/pull/10877
//
// Change to this link:
// https://doc.rust-lang.org/cargo/reference/semver.html#attr-adding-non-exhaustive
reference_link: Some("https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute"),
query: r#"
{
CrateDiff {
baseline {
item {
... on Struct {
visibility_limit @filter(op: "=", value: ["$public"]) @output
name @output @tag
struct_type @output
attrs @filter(op: "not_contains", value: ["$non_exhaustive"])

# Ensure the struct could previously be constructed outside of its crate
# using a struct literal: it did not have any private fields.
field @fold @transform(op: "count") @filter(op: "=", value: ["$zero"]) {
visibility_limit @filter(op: "!=", value: ["$public"])
}

path {
path @output @tag
}

span_: span @optional {
filename @output
begin_line @output
}
}
}
}
current @fold @transform(op: "count") @filter(op: ">", value: ["$zero"]) {
item {
... on Struct {
visibility_limit @filter(op: "=", value: ["$public"])
name @filter(op: "=", value: ["%name"])

# The struct is now marked #[non_exhaustive] so it can't be constructed
# using a struct literal outside of its crate. This is the breaking change.
attrs @filter(op: "contains", value: ["$non_exhaustive"])

path {
path @filter(op: "=", value: ["%path"])
}
}
}
}
}
}"#,
arguments: {
"public": "public",
"non_exhaustive": "#[non_exhaustive]",
"zero": 0,
},
error_message: "A public struct has been marked #[non_exhaustive], which will prevent it from being constructed using a struct literal outside of its crate. It previously had no private fields, so a struct literal could be used to construct it outside its crate.",
per_result_error_template: Some("struct {{name}}, previously in file {{span_filename}}:{{span_begin_line}}"),
)
4 changes: 2 additions & 2 deletions src/test_data/enum_missing.output.ron
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": String("WillBeRemovedEnum"),
"path": List([String("semver_tests"), String("test_cases"), String("WillBeRemovedEnum")]),
"visibility_limit": String("public"),
"span_filename": String("src/test_cases.rs"),
"span_begin_line": Uint64(15),
"span_filename": String("src/test_cases/mod.rs"),
"span_begin_line": Uint64(17),
}
]
4 changes: 2 additions & 2 deletions src/test_data/enum_variant_missing.output.ron
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"variant_name": String("Bar"),
"path": List([String("semver_tests"), String("test_cases"), String("VariantWillBeRemoved")]),
"visibility_limit": String("public"),
"span_filename": String("src/test_cases.rs"),
"span_begin_line": Uint64(22),
"span_filename": String("src/test_cases/mod.rs"),
"span_begin_line": Uint64(24),
}
]
41 changes: 41 additions & 0 deletions src/test_data/struct_marked_non_exhaustive.output.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[
{
"name": String("UnitStruct"),
"struct_type": String("unit"),
"path": List([
String("semver_tests"),
String("test_cases"),
String("non_exhaustive"),
String("UnitStruct"),
]),
"span_filename": String("src/test_cases/non_exhaustive.rs"),
"span_begin_line": Uint64(12),
"visibility_limit": String("public"),
},
{
"name": String("TupleStruct"),
"struct_type": String("tuple"),
"path": List([
String("semver_tests"),
String("test_cases"),
String("non_exhaustive"),
String("TupleStruct"),
]),
"span_filename": String("src/test_cases/non_exhaustive.rs"),
"span_begin_line": Uint64(19),
"visibility_limit": String("public"),
},
{
"name": String("ExternallyConstructibleStruct"),
"struct_type": String("plain"),
"path": List([
String("semver_tests"),
String("test_cases"),
String("non_exhaustive"),
String("ExternallyConstructibleStruct"),
]),
"span_filename": String("src/test_cases/non_exhaustive.rs"),
"span_begin_line": Uint64(26),
"visibility_limit": String("public"),
}
]
4 changes: 2 additions & 2 deletions src/test_data/struct_missing.output.ron
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"struct_type": String("unit"),
"path": List([String("semver_tests"), String("test_cases"), String("WillBeRemovedStruct")]),
"visibility_limit": String("public"),
"span_filename": String("src/test_cases.rs"),
"span_begin_line": Uint64(3),
"span_filename": String("src/test_cases/mod.rs"),
"span_begin_line": Uint64(5),
}
]
4 changes: 2 additions & 2 deletions src/test_data/struct_pub_field_missing.output.ron
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"struct_type": String("plain"),
"path": List([String("semver_tests"), String("test_cases"), String("FieldWillBeRemoved")]),
"field_name": String("bar"),
"span_filename": String("src/test_cases.rs"),
"span_begin_line": Uint64(10),
"span_filename": String("src/test_cases/mod.rs"),
"span_begin_line": Uint64(12),
}
]