From 78e88f32d7d883aba0b5a51d3f227b0fc9e12172 Mon Sep 17 00:00:00 2001 From: Predrag Gruevski <2348618+obi1kenobi@users.noreply.github.com> Date: Fri, 22 Jul 2022 18:12:58 -0400 Subject: [PATCH] Semver-check for functions being removed. (#22) * Add Function and Method types to the schema. * Add semver check for functions being removed. --- scripts/regenerate_test_rustdocs.sh | 7 +- semver_tests/Cargo.toml | 7 +- semver_tests/src/test_cases/item_missing.rs | 7 +- src/adapter.rs | 169 ++++++++++---------- src/queries/function_missing.ron | 47 ++++++ src/query.rs | 1 + src/rustdoc_schema.graphql | 62 +++++++ src/test_data/function_missing.output.ron | 14 ++ src/test_data/struct_missing.output.ron | 2 +- 9 files changed, 219 insertions(+), 97 deletions(-) create mode 100644 src/queries/function_missing.ron create mode 100644 src/test_data/function_missing.output.ron diff --git a/scripts/regenerate_test_rustdocs.sh b/scripts/regenerate_test_rustdocs.sh index 5c8ae8f5..3e68ceb3 100755 --- a/scripts/regenerate_test_rustdocs.sh +++ b/scripts/regenerate_test_rustdocs.sh @@ -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' ) diff --git a/semver_tests/Cargo.toml b/semver_tests/Cargo.toml index 90b722f0..ab54374d 100644 --- a/semver_tests/Cargo.toml +++ b/semver_tests/Cargo.toml @@ -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 = [] diff --git a/semver_tests/src/test_cases/item_missing.rs b/semver_tests/src/test_cases/item_missing.rs index 9ee5cca1..b08f56a5 100644 --- a/semver_tests/src/test_cases/item_missing.rs +++ b/semver_tests/src/test_cases/item_missing.rs @@ -1,7 +1,10 @@ -/// Testing: +//! Testing: + #[cfg(not(feature = "struct_missing"))] pub struct WillBeRemovedStruct; -/// Testing: #[cfg(not(feature = "enum_missing"))] pub enum WillBeRemovedEnum {} + +#[cfg(not(feature = "function_missing"))] +pub fn will_be_removed_fn() {} diff --git a/src/adapter.rs b/src/adapter.rs index 24fd9f58..1d5b31cc 100644 --- a/src/adapter.rs +++ b/src/adapter.rs @@ -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}, @@ -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), @@ -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> { @@ -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>, field_name: &str, @@ -286,69 +344,13 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { _vertex_hint: Vid, ) -> Box, 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" => { @@ -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" @@ -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}"), } } @@ -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)); @@ -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; @@ -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| { @@ -623,31 +634,12 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { _vertex_hint: Vid, ) -> Box, 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!( @@ -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, ); } diff --git a/src/queries/function_missing.ron b/src/queries/function_missing.ron new file mode 100644 index 00000000..c1e81849 --- /dev/null +++ b/src/queries/function_missing.ron @@ -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}}"), +) diff --git a/src/query.rs b/src/query.rs index 6c442f57..7f76c088 100644 --- a/src/query.rs +++ b/src/query.rs @@ -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"); diff --git a/src/rustdoc_schema.graphql b/src/rustdoc_schema.graphql index f1d6e846..ecff3caf 100644 --- a/src/rustdoc_schema.graphql +++ b/src/rustdoc_schema.graphql @@ -220,3 +220,65 @@ type Path { """ path: [String!]! } + +""" +A function-like entity, like a function, function pointer, or method. + +Combines: +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Header.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.FnDecl.html +""" +interface FunctionLike { + const: Boolean! + unsafe: Boolean! + async: Boolean! +} + +""" +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Item.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/enum.ItemEnum.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Function.html +""" +type Function implements Item & FunctionLike & Importable { + # properties from Item + id: String! + crate_id: Int! + name: String + docs: String + attrs: [String!]! + visibility_limit: String! + + # properties from FunctionLike + const: Boolean! + unsafe: Boolean! + async: Boolean! + + # edges from Item + span: Span + + # edges from Importable + path: [Path!] +} + +""" +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Item.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/enum.ItemEnum.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Method.html +""" +type Method implements Item & FunctionLike { + # properties from Item + id: String! + crate_id: Int! + name: String + docs: String + attrs: [String!]! + visibility_limit: String! + + # properties from FunctionLike + const: Boolean! + unsafe: Boolean! + async: Boolean! + + # edge from Item + span: Span +} diff --git a/src/test_data/function_missing.output.ron b/src/test_data/function_missing.output.ron new file mode 100644 index 00000000..2979a18b --- /dev/null +++ b/src/test_data/function_missing.output.ron @@ -0,0 +1,14 @@ +[ + { + "name": String("will_be_removed_fn"), + "path": List([ + String("semver_tests"), + String("test_cases"), + String("item_missing"), + String("will_be_removed_fn"), + ]), + "visibility_limit": String("public"), + "span_filename": String("src/test_cases/item_missing.rs"), + "span_begin_line": Uint64(10), + } +] diff --git a/src/test_data/struct_missing.output.ron b/src/test_data/struct_missing.output.ron index ca74add3..981111f7 100644 --- a/src/test_data/struct_missing.output.ron +++ b/src/test_data/struct_missing.output.ron @@ -10,6 +10,6 @@ ]), "visibility_limit": String("public"), "span_filename": String("src/test_cases/item_missing.rs"), - "span_begin_line": Uint64(3), + "span_begin_line": Uint64(4), } ]