Skip to content

Commit

Permalink
Semver-check for functions being removed. (#22)
Browse files Browse the repository at this point in the history
* Add Function and Method types to the schema.

* Add semver check for functions being removed.
  • Loading branch information
obi1kenobi authored Jul 22, 2022
1 parent 052df74 commit 78e88f3
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 97 deletions.
7 changes: 4 additions & 3 deletions scripts/regenerate_test_rustdocs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ mv "$RUSTDOC_OUTPUT" "$TARGET_DIR/baseline.json"

# For each feature, re-run rustdoc with it enabled.
features=(
'enum_missing'
'enum_variant_added'
'enum_variant_missing'
'function_missing'
'struct_marked_non_exhaustive'
'struct_missing'
'struct_pub_field_missing'
'enum_missing'
'enum_variant_missing'
'enum_variant_added'
'unit_struct_changed_kind'
'variant_marked_non_exhaustive'
)
Expand Down
7 changes: 4 additions & 3 deletions semver_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ edition = "2021"
[dependencies]

[features]
enum_missing = []
enum_variant_added = []
enum_variant_missing = []
function_missing = []
struct_marked_non_exhaustive = []
struct_missing = []
struct_pub_field_missing = []
enum_missing = []
enum_variant_missing = []
enum_variant_added = []
unit_struct_changed_kind = []
variant_marked_non_exhaustive = []
7 changes: 5 additions & 2 deletions semver_tests/src/test_cases/item_missing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/// Testing: <https://doc.rust-lang.org/cargo/reference/semver.html#item-remove>
//! Testing: <https://doc.rust-lang.org/cargo/reference/semver.html#item-remove>

#[cfg(not(feature = "struct_missing"))]
pub struct WillBeRemovedStruct;

/// Testing: <https://doc.rust-lang.org/cargo/reference/semver.html#item-remove>
#[cfg(not(feature = "enum_missing"))]
pub enum WillBeRemovedEnum {}

#[cfg(not(feature = "function_missing"))]
pub fn will_be_removed_fn() {}
169 changes: 81 additions & 88 deletions src/adapter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use rustdoc_types::{Crate, Enum, Item, Span, Struct, Type, Variant};
use rustdoc_types::{Crate, Enum, Function, Item, Method, Span, Struct, Type, Variant};
use trustfall_core::{
interpreter::{Adapter, DataContext, InterpretedQuery},
ir::{EdgeParameters, Eid, FieldValue, Vid},
Expand Down Expand Up @@ -80,6 +80,29 @@ pub enum TokenKind<'a> {

#[allow(dead_code)]
impl<'a> Token<'a> {
/// The name of the actual runtime type of this token,
/// intended to fulfill resolution requests for the __typename property.
#[inline]
fn typename(&self) -> &'static str {
match self.kind {
TokenKind::Item(item) => match &item.inner {
rustdoc_types::ItemEnum::Struct(..) => "Struct",
rustdoc_types::ItemEnum::Enum(..) => "Enum",
rustdoc_types::ItemEnum::Function(..) => "Function",
rustdoc_types::ItemEnum::Method(..) => "Method",
rustdoc_types::ItemEnum::Variant(Variant::Plain) => "PlainVariant",
rustdoc_types::ItemEnum::Variant(Variant::Tuple(..)) => "TupleVariant",
rustdoc_types::ItemEnum::Variant(Variant::Struct(..)) => "StructVariant",
rustdoc_types::ItemEnum::StructField(..) => "StructField",
_ => unreachable!("unexpected item.inner for item: {item:?}"),
},
TokenKind::Span(..) => "Span",
TokenKind::Path(..) => "Path",
TokenKind::Crate(..) => "Crate",
TokenKind::CrateDiff(..) => "CrateDiff",
}
}

fn as_crate_diff(&self) -> Option<(&'a Crate, &'a Crate)> {
match &self.kind {
TokenKind::CrateDiff(tuple) => Some(*tuple),
Expand Down Expand Up @@ -142,6 +165,20 @@ impl<'a> Token<'a> {
_ => None,
}
}

fn as_function(&self) -> Option<&'a Function> {
self.as_item().and_then(|item| match &item.inner {
rustdoc_types::ItemEnum::Function(func) => Some(func),
_ => None,
})
}

fn as_method(&self) -> Option<&'a Method> {
self.as_item().and_then(|item| match &item.inner {
rustdoc_types::ItemEnum::Method(func) => Some(func),
_ => None,
})
}
}

impl<'a> From<&'a Item> for TokenKind<'a> {
Expand Down Expand Up @@ -239,6 +276,27 @@ fn get_path_property(token: &Token, field_name: &str) -> FieldValue {
}
}

fn get_function_like_property(token: &Token, field_name: &str) -> FieldValue {
let maybe_function = token.as_function();
let maybe_method = token.as_method();

let (header, _decl) = maybe_function
.map(|func| (&func.header, &func.decl))
.unwrap_or_else(|| {
let method = maybe_method.unwrap_or_else(|| {
unreachable!("token was neither a function nor a method: {token:?}")
});
(&method.header, &method.decl)
});

match field_name {
"const" => header.const_.into(),
"async" => header.async_.into(),
"unsafe" => header.unsafe_.into(),
_ => unreachable!("FunctionLike property {field_name}"),
}
}

fn property_mapper<'a>(
ctx: DataContext<Token<'a>>,
field_name: &str,
Expand Down Expand Up @@ -286,69 +344,13 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
_vertex_hint: Vid,
) -> Box<dyn Iterator<Item = (DataContext<Self::DataToken>, FieldValue)> + 'a> {
if field_name.as_ref() == "__typename" {
match current_type_name.as_ref() {
"Crate" | "Struct" | "StructField" | "Span" | "Enum" | "PlainVariant"
| "TupleVariant" | "StructVariant" => {
// These types have no subtypes, so their __typename
// is always equal to their statically-determined type.
let typename: FieldValue = current_type_name.as_ref().into();
Box::new(data_contexts.map(move |ctx| {
if ctx.current_token.is_some() {
(ctx, typename.clone())
} else {
(ctx, FieldValue::Null)
}
}))
}
"Variant" => {
// Inspect the inner type of the token and
// output the appropriate __typename value.
Box::new(data_contexts.map(|ctx| {
let value = match &ctx.current_token {
None => FieldValue::Null,
Some(token) => match token.as_item() {
Some(item) => match &item.inner {
rustdoc_types::ItemEnum::Variant(Variant::Plain) => {
"PlainVariant".into()
}
rustdoc_types::ItemEnum::Variant(Variant::Tuple(..)) => {
"TupleVariant".into()
}
rustdoc_types::ItemEnum::Variant(Variant::Struct(..)) => {
"StructVariant".into()
}
_ => {
unreachable!("unexpected item.inner type: {:?}", item.inner)
}
},
_ => unreachable!("unexpected token type: {token:?}"),
},
};
(ctx, value)
}))
}
"Item" => {
// Inspect the inner type of the token and
// output the appropriate __typename value.
Box::new(data_contexts.map(|ctx| {
let value = match &ctx.current_token {
None => FieldValue::Null,
Some(token) => match token.as_item() {
Some(item) => match &item.inner {
rustdoc_types::ItemEnum::Struct(_) => "Struct".into(),
rustdoc_types::ItemEnum::StructField(_) => "StructField".into(),
_ => {
unreachable!("unexpected item.inner type: {:?}", item.inner)
}
},
_ => unreachable!("unexpected token type: {token:?}"),
},
};
(ctx, value)
}))
Box::new(data_contexts.map(|ctx| match &ctx.current_token {
Some(token) => {
let value = token.typename().into();
(ctx, value)
}
_ => unreachable!("project_property for __typename on {current_type_name}"),
}
None => (ctx, FieldValue::Null),
}))
} else {
match current_type_name.as_ref() {
"Crate" => {
Expand All @@ -362,7 +364,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
}))
}
"Struct" | "StructField" | "Enum" | "Variant" | "PlainVariant" | "TupleVariant"
| "StructVariant"
| "StructVariant" | "Function" | "Method"
if matches!(
field_name.as_ref(),
"id" | "crate_id" | "name" | "docs" | "attrs" | "visibility_limit"
Expand Down Expand Up @@ -391,6 +393,13 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
property_mapper(ctx, field_name.as_ref(), get_path_property)
}))
}
"FunctionLike" | "Function" | "Method"
if matches!(field_name.as_ref(), "const" | "unsafe" | "async") =>
{
Box::new(data_contexts.map(move |ctx| {
property_mapper(ctx, field_name.as_ref(), get_function_like_property)
}))
}
_ => unreachable!("project_property {current_type_name} {field_name}"),
}
}
Expand Down Expand Up @@ -470,6 +479,8 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
| rustdoc_types::ItemEnum::StructField(..)
| rustdoc_types::ItemEnum::Enum(..)
| rustdoc_types::ItemEnum::Variant(..)
| rustdoc_types::ItemEnum::Function(..)
| rustdoc_types::ItemEnum::Method(..)
)
})
.map(move |value| origin.make_item_token(value));
Expand All @@ -484,7 +495,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
),
}
}
"Importable" | "Struct" | "Enum" if edge_name.as_ref() == "path" => {
"Importable" | "Struct" | "Enum" | "Function" if edge_name.as_ref() == "path" => {
let current_crate = self.current_crate;
let previous_crate = self.previous_crate;

Expand Down Expand Up @@ -518,7 +529,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
}))
}
"Item" | "Struct" | "StructField" | "Enum" | "Variant" | "PlainVariant"
| "TupleVariant" | "StructVariant"
| "TupleVariant" | "StructVariant" | "Function" | "Method"
if edge_name.as_ref() == "span" =>
{
Box::new(data_contexts.map(move |ctx| {
Expand Down Expand Up @@ -623,31 +634,12 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> {
_vertex_hint: Vid,
) -> Box<dyn Iterator<Item = (DataContext<Self::DataToken>, bool)> + 'a> {
match current_type_name.as_ref() {
"Item" | "Variant" => {
"Item" | "Variant" | "FunctionLike" => {
Box::new(data_contexts.map(move |ctx| {
let can_coerce = match &ctx.current_token {
None => false,
Some(token) => {
let actual_type_name = match token.as_item() {
Some(item) => match &item.inner {
rustdoc_types::ItemEnum::Struct(..) => "Struct",
rustdoc_types::ItemEnum::StructField(..) => "StructField",
rustdoc_types::ItemEnum::Enum(..) => "Enum",
rustdoc_types::ItemEnum::Variant(Variant::Plain) => {
"PlainVariant"
}
rustdoc_types::ItemEnum::Variant(Variant::Tuple(..)) => {
"TupleVariant"
}
rustdoc_types::ItemEnum::Variant(Variant::Struct(..)) => {
"StructVariant"
}
_ => {
unreachable!("unexpected item.inner type: {:?}", item.inner)
}
},
_ => unreachable!("unexpected token type: {token:?}"),
};
let actual_type_name = token.typename();

match coerce_to_type_name.as_ref() {
"Variant" => matches!(
Expand Down Expand Up @@ -751,10 +743,11 @@ mod tests {
enum_missing,
enum_variant_added,
enum_variant_missing,
function_missing,
struct_marked_non_exhaustive,
struct_missing,
struct_pub_field_missing,
unit_struct_changed_kind,
struct_marked_non_exhaustive,
variant_marked_non_exhaustive,
);
}
47 changes: 47 additions & 0 deletions src/queries/function_missing.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
SemverQuery(
id: "function_missing",
human_readable_name: "pub fn removed or renamed",
description: "A publicly-visible function is no longer available under its prior name, which is a major breaking change for code that depends on it.",
required_update: Major,
reference_link: Some("https://doc.rust-lang.org/cargo/reference/semver.html#item-remove"),
query: r#"
{
CrateDiff {
baseline {
item {
... on Function {
visibility_limit @filter(op: "=", value: ["$public"]) @output
name @output @tag
path {
path @output @tag
}
span_: span @optional {
filename @output
begin_line @output
}
}
}
}
current @fold @transform(op: "count") @filter(op: "=", value: ["$zero"]) {
item {
... on Function {
visibility_limit @filter(op: "=", value: ["$public"])
name @filter(op: "=", value: ["%name"])
path {
path @filter(op: "=", value: ["%path"])
}
}
}
}
}
}"#,
arguments: {
"public": "public",
"zero": 0,
},
error_message: "A publicly-visible function is no longer available under its prior name. It may have been renamed or removed entirely.",
per_result_error_template: Some("function {{name}}, previously in file {{span_filename}}:{{span_begin_line}}"),
)
1 change: 1 addition & 0 deletions src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ impl SemverQuery {
include_str!("./queries/struct_pub_field_missing.ron"),
include_str!("./queries/unit_struct_changed_kind.ron"),
include_str!("./queries/variant_marked_non_exhaustive.ron"),
include_str!("./queries/function_missing.ron"),
];
for query_text in query_text_contents {
let query: SemverQuery = ron::from_str(query_text).expect("query failed to parse");
Expand Down
Loading

0 comments on commit 78e88f3

Please sign in to comment.