#[cfg(doc)]
use crate::{ExecutableDocument, Schema};
pub(crate) mod argument;
pub(crate) mod diagnostics;
pub(crate) mod directive;
pub(crate) mod enum_;
pub(crate) mod field;
pub(crate) mod fragment;
pub(crate) mod input_object;
pub(crate) mod interface;
pub(crate) mod object;
pub(crate) mod operation;
pub(crate) mod scalar;
pub(crate) mod schema;
pub(crate) mod selection;
pub(crate) mod union_;
pub(crate) mod value;
pub(crate) mod variable;
use crate::ast::Name;
use crate::diagnostic::{CliReport, Diagnostic, ToCliReport};
use crate::executable::BuildError as ExecutableBuildError;
use crate::execution::{GraphQLError, Response};
use crate::schema::BuildError as SchemaBuildError;
use crate::SourceMap;
use crate::{Node, NodeLocation};
use indexmap::IndexSet;
use std::fmt;
use std::sync::Arc;
pub(crate) use crate::node::FileId;
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(transparent)]
pub struct Valid<T>(pub(crate) T);
impl<T> Valid<T> {
pub fn assume_valid(document: T) -> Self {
Self(document)
}
pub fn assume_valid_ref(document: &T) -> &Self {
let ptr: *const T = document;
let ptr: *const Valid<T> = ptr.cast();
unsafe { &*ptr }
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::ops::Deref for Valid<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> AsRef<T> for Valid<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T: fmt::Display> fmt::Display for Valid<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub struct WithErrors<T> {
pub partial: T,
pub errors: DiagnosticList,
}
impl<T> fmt::Debug for WithErrors<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.errors.fmt(f)
}
}
impl<T> fmt::Display for WithErrors<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.errors.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct SuspectedValidationBug {
pub message: String,
pub location: Option<NodeLocation>,
}
impl SuspectedValidationBug {
pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
let Self { message, location } = self;
let mut err = GraphQLError::new(message, location, sources);
err.extensions
.insert("APOLLO_SUSPECTED_VALIDATION_BUG", true.into());
err
}
pub fn into_response(self, sources: &SourceMap) -> Response {
Response::from_request_error(self.into_graphql_error(sources))
}
}
#[derive(Clone)]
pub struct DiagnosticList {
pub(crate) sources: SourceMap,
diagnostics_data: Vec<DiagnosticData>,
}
#[derive(thiserror::Error, Debug, Clone)]
#[error("{details}")]
pub struct DiagnosticData {
location: Option<NodeLocation>,
details: Details,
}
#[derive(thiserror::Error, Debug, Clone)]
pub(crate) enum Details {
#[error("{message}")]
ParserLimit { message: String },
#[error("syntax error: {message}")]
SyntaxError { message: String },
#[error("{0}")]
SchemaBuildError(SchemaBuildError),
#[error("{0}")]
ExecutableBuildError(ExecutableBuildError),
#[error(transparent)]
CompilerDiagnostic(diagnostics::DiagnosticData),
#[error("too much recursion")]
RecursionLimitError,
}
impl DiagnosticData {
#[doc(hidden)]
pub fn unstable_error_name(&self) -> Option<&'static str> {
match &self.details {
Details::CompilerDiagnostic(diagnostic) => {
use diagnostics::DiagnosticData::*;
Some(match diagnostic {
RecursionError { .. } => "RecursionError",
UniqueVariable { .. } => "UniqueVariable",
UniqueArgument { .. } => "UniqueArgument",
UniqueInputValue { .. } => "UniqueInputValue",
UndefinedArgument { .. } => "UndefinedArgument",
UndefinedDefinition { .. } => "UndefinedDefinition",
UndefinedDirective { .. } => "UndefinedDirective",
UndefinedVariable { .. } => "UndefinedVariable",
UndefinedFragment { .. } => "UndefinedFragment",
UndefinedEnumValue { .. } => "UndefinedEnumValue",
UndefinedInputValue { .. } => "UndefinedInputValue",
MissingInterfaceField { .. } => "MissingInterfaceField",
RequiredArgument { .. } => "RequiredArgument",
RequiredField { .. } => "RequiredField",
TransitiveImplementedInterfaces { .. } => "TransitiveImplementedInterfaces",
OutputType { .. } => "OutputType",
InputType { .. } => "InputType",
VariableInputType { .. } => "VariableInputType",
QueryRootOperationType { .. } => "QueryRootOperationType",
UnusedVariable { .. } => "UnusedVariable",
RootOperationObjectType { .. } => "RootOperationObjectType",
UnionMemberObjectType { .. } => "UnionMemberObjectType",
UnsupportedLocation { .. } => "UnsupportedLocation",
UnsupportedValueType { .. } => "UnsupportedValueType",
IntCoercionError { .. } => "IntCoercionError",
FloatCoercionError { .. } => "FloatCoercionError",
UniqueDirective { .. } => "UniqueDirective",
MissingSubselection { .. } => "MissingSubselection",
InvalidFragmentTarget { .. } => "InvalidFragmentTarget",
InvalidFragmentSpread { .. } => "InvalidFragmentSpread",
UnusedFragment { .. } => "UnusedFragment",
DisallowedVariableUsage { .. } => "DisallowedVariableUsage",
RecursiveDirectiveDefinition { .. } => "RecursiveDirectiveDefinition",
RecursiveInterfaceDefinition { .. } => "RecursiveInterfaceDefinition",
RecursiveInputObjectDefinition { .. } => "RecursiveInputObjectDefinition",
RecursiveFragmentDefinition { .. } => "RecursiveFragmentDefinition",
DeeplyNestedType { .. } => "DeeplyNestedType",
})
}
Details::ExecutableBuildError(error) => Some(match error {
ExecutableBuildError::UndefinedField { .. } => "UndefinedField",
ExecutableBuildError::TypeSystemDefinition { .. } => "TypeSystemDefinition",
ExecutableBuildError::AmbiguousAnonymousOperation { .. } => {
"AmbiguousAnonymousOperation"
}
ExecutableBuildError::OperationNameCollision { .. } => "OperationNameCollision",
ExecutableBuildError::FragmentNameCollision { .. } => "FragmentNameCollision",
ExecutableBuildError::UndefinedRootOperation { .. } => "UndefinedRootOperation",
ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
"UndefinedTypeInNamedFragmentTypeCondition"
}
ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition { .. } => {
"UndefinedTypeInInlineFragmentTypeCondition"
}
ExecutableBuildError::SubselectionOnScalarType { .. } => "SubselectionOnScalarType",
ExecutableBuildError::SubselectionOnEnumType { .. } => "SubselectionOnEnumType",
ExecutableBuildError::SubscriptionUsesMultipleFields { .. } => {
"SubscriptionUsesMultipleFields"
}
ExecutableBuildError::SubscriptionUsesIntrospection { .. } => {
"SubscriptionUsesIntrospection"
}
ExecutableBuildError::ConflictingFieldType { .. } => "ConflictingFieldType",
ExecutableBuildError::ConflictingFieldName { .. } => "ConflictingFieldName",
ExecutableBuildError::ConflictingFieldArgument { .. } => "ConflictingFieldArgument",
}),
_ => None,
}
}
}
impl ToCliReport for DiagnosticData {
fn location(&self) -> Option<NodeLocation> {
self.location
}
fn report(&self, report: &mut CliReport) {
if let Details::CompilerDiagnostic(diagnostic) = &self.details {
diagnostic.report(self.location, report);
return;
}
match &self.details {
Details::CompilerDiagnostic(_) => unreachable!(),
Details::ParserLimit { message, .. } => report.with_label_opt(self.location, message),
Details::SyntaxError { message, .. } => report.with_label_opt(self.location, message),
Details::SchemaBuildError(err) => match err {
SchemaBuildError::ExecutableDefinition { .. } => report.with_label_opt(
self.location,
"remove this definition, or use `parse_mixed()`",
),
SchemaBuildError::SchemaDefinitionCollision {
previous_location, ..
} => {
report.with_label_opt(*previous_location, "previous `schema` definition here");
report.with_label_opt(self.location, "`schema` redefined here");
report.with_help(
"merge this definition with the previous one, or use `extend schema`",
);
}
SchemaBuildError::DirectiveDefinitionCollision {
previous_location,
name,
..
} => {
report.with_label_opt(
*previous_location,
format_args!("previous definition of `@{name}` here"),
);
report.with_label_opt(self.location, format_args!("`@{name}` redefined here"));
report.with_help("remove or rename one of the definitions");
}
SchemaBuildError::TypeDefinitionCollision {
previous_location,
name,
..
} => {
report.with_label_opt(
*previous_location,
format_args!("previous definition of `{name}` here"),
);
report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
report.with_help("remove or rename one of the definitions, or use `extend`");
}
SchemaBuildError::BuiltInScalarTypeRedefinition { .. } => {
report.with_label_opt(self.location, "remove this scalar definition");
}
SchemaBuildError::OrphanSchemaExtension { .. } => {
report.with_label_opt(self.location, "extension here")
}
SchemaBuildError::OrphanTypeExtension { .. } => {
report.with_label_opt(self.location, "extension here")
}
SchemaBuildError::TypeExtensionKindMismatch { def_location, .. } => {
report.with_label_opt(*def_location, "type definition");
report.with_label_opt(self.location, "extension here")
}
SchemaBuildError::DuplicateRootOperation {
previous_location,
operation_type,
..
} => {
report.with_label_opt(
*previous_location,
format_args!("previous definition of `{operation_type}` here"),
);
report.with_label_opt(
self.location,
format_args!("`{operation_type}` redefined here"),
);
}
SchemaBuildError::DuplicateImplementsInterfaceInObject {
name_at_previous_location,
..
}
| SchemaBuildError::DuplicateImplementsInterfaceInInterface {
name_at_previous_location,
..
} => {
let previous_location = &name_at_previous_location.location();
let name = name_at_previous_location;
report.with_label_opt(
*previous_location,
format_args!("previous implementation of `{name}` here"),
);
report.with_label_opt(
self.location,
format_args!("`{name}` implemented again here"),
);
}
SchemaBuildError::ObjectFieldNameCollision {
name_at_previous_location,
..
}
| SchemaBuildError::InterfaceFieldNameCollision {
name_at_previous_location,
..
}
| SchemaBuildError::EnumValueNameCollision {
name_at_previous_location,
..
}
| SchemaBuildError::UnionMemberNameCollision {
name_at_previous_location,
..
}
| SchemaBuildError::InputFieldNameCollision {
name_at_previous_location,
..
} => {
let previous_location = &name_at_previous_location.location();
let name = name_at_previous_location;
report.with_label_opt(
*previous_location,
format_args!("previous definition of `{name}` here"),
);
report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
}
},
Details::ExecutableBuildError(err) => match err {
ExecutableBuildError::TypeSystemDefinition { .. } => report.with_label_opt(
self.location,
"remove this definition, or use `parse_mixed()`",
),
ExecutableBuildError::AmbiguousAnonymousOperation { .. } => {
report.with_label_opt(self.location, "provide a name for this definition");
report.with_help(
"GraphQL requires operations to be named if the document has more than one",
);
}
ExecutableBuildError::OperationNameCollision {
name_at_previous_location,
..
}
| ExecutableBuildError::FragmentNameCollision {
name_at_previous_location,
..
} => {
let previous_location = &name_at_previous_location.location();
let name = name_at_previous_location;
report.with_label_opt(
*previous_location,
format_args!("previous definition of `{name}` here"),
);
report.with_label_opt(self.location, format_args!("`{name}` redefined here"));
}
ExecutableBuildError::UndefinedRootOperation { operation_type, .. } => {
report.with_label_opt(
self.location,
format_args!(
"`{operation_type}` is not defined in the schema and is therefore not supported"
),
);
report.with_help(format_args!(
"consider defining a `{operation_type}` root operation type in your schema"
))
}
ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
report.with_label_opt(self.location, "type condition here")
}
ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
path, ..
} => {
report.with_label_opt(self.location, "type condition here");
report.with_note(format_args!("path to the inline fragment: `{path} → ...`"))
}
ExecutableBuildError::SubselectionOnScalarType { path, .. }
| ExecutableBuildError::SubselectionOnEnumType { path, .. } => {
report.with_label_opt(self.location, "remove subselections here");
report.with_note(format_args!("path to the field: `{path}`"))
}
ExecutableBuildError::UndefinedField {
field_name,
type_name,
path,
..
} => {
report.with_label_opt(
self.location,
format_args!("field `{field_name}` selected here"),
);
report.with_label_opt(
type_name.location(),
format_args!("type `{type_name}` defined here"),
);
report.with_note(format_args!("path to the field: `{path}`"))
}
ExecutableBuildError::SubscriptionUsesMultipleFields { fields, .. } => {
report.with_label_opt(
self.location,
format_args!("subscription with {} root fields", fields.len()),
);
report.with_help(format_args!(
"There are {} root fields: {}. This is not allowed.",
fields.len(),
CommaSeparated(fields)
));
}
ExecutableBuildError::SubscriptionUsesIntrospection { field, .. } => {
report.with_label_opt(
self.location,
format_args!("{field} is an introspection field"),
);
}
ExecutableBuildError::ConflictingFieldType {
alias,
original_location,
original_coordinate,
original_type,
conflicting_location,
conflicting_coordinate,
conflicting_type,
} => {
report.with_label_opt(
*original_location,
format_args!(
"`{alias}` is selected from `{original_coordinate}: {original_type}` here"
),
);
report.with_label_opt(
*conflicting_location,
format_args!("`{alias}` is selected from `{conflicting_coordinate}: {conflicting_type}` here"),
);
}
ExecutableBuildError::ConflictingFieldArgument {
alias,
original_location,
original_coordinate,
original_value,
conflicting_location,
conflicting_coordinate: _,
conflicting_value,
} => {
let argument = &original_coordinate.argument;
match (original_value, conflicting_value) {
(Some(_), Some(_)) => {
report.with_label_opt(
*original_location,
format_args!(
"`{original_coordinate}` is used with one argument value here"
),
);
report.with_label_opt(
*conflicting_location,
"but a different value here",
);
}
(Some(_), None) => {
report.with_label_opt(
*original_location,
format!("`{alias}` is selected with argument `{argument}` here",),
);
report.with_label_opt(
*conflicting_location,
format!("but argument `{argument}` is not provided here"),
);
}
(None, Some(_)) => {
report.with_label_opt(
*conflicting_location,
format!("`{alias}` is selected with argument `{argument}` here",),
);
report.with_label_opt(
*original_location,
format!("but argument `{argument}` is not provided here"),
);
}
(None, None) => unreachable!(),
}
report.with_help("The same name cannot be selected multiple times with different arguments, because it's not clear which set of arguments should be used to fill the response. If you intend to use diverging arguments, consider adding an alias to differentiate");
}
ExecutableBuildError::ConflictingFieldName {
alias: field,
original_selection,
original_location,
conflicting_selection,
conflicting_location,
} => {
report.with_label_opt(
*original_location,
format_args!("`{field}` is selected from `{original_selection}` here"),
);
report.with_label_opt(
*conflicting_location,
format_args!("`{field}` is selected from `{conflicting_selection}` here"),
);
report.with_help("Both fields may be present on the schema type, so it's not clear which one should be used to fill the response");
}
},
Details::RecursionLimitError => {}
}
}
}
impl DiagnosticList {
pub fn new(sources: SourceMap) -> Self {
Self {
sources,
diagnostics_data: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.diagnostics_data.is_empty()
}
pub fn len(&self) -> usize {
self.diagnostics_data.len()
}
pub fn iter(
&self,
) -> impl DoubleEndedIterator<Item = Diagnostic<'_, DiagnosticData>> + ExactSizeIterator {
self.diagnostics_data
.iter()
.map(|data| data.to_diagnostic(&self.sources))
}
pub(crate) fn push(&mut self, location: Option<NodeLocation>, details: impl Into<Details>) {
self.diagnostics_data.push(DiagnosticData {
location,
details: details.into(),
})
}
pub fn merge(&mut self, other: Self) {
if !Arc::ptr_eq(&self.sources, &other.sources) {
let sources = Arc::make_mut(&mut self.sources);
for (&k, v) in &*other.sources {
sources.entry(k).or_insert_with(|| v.clone());
}
}
self.diagnostics_data.extend(other.diagnostics_data);
self.sort()
}
fn sort(&mut self) {
self.diagnostics_data
.sort_by_key(|err| err.location.map(|loc| (loc.file_id(), loc.offset())));
}
pub(crate) fn into_result(mut self) -> Result<(), Self> {
if self.diagnostics_data.is_empty() {
Ok(())
} else {
self.sort();
Err(self)
}
}
pub(crate) fn into_result_with<T>(self, value: T) -> Result<T, WithErrors<T>> {
match self.into_result() {
Ok(()) => Ok(value),
Err(errors) => Err(WithErrors {
partial: value,
errors,
}),
}
}
pub(crate) fn into_valid_result<T>(self, value: T) -> Result<Valid<T>, WithErrors<T>> {
match self.into_result() {
Ok(()) => Ok(Valid(value)),
Err(errors) => Err(WithErrors {
partial: value,
errors,
}),
}
}
}
impl fmt::Display for DiagnosticList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for diagnostic in self.iter() {
fmt::Display::fmt(&diagnostic, f)?
}
Ok(())
}
}
impl fmt::Debug for DiagnosticList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for diagnostic in self.iter() {
fmt::Debug::fmt(&diagnostic, f)?
}
Ok(())
}
}
impl From<SchemaBuildError> for Details {
fn from(value: SchemaBuildError) -> Self {
Details::SchemaBuildError(value)
}
}
impl From<ExecutableBuildError> for Details {
fn from(value: ExecutableBuildError) -> Self {
Details::ExecutableBuildError(value)
}
}
impl From<diagnostics::DiagnosticData> for Details {
fn from(value: diagnostics::DiagnosticData) -> Self {
Details::CompilerDiagnostic(value)
}
}
const DEFAULT_RECURSION_LIMIT: usize = 32;
#[derive(Debug, Clone, thiserror::Error)]
#[error("Recursion limit reached")]
#[non_exhaustive]
struct RecursionLimitError {}
#[derive(Debug)]
struct RecursionStack {
seen: IndexSet<Name>,
high: usize,
limit: usize,
}
impl RecursionStack {
fn new() -> Self {
Self {
seen: IndexSet::new(),
high: 0,
limit: DEFAULT_RECURSION_LIMIT,
}
}
fn with_root(root: Name) -> Self {
let mut stack = Self::new();
stack.seen.insert(root);
stack
}
fn with_limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
pub(crate) fn guard(&mut self) -> RecursionGuard<'_> {
RecursionGuard(self)
}
}
struct RecursionGuard<'a>(&'a mut RecursionStack);
impl RecursionGuard<'_> {
fn push(&mut self, name: &Name) -> Result<RecursionGuard<'_>, RecursionLimitError> {
let new = self.0.seen.insert(name.clone());
debug_assert!(
new,
"cannot push the same name twice to RecursionGuard, check contains() first"
);
self.0.high = self.0.high.max(self.0.seen.len());
if self.0.seen.len() > self.0.limit {
Err(RecursionLimitError {})
} else {
Ok(RecursionGuard(self.0))
}
}
fn contains(&self, name: &Name) -> bool {
self.0.seen.contains(name)
}
fn first(&self) -> Option<&Name> {
self.0.seen.first()
}
}
impl Drop for RecursionGuard<'_> {
fn drop(&mut self) {
let _ = self.0.seen.pop();
}
}
#[derive(Debug, Clone, thiserror::Error)]
enum CycleError<T> {
#[error("Cycle detected")]
Recursed(Vec<Node<T>>),
#[error(transparent)]
Limit(#[from] RecursionLimitError),
}
impl<T> CycleError<T> {
fn trace(mut self, node: &Node<T>) -> Self {
if let Self::Recursed(trace) = &mut self {
trace.push(node.clone());
}
self
}
}
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(())
}
}