Skip to content

Commit

Permalink
Suggest alternate type/interface if you pick one that does not exist
Browse files Browse the repository at this point in the history
Reviewed By: kassens

Differential Revision: D34434109

fbshipit-source-id: e4bc56c5d46d64963966f190e4dbb46c27f756e4
  • Loading branch information
captbaritone authored and facebook-github-bot committed Feb 28, 2022
1 parent dbe2c9f commit 21a89d7
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 52 deletions.
31 changes: 2 additions & 29 deletions compiler/crates/graphql-ir/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -466,31 +467,3 @@ impl WithDiagnosticData for ValidationMessageWithData {
fn into_box(item: StringKey) -> Box<dyn DiagnosticDisplay> {
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::<Vec<String>>()
.join(", "),
last_option.unwrap_or_else(|| "".intern())
)
}
};

format!(" Did you mean {}?", suggestions_string)
}
20 changes: 13 additions & 7 deletions compiler/crates/relay-docblock/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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<StringKey>,
},

#[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<StringKey>,
},
}
34 changes: 21 additions & 13 deletions compiler/crates/relay-docblock/src/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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,
)])
}
},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 │ *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 │ *
Expand Down
71 changes: 70 additions & 1 deletion compiler/crates/schema/src/suggestion_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<StringKey> {
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::<Vec<StringKey>>();

suggestion_list(input, &type_names, GraphQLSuggestions::MAX_SUGGESTIONS)
}

pub fn interface_type_suggestions(&self, input: StringKey) -> Vec<StringKey> {
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::<Vec<StringKey>>();

suggestion_list(input, &type_names, GraphQLSuggestions::MAX_SUGGESTIONS)
}

pub fn field_name_suggestion(&self, type_: Option<Type>, input: StringKey) -> Vec<StringKey> {
if !self.enabled {
return Vec::new();
Expand Down Expand Up @@ -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::<Vec<String>>()
.join(", "),
last_option.unwrap_or_else(|| "".intern())
)
}
};

format!(" Did you mean {}?", suggestions_string)
}

#[cfg(test)]
mod tests {
use super::suggestion_list;
Expand Down

0 comments on commit 21a89d7

Please sign in to comment.