From 21a89d731ed46c6fa130c28001ceccd118f380f6 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Mon, 28 Feb 2022 09:16:37 -0800 Subject: [PATCH] Suggest alternate type/interface if you pick one that does not exist Reviewed By: kassens Differential Revision: D34434109 fbshipit-source-id: e4bc56c5d46d64963966f190e4dbb46c27f756e4 --- compiler/crates/graphql-ir/src/errors.rs | 31 +------- compiler/crates/relay-docblock/src/errors.rs | 20 ++++-- compiler/crates/relay-docblock/src/ir.rs | 34 +++++---- ...lver-on-invalid-interface.invalid.expected | 2 +- ...-resolver-on-invalid-type.invalid.expected | 2 +- compiler/crates/schema/src/suggestion_list.rs | 71 ++++++++++++++++++- 6 files changed, 108 insertions(+), 52 deletions(-) diff --git a/compiler/crates/graphql-ir/src/errors.rs b/compiler/crates/graphql-ir/src/errors.rs index 9142c132f08ac..dc6cdf247bd78 100644 --- a/compiler/crates/graphql-ir/src/errors.rs +++ b/compiler/crates/graphql-ir/src/errors.rs @@ -7,7 +7,8 @@ use common::{DiagnosticDisplay, WithDiagnosticData}; use graphql_syntax::OperationKind; -use intern::string_key::{Intern, StringKey}; +use intern::string_key::StringKey; +use schema::suggestion_list::did_you_mean; use schema::{Type, TypeReference}; use thiserror::Error; @@ -466,31 +467,3 @@ impl WithDiagnosticData for ValidationMessageWithData { fn into_box(item: StringKey) -> Box { Box::new(item) } - -/// Given [ A, B, C ] return ' Did you mean A, B, or C?'. -fn did_you_mean(suggestions: &[StringKey]) -> String { - if suggestions.is_empty() { - return "".to_string(); - } - - let suggestions_string = match suggestions.len() { - 1 => format!("`{}`", suggestions[0].lookup()), - 2 => format!("`{}` or `{}`", suggestions[0], suggestions[1]), - _ => { - let mut suggestions = suggestions.to_vec(); - let last_option = suggestions.pop(); - - format!( - "{}, or `{}`", - suggestions - .iter() - .map(|suggestion| format!("`{}`", suggestion.lookup())) - .collect::>() - .join(", "), - last_option.unwrap_or_else(|| "".intern()) - ) - } - }; - - format!(" Did you mean {}?", suggestions_string) -} diff --git a/compiler/crates/relay-docblock/src/errors.rs b/compiler/crates/relay-docblock/src/errors.rs index cdc44821d4137..4f65b2b6c266e 100644 --- a/compiler/crates/relay-docblock/src/errors.rs +++ b/compiler/crates/relay-docblock/src/errors.rs @@ -6,9 +6,10 @@ */ use intern::string_key::StringKey; +use schema::suggestion_list::did_you_mean; use thiserror::Error; -#[derive(Clone, Copy, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Clone, Debug, Error, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ErrorMessages { #[error("Unexpected docblock field \"@{field_name}\"")] UnknownField { field_name: StringKey }, @@ -38,10 +39,15 @@ pub enum ErrorMessages { ExpectedOnTypeOrOnInterface, #[error( - "Invalid interface given for `onInterface`. \"{interface_name}\" is not an existing GraphQL interface." - )] - InvalidOnInterface { interface_name: StringKey }, - - #[error("Invalid type given for `onType`. \"{type_name}\" is not an existing GraphQL type.")] - InvalidOnType { type_name: StringKey }, + "Invalid interface given for `onInterface`. \"{interface_name}\" is not an existing GraphQL interface.{suggestions}", suggestions = did_you_mean(suggestions))] + InvalidOnInterface { + interface_name: StringKey, + suggestions: Vec, + }, + + #[error("Invalid type given for `onType`. \"{type_name}\" is not an existing GraphQL type.{suggestions}", suggestions = did_you_mean(suggestions))] + InvalidOnType { + type_name: StringKey, + suggestions: Vec, + }, } diff --git a/compiler/crates/relay-docblock/src/ir.rs b/compiler/crates/relay-docblock/src/ir.rs index 6488b4f72ee29..ea8d41e9d8730 100644 --- a/compiler/crates/relay-docblock/src/ir.rs +++ b/compiler/crates/relay-docblock/src/ir.rs @@ -15,7 +15,7 @@ use graphql_syntax::{ use intern::string_key::{Intern, StringKey}; use lazy_static::lazy_static; -use schema::{InterfaceID, SDLSchema, Schema}; +use schema::{suggestion_list::GraphQLSuggestions, InterfaceID, SDLSchema, Schema}; lazy_static! { static ref INT_TYPE: StringKey = "Int".intern(); @@ -93,12 +93,16 @@ impl RelayResolverIr { .and_then(|t| t.get_object_id()) { Some(_) => Ok(self.object_definitions(on_type)), - None => Err(vec![Diagnostic::error( - ErrorMessages::InvalidOnType { - type_name: on_type.item, - }, - on_type.location, - )]), + None => { + let suggestor = GraphQLSuggestions::new(schema); + Err(vec![Diagnostic::error( + ErrorMessages::InvalidOnType { + type_name: on_type.item, + suggestions: suggestor.object_type_suggestions(on_type.item), + }, + on_type.location, + )]) + } } } On::Interface(on_interface) => match schema @@ -108,12 +112,16 @@ impl RelayResolverIr { Some(interface_type) => { Ok(self.interface_definitions(on_interface, interface_type, schema)) } - None => Err(vec![Diagnostic::error( - ErrorMessages::InvalidOnInterface { - interface_name: on_interface.item, - }, - on_interface.location, - )]), + None => { + let suggestor = GraphQLSuggestions::new(schema); + Err(vec![Diagnostic::error( + ErrorMessages::InvalidOnInterface { + interface_name: on_interface.item, + suggestions: suggestor.interface_type_suggestions(on_interface.item), + }, + on_interface.location, + )]) + } }, } } diff --git a/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected b/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected index 88a844b4e6ae7..41ed01e01e8d3 100644 --- a/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected +++ b/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-interface.invalid.expected @@ -21,7 +21,7 @@ * again. Anyway, I'm rambling now. Its a page that the user likes. A lot. */ ==================================== ERROR ==================================== -✖︎ Invalid interface given for `onInterface`. "Loser" is not an existing GraphQL interface. +✖︎ Invalid interface given for `onInterface`. "Loser" is not an existing GraphQL interface. Did you mean `Node`? /path/to/test/fixture/relay-resolver-on-invalid-interface.invalid.js:4:17 3 │ * diff --git a/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected b/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected index dda3264af9588..324d04d78cd37 100644 --- a/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected +++ b/compiler/crates/relay-docblock/tests/to_schema/fixtures/relay-resolver-on-invalid-type.invalid.expected @@ -21,7 +21,7 @@ * again. Anyway, I'm rambling now. Its a page that the user likes. A lot. */ ==================================== ERROR ==================================== -✖︎ Invalid type given for `onType`. "Loser" is not an existing GraphQL type. +✖︎ Invalid type given for `onType`. "Loser" is not an existing GraphQL type. Did you mean `User`? /path/to/test/fixture/relay-resolver-on-invalid-type.invalid.js:4:12 3 │ * diff --git a/compiler/crates/schema/src/suggestion_list.rs b/compiler/crates/schema/src/suggestion_list.rs index ca3217b1ac3ea..588a8f6a67bcd 100644 --- a/compiler/crates/schema/src/suggestion_list.rs +++ b/compiler/crates/schema/src/suggestion_list.rs @@ -6,7 +6,7 @@ */ use crate::{SDLSchema, Schema, Type}; -use intern::string_key::StringKey; +use intern::string_key::{Intern, StringKey}; use strsim::damerau_levenshtein; /// Computes the lexical distance between strings A and B. @@ -162,6 +162,46 @@ impl<'schema> GraphQLSuggestions<'schema> { suggestion_list(input, &type_names, GraphQLSuggestions::MAX_SUGGESTIONS) } + pub fn object_type_suggestions(&self, input: StringKey) -> Vec { + if !self.enabled { + return Vec::new(); + } + + let type_names = self + .schema + .get_type_map() + .filter_map(|(type_name, type_)| { + if type_.is_object() { + Some(*type_name) + } else { + None + } + }) + .collect::>(); + + suggestion_list(input, &type_names, GraphQLSuggestions::MAX_SUGGESTIONS) + } + + pub fn interface_type_suggestions(&self, input: StringKey) -> Vec { + if !self.enabled { + return Vec::new(); + } + + let type_names = self + .schema + .get_type_map() + .filter_map(|(type_name, type_)| { + if type_.is_interface() { + Some(*type_name) + } else { + None + } + }) + .collect::>(); + + suggestion_list(input, &type_names, GraphQLSuggestions::MAX_SUGGESTIONS) + } + pub fn field_name_suggestion(&self, type_: Option, input: StringKey) -> Vec { if !self.enabled { return Vec::new(); @@ -195,6 +235,35 @@ impl<'schema> GraphQLSuggestions<'schema> { suggestion_list(input, &field_names, GraphQLSuggestions::MAX_SUGGESTIONS) } } + +/// Given [ A, B, C ] return ' Did you mean A, B, or C?'. +pub fn did_you_mean(suggestions: &[StringKey]) -> String { + if suggestions.is_empty() { + return "".to_string(); + } + + let suggestions_string = match suggestions.len() { + 1 => format!("`{}`", suggestions[0].lookup()), + 2 => format!("`{}` or `{}`", suggestions[0], suggestions[1]), + _ => { + let mut suggestions = suggestions.to_vec(); + let last_option = suggestions.pop(); + + format!( + "{}, or `{}`", + suggestions + .iter() + .map(|suggestion| format!("`{}`", suggestion.lookup())) + .collect::>() + .join(", "), + last_option.unwrap_or_else(|| "".intern()) + ) + } + }; + + format!(" Did you mean {}?", suggestions_string) +} + #[cfg(test)] mod tests { use super::suggestion_list;