use crate::ast;
use crate::ast::DirectiveLocation;
use crate::ast::Type;
use crate::coordinate::SchemaCoordinate;
use crate::coordinate::TypeAttributeCoordinate;
use crate::diagnostic::CliReport;
use crate::executable;
use crate::parser::SourceSpan;
use crate::Name;
use crate::Node;
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error, Clone, Hash, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) enum DiagnosticData {
#[error("the variable `${name}` is declared multiple times")]
UniqueVariable {
name: Name,
original_definition: Option<SourceSpan>,
redefined_definition: Option<SourceSpan>,
},
#[error("the argument `{name}` is provided multiple times")]
UniqueArgument {
name: Name,
original_definition: Option<SourceSpan>,
redefined_definition: Option<SourceSpan>,
},
#[error("the value `{name}` is defined multiple times")]
UniqueInputValue {
name: Name,
original_definition: Option<SourceSpan>,
redefined_definition: Option<SourceSpan>,
},
#[error("the argument `{name}` is not supported by `{coordinate}`")]
UndefinedArgument {
name: Name,
coordinate: SchemaCoordinate,
definition_location: Option<SourceSpan>,
},
#[error("cannot find type `{name}` in this document")]
UndefinedDefinition {
name: Name,
},
#[error("cannot find directive `@{name}` in this document")]
UndefinedDirective {
name: Name,
},
#[error("variable `${name}` is not defined")]
UndefinedVariable {
name: Name,
},
#[error("cannot find fragment `{name}` in this document")]
UndefinedFragment {
name: Name,
},
#[error("value `{value}` does not exist on `{definition}`")]
UndefinedEnumValue {
value: Name,
definition: Name,
definition_location: Option<SourceSpan>,
},
#[error("field `{value}` does not exist on `{definition}`")]
UndefinedInputValue {
value: Name,
definition: Name,
definition_location: Option<SourceSpan>,
},
#[error("type `{name}` does not satisfy interface `{interface}`: missing field `{field}`")]
MissingInterfaceField {
name: Name,
implements_location: Option<SourceSpan>,
interface: Name,
field: Name,
field_location: Option<SourceSpan>,
},
#[error("the required argument `{coordinate}` is not provided")]
RequiredArgument {
name: Name,
coordinate: SchemaCoordinate,
expected_type: Node<Type>,
definition_location: Option<SourceSpan>,
},
#[error("the required field `{coordinate}` is not provided")]
RequiredField {
name: Name,
coordinate: TypeAttributeCoordinate,
expected_type: Node<Type>,
definition_location: Option<SourceSpan>,
},
#[error(
"interface `{interface}` declares that it implements `{via_interface}`, but to do so it must also implement `{missing_interface}`"
)]
TransitiveImplementedInterfaces {
interface: Name,
via_interface: Name,
transitive_interface_location: Option<SourceSpan>,
missing_interface: Name,
},
#[error("`{name}` field must return an output type")]
OutputType {
name: Name,
describe_type: &'static str,
type_location: Option<SourceSpan>,
},
#[error("`{name}` field must be of an input type")]
InputType {
name: Name,
describe_type: &'static str,
type_location: Option<SourceSpan>,
},
#[error("`${name}` variable must be of an input type")]
VariableInputType {
name: Name,
ty: Node<Type>,
describe_type: &'static str,
},
#[error("missing query root operation type in schema definition")]
QueryRootOperationType,
#[error("unused variable: `${name}`")]
UnusedVariable { name: Name },
#[error("`{name}` field must return an object type")]
RootOperationObjectType {
name: Name,
describe_type: &'static str,
},
#[error("union member `{name}` must be an object type")]
UnionMemberObjectType {
name: Name,
describe_type: &'static str,
},
#[error("{name} directive is not supported for {location} location")]
UnsupportedLocation {
name: Name,
location: DirectiveLocation,
valid_locations: Vec<DirectiveLocation>,
definition_location: Option<SourceSpan>,
},
#[error("expected value of type {ty}, found {}", .value.describe())]
UnsupportedValueType {
value: Node<ast::Value>,
ty: Node<Type>,
definition_location: Option<SourceSpan>,
},
#[error("int cannot represent non 32-bit signed integer value")]
IntCoercionError {
value: String,
},
#[error("float cannot represent non-finite 64-bit floating point value")]
FloatCoercionError {
value: String,
},
#[error("non-repeatable directive {name} can only be used once per location")]
UniqueDirective {
name: Name,
original_application: Option<SourceSpan>,
},
#[error("interface, union and object types must have a subselection set")]
MissingSubselection {
coordinate: TypeAttributeCoordinate,
output_type: Name,
describe_type: &'static str,
},
#[error(
"{} must have a composite type in its type condition",
fragment_name_or_inline(name)
)]
InvalidFragmentTarget {
name: Option<Name>,
ty: Name,
},
#[error(
"{} with type condition `{type_condition}` cannot be applied to `{type_name}`",
fragment_name_or_inline(name)
)]
InvalidFragmentSpread {
name: Option<Name>,
type_name: Name,
type_condition: Name,
fragment_location: Option<SourceSpan>,
type_location: Option<SourceSpan>,
},
#[error("fragment `{name}` must be used in an operation")]
UnusedFragment {
name: Name,
},
#[error(
"variable `${variable}` of type `{variable_type}` cannot be used for argument `{argument}` of type `{argument_type}`"
)]
DisallowedVariableUsage {
variable: Name,
variable_type: Type,
variable_location: Option<SourceSpan>,
argument: Name,
argument_type: Type,
argument_location: Option<SourceSpan>,
},
#[error("`{name}` directive definition cannot reference itself")]
RecursiveDirectiveDefinition {
name: Name,
trace: Vec<Node<ast::Directive>>,
},
#[error("interface {name} cannot implement itself")]
RecursiveInterfaceDefinition { name: Name },
#[error("`{name}` input object cannot reference itself")]
RecursiveInputObjectDefinition {
name: Name,
trace: Vec<Node<ast::InputValueDefinition>>,
},
#[error("`{name}` fragment cannot reference itself")]
RecursiveFragmentDefinition {
head_location: Option<SourceSpan>,
name: Name,
trace: Vec<Node<executable::FragmentSpread>>,
},
#[error("`{name}` contains too much nesting")]
DeeplyNestedType {
name: Name,
describe_type: &'static str,
},
#[error("too much recursion")]
RecursionError {},
#[error("`{type_name}` has no fields")]
EmptyFieldSet {
type_name: Name,
type_location: Option<SourceSpan>,
extensions_locations: Vec<Option<SourceSpan>>,
},
#[error("`{type_name}` has no enum values")]
EmptyValueSet {
type_name: Name,
type_location: Option<SourceSpan>,
extensions_locations: Vec<Option<SourceSpan>>,
},
#[error("`{type_name}` has no member types")]
EmptyMemberSet {
type_name: Name,
type_location: Option<SourceSpan>,
extensions_locations: Vec<Option<SourceSpan>>,
},
#[error("`{type_name}` has no input values")]
EmptyInputValueSet {
type_name: Name,
type_location: Option<SourceSpan>,
extensions_locations: Vec<Option<SourceSpan>>,
},
#[error(
"{describe} cannot be named `{name}` as names starting with two underscores are reserved"
)]
ReservedName { name: Name, describe: &'static str },
}
impl DiagnosticData {
pub(crate) fn report(&self, main_location: Option<SourceSpan>, report: &mut CliReport) {
match self {
DiagnosticData::UniqueVariable {
name,
original_definition,
redefined_definition,
} => {
report.with_label_opt(
*original_definition,
format_args!("previous definition of `${name}` here"),
);
report.with_label_opt(
*redefined_definition,
format_args!("`${name}` defined again here"),
);
}
DiagnosticData::UniqueArgument {
name,
original_definition,
redefined_definition,
} => {
report.with_label_opt(
*original_definition,
format_args!("previously provided `{name}` here"),
);
report.with_label_opt(
*redefined_definition,
format_args!("`{name}` provided again here"),
);
report.with_help(format_args!(
"`{name}` argument must only be provided once."
));
}
DiagnosticData::UniqueInputValue {
name,
original_definition,
redefined_definition,
} => {
report.with_label_opt(
*original_definition,
format_args!("previous definition of `{name}` here"),
);
report.with_label_opt(
*redefined_definition,
format_args!("`{name}` defined again here"),
);
report.with_help(format_args!(
"`{name}` must only be defined once in this argument list or input object definition."
));
}
DiagnosticData::UndefinedArgument {
coordinate,
definition_location,
..
} => {
report.with_label_opt(main_location, "argument by this name not found");
report.with_label_opt(
*definition_location,
format_args!("{coordinate} defined here"),
);
}
DiagnosticData::RequiredArgument {
name,
coordinate: _,
expected_type: _,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("missing value for argument `{name}`"),
);
report.with_label_opt(*definition_location, "argument defined here");
}
DiagnosticData::RequiredField {
name,
coordinate: _,
expected_type: _,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("missing value for field `{name}`"),
);
report.with_label_opt(*definition_location, "field defined here");
}
DiagnosticData::UndefinedDefinition { .. } => {
report.with_label_opt(main_location, "not found in this scope");
}
DiagnosticData::UndefinedDirective { .. } => {
report.with_label_opt(main_location, "directive not defined");
}
DiagnosticData::UndefinedVariable { .. } => {
report.with_label_opt(main_location, "not found in this scope");
}
DiagnosticData::UndefinedFragment { name } => {
report.with_label_opt(
main_location,
format_args!("fragment `{name}` is not defined"),
);
}
DiagnosticData::UndefinedEnumValue {
value: _,
definition,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("value does not exist on `{definition}` enum"),
);
report.with_label_opt(*definition_location, "enum defined here");
}
DiagnosticData::UndefinedInputValue {
value: _,
definition,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("value does not exist on `{definition}` input object"),
);
report.with_label_opt(*definition_location, "input object defined here");
}
DiagnosticData::RecursiveDirectiveDefinition { name, trace } => {
report.with_label_opt(main_location, "recursive directive definition");
label_recursive_trace(report, trace, name, |directive| &directive.name);
}
DiagnosticData::RecursiveInterfaceDefinition { name } => {
report.with_label_opt(
main_location,
format_args!("interface {name} cannot implement itself"),
);
}
DiagnosticData::RecursiveInputObjectDefinition { name, trace } => {
report.with_label_opt(main_location, "cyclical input object definition");
label_recursive_trace(report, trace, name, |reference| &reference.name);
}
DiagnosticData::RecursiveFragmentDefinition {
head_location,
name,
trace,
} => {
report.with_label_opt(
head_location.or(main_location),
"recursive fragment definition",
);
label_recursive_trace(report, trace, name, |reference| &reference.fragment_name);
}
DiagnosticData::DeeplyNestedType { describe_type, .. } => {
report.with_label_opt(
main_location,
format_args!(
"references a very long chain of {describe_type}s in its definition"
),
);
}
DiagnosticData::MissingInterfaceField {
name: _,
implements_location,
interface,
field,
field_location,
} => {
report.with_label_opt(
main_location,
format_args!("add `{field}` field to this type"),
);
report.with_label_opt(
*implements_location,
format_args!("implementation of interface {interface} declared here"),
);
report.with_label_opt(
*field_location,
format_args!("`{interface}.{field}` originally defined here"),
);
report.with_help(
"An object or interface must declare all fields required by the interfaces it implements",
)
}
DiagnosticData::TransitiveImplementedInterfaces {
interface: _,
via_interface,
transitive_interface_location,
missing_interface,
} => {
report.with_label_opt(
*transitive_interface_location,
format!(
"implementation of {missing_interface} declared by {via_interface} here"
),
);
report.with_label_opt(
main_location,
format_args!("{missing_interface} must also be implemented here"),
);
}
DiagnosticData::UnusedVariable { .. } => {
report.with_label_opt(main_location, "variable is never used");
}
DiagnosticData::UnusedFragment { name } => {
report.with_label_opt(main_location, format_args!("`{name}` is defined here"));
report.with_help(format_args!(
"fragment `{name}` must be used in an operation"
));
}
DiagnosticData::RootOperationObjectType {
name: _,
describe_type,
} => {
report.with_label_opt(main_location, format_args!("this is {describe_type}"));
report.with_help("Root operation type must be an object type.");
}
DiagnosticData::UnionMemberObjectType {
name: _,
describe_type,
} => {
report.with_label_opt(main_location, format_args!("this is {describe_type}"));
report.with_help("Union members must be object types.");
}
DiagnosticData::OutputType {
name,
describe_type,
type_location,
} => {
report.with_label_opt(
type_location.or(main_location),
format_args!("this is {describe_type}"),
);
report.with_help(format!("Scalars, Objects, Interfaces, Unions and Enums are output types. Change `{name}` field to return one of these output types."));
}
DiagnosticData::InputType {
name,
describe_type,
type_location,
} => {
report.with_label_opt(
type_location.or(main_location),
format_args!("this is {describe_type}"),
);
report.with_help(format!("Scalars, Enums, and Input Objects are input types. Change `{name}` field to take one of these input types."));
}
DiagnosticData::VariableInputType {
name: _,
ty,
describe_type,
} => {
report.with_label_opt(
ty.location().or(main_location),
format_args!("this is {describe_type}"),
);
report.with_help("objects, unions, and interfaces cannot be used because variables can only be of input type");
}
DiagnosticData::QueryRootOperationType => {
report.with_label_opt(
main_location,
"`query` root operation type must be defined here",
);
}
DiagnosticData::UnsupportedLocation {
name: _,
location,
valid_locations,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("directive cannot be used on {location}"),
);
report.with_label_opt(*definition_location, "directive defined here");
report.with_help(format!(
"the directive must be used in a location that the service has declared support for: {}",
CommaSeparated(valid_locations),
));
}
DiagnosticData::UnsupportedValueType {
value,
ty,
definition_location,
} => {
report.with_label_opt(
main_location,
format_args!("provided value is {}", value.describe()),
);
report.with_label_opt(
*definition_location,
format_args!("expected type declared here as {ty}"),
);
}
DiagnosticData::IntCoercionError { .. } => {
report.with_label_opt(main_location, "cannot be coerced to a 32-bit integer");
}
DiagnosticData::FloatCoercionError { .. } => {
report.with_label_opt(main_location, "cannot be coerced to a finite 64-bit float");
}
DiagnosticData::UniqueDirective {
name,
original_application,
} => {
report.with_label_opt(
*original_application,
format_args!("directive `@{name}` first called here"),
);
report.with_label_opt(
main_location,
format_args!("directive `@{name}` called again here"),
);
}
DiagnosticData::MissingSubselection {
coordinate,
output_type,
describe_type,
} => {
report.with_label_opt(
main_location,
format_args!(
"`{coordinate}` is {describe_type} `{output_type}` and must select fields"
),
);
}
DiagnosticData::InvalidFragmentTarget { name: _, ty } => {
report.with_label_opt(
main_location,
format!("fragment declares unsupported type condition `{ty}`"),
);
report.with_help("fragments cannot be defined on enums, scalars and input objects");
}
DiagnosticData::InvalidFragmentSpread {
name,
type_name: _,
type_condition,
fragment_location,
type_location,
} => {
if let Some(name) = name {
report.with_label_opt(
main_location,
format_args!("fragment `{name}` cannot be applied"),
);
report.with_label_opt(
*fragment_location,
format_args!(
"fragment declared with type condition `{type_condition}` here"
),
);
} else {
report.with_label_opt(main_location, "inline fragment cannot be applied");
}
report.with_label_opt(
*type_location,
format!("type condition `{type_condition}` is not assignable to this type"),
);
}
DiagnosticData::DisallowedVariableUsage {
variable,
variable_type,
variable_location,
..
} => {
report.with_label_opt(
*variable_location,
format_args!(
"variable `${variable}` of type `{variable_type}` is declared here"
),
);
report.with_label_opt(
main_location,
format_args!("variable `${variable}` used here"),
);
}
DiagnosticData::RecursionError {} => {}
DiagnosticData::EmptyFieldSet {
type_name,
type_location,
extensions_locations,
} => {
Self::report_empty_type(
report,
type_name,
type_location,
extensions_locations,
"fields",
);
}
DiagnosticData::EmptyValueSet {
type_name,
type_location,
extensions_locations,
} => {
Self::report_empty_type(
report,
type_name,
type_location,
extensions_locations,
"enum values",
);
}
DiagnosticData::EmptyMemberSet {
type_name,
type_location,
extensions_locations,
} => {
Self::report_empty_type(
report,
type_name,
type_location,
extensions_locations,
"union member types",
);
}
DiagnosticData::EmptyInputValueSet {
type_name,
type_location,
extensions_locations,
} => {
Self::report_empty_type(
report,
type_name,
type_location,
extensions_locations,
"input values",
);
}
DiagnosticData::ReservedName { name, .. } => {
report.with_label_opt(name.location(), "Pick a different name here");
}
}
}
fn report_empty_type(
report: &mut CliReport,
type_name: &Name,
type_location: &Option<SourceSpan>,
extensions_locations: &[Option<SourceSpan>],
describe_missing_kind: &str,
) {
report.with_label_opt(
*type_location,
format_args!("{type_name} type defined here"),
);
extensions_locations.iter().for_each(|location| {
report.with_label_opt(
*location,
format_args!("{type_name} extension defined here"),
);
});
let and_extensions_message = if !extensions_locations.is_empty() {
" or its type extensions"
} else {
""
};
report.with_help(format!(
"Define one or more {describe_missing_kind} on `{type_name}`{and_extensions_message}."
));
}
}
fn label_recursive_trace<T>(
report: &mut CliReport,
trace: &[Node<T>],
original_name: &str,
get_name: impl Fn(&T) -> &str,
) {
if let Some((cyclical_application, path)) = trace.split_first() {
let mut prev_name = original_name;
for node in path.iter().rev() {
let name = get_name(node);
report.with_label_opt(
node.location(),
format!("`{prev_name}` references `{name}` here..."),
);
prev_name = name;
}
report.with_label_opt(
cyclical_application.location(),
format!("`{prev_name}` circularly references `{original_name}` here"),
);
}
}
struct CommaSeparated<'a, It>(&'a It);
impl<'a, T, It> fmt::Display for CommaSeparated<'a, It>
where
T: fmt::Display,
&'a It: IntoIterator<Item = T>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut it = self.0.into_iter();
if let Some(element) = it.next() {
element.fmt(f)?;
}
for element in it {
f.write_str(", ")?;
element.fmt(f)?;
}
Ok(())
}
}
pub(crate) struct NameOrAnon<'a, T> {
pub name: Option<&'a T>,
pub if_some_prefix: &'a str,
pub if_none: &'a str,
}
impl<T> fmt::Display for NameOrAnon<'_, T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.name {
Some(name) => write!(f, "{} `{}`", self.if_some_prefix, name),
None => f.write_str(self.if_none),
}
}
}
fn fragment_name_or_inline<T>(name: &'_ Option<T>) -> NameOrAnon<'_, T> {
NameOrAnon {
name: name.as_ref(),
if_some_prefix: "fragment",
if_none: "inline fragment",
}
}