use std::{fmt, sync::Arc};
use crate::database::hir::{DirectiveLocation, HirNodeLocation};
use crate::database::{InputDatabase, SourceCache};
use crate::FileId;
use thiserror::Error;
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GraphQLLocation {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GraphQLError {
pub message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub locations: Vec<GraphQLLocation>,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct DiagnosticLocation {
file_id: FileId,
offset: usize,
length: usize,
}
impl DiagnosticLocation {
pub fn file_id(&self) -> FileId {
self.file_id
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn node_len(&self) -> usize {
self.length
}
}
impl From<(FileId, rowan::TextRange)> for DiagnosticLocation {
fn from((file_id, range): (FileId, rowan::TextRange)) -> Self {
Self {
file_id,
offset: range.start().into(),
length: range.len().into(),
}
}
}
impl From<(FileId, usize, usize)> for DiagnosticLocation {
fn from((file_id, offset, length): (FileId, usize, usize)) -> Self {
Self {
file_id,
offset,
length,
}
}
}
impl From<HirNodeLocation> for DiagnosticLocation {
fn from(location: HirNodeLocation) -> Self {
Self {
file_id: location.file_id(),
offset: location.offset(),
length: location.node_len(),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Label {
pub location: DiagnosticLocation,
pub text: String,
}
impl Label {
pub fn new(location: impl Into<DiagnosticLocation>, text: impl Into<String>) -> Self {
Self {
location: location.into(),
text: text.into(),
}
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub struct ApolloDiagnostic {
cache: Arc<SourceCache>,
pub location: DiagnosticLocation,
pub labels: Vec<Label>,
pub help: Option<String>,
pub data: Box<DiagnosticData>,
}
impl ApolloDiagnostic {
pub fn new<DB: InputDatabase + ?Sized>(
db: &DB,
location: DiagnosticLocation,
data: DiagnosticData,
) -> Self {
Self {
cache: db.source_cache(),
location,
labels: vec![],
help: None,
data: Box::new(data),
}
}
pub fn help(self, help: impl Into<String>) -> Self {
Self {
help: Some(help.into()),
..self
}
}
pub fn labels(self, labels: impl Into<Vec<Label>>) -> Self {
Self {
labels: labels.into(),
..self
}
}
pub fn label(mut self, label: Label) -> Self {
self.labels.push(label);
self
}
pub fn get_line_column(&self) -> Option<GraphQLLocation> {
self.cache
.get_line_column(self.location.file_id, self.location.offset)
.map(|(line, column)| GraphQLLocation {
line: line + 1,
column: column + 1,
})
}
}
impl fmt::Display for ApolloDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buf = std::io::Cursor::new(Vec::<u8>::new());
self.to_report()
.write(self.cache.as_ref(), &mut buf)
.unwrap();
writeln!(f, "{}", std::str::from_utf8(&buf.into_inner()).unwrap())
}
}
#[derive(Debug, Error, Clone, Hash, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagnosticData {
#[error("syntax error: {message}")]
SyntaxError { message: String },
#[error("limit exceeded: {message}")]
LimitExceeded { message: String },
#[error("expected identifier")]
MissingIdent,
#[error(
"executable documents can only contain executable definitions, {}",
name.as_ref().map(|name| format!("but `{name}` is a(n) {kind}"))
.unwrap_or_else(|| format!("got {kind}"))
)]
ExecutableDefinition {
name: Option<String>,
kind: &'static str,
},
#[error("{ty} `{name}` is defined multiple times")]
UniqueDefinition {
ty: &'static str,
name: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("argument `{name}` is defined multiple times")]
UniqueArgument {
name: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("value `{name}` is defined multiple times")]
UniqueInputValue {
name: String,
original_value: DiagnosticLocation,
redefined_value: DiagnosticLocation,
},
#[error("enum member `{name}` is defined multiple times in `{coordinate}`")]
UniqueEnumValue {
name: String,
coordinate: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("subscription operation has multiple root fields")]
SingleRootField {
fields: usize,
subscription: DiagnosticLocation,
},
#[error("operation type {ty} is not defined")]
UnsupportedOperation {
ty: &'static str,
},
#[error("cannot query field `{field}` on type `{ty}`")]
UndefinedField {
field: String,
ty: String,
},
#[error("the argument `{name}` is not supported by {coordinate}")]
UndefinedArgument { name: String, coordinate: String },
#[error("type `{name}` is not defined")]
UndefinedDefinition {
name: String,
},
#[error("directive `@{name}` is not defined")]
UndefinedDirective {
name: String,
},
#[error("variable `{name}` is not defined")]
UndefinedVariable {
name: String,
},
#[error("fragment `{name}` is not defined")]
UndefinedFragment {
name: String,
},
#[error("value `{value}` does not exist on `{definition}` type")]
UndefinedValue {
value: String,
definition: String,
},
#[error("type extension for `{name}` is the wrong kind")]
WrongTypeExtension {
name: String,
definition: DiagnosticLocation,
extension: DiagnosticLocation,
},
#[error("directive definition `{name}` references itself")]
RecursiveDirectiveDefinition { name: String },
#[error("interface `{name}` implements itself")]
RecursiveInterfaceDefinition { name: String },
#[error("input object `{name}` references itself")]
RecursiveInputObjectDefinition { name: String },
#[error("fragment `{name}` references itself")]
RecursiveFragmentDefinition { name: String },
#[error("enum value `{value}` does not have a conventional all-caps name")]
CapitalizedValue { value: String },
#[error("field `{field}` is declared multiple times")]
UniqueField {
field: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("type `{name}` does not satisfy interface `{interface}` because it is missing field `{field}`")]
MissingField {
name: String,
interface: String,
field: String,
},
#[error("required argument `{coordinate}` is not provided")]
RequiredArgument { coordinate: String },
#[error("type `{ty}` implements interface `{interface}` multiple times")]
DuplicateImplementsInterface { ty: String, interface: String },
#[error("type `{ty}` must implement `{missing_interface}`")]
TransitiveImplementedInterfaces {
ty: String,
missing_interface: String,
},
#[error("`{name}` field does not return an output type")]
OutputType {
name: String,
ty: &'static str,
},
#[error("type `{ty}` for input field or argument `{name}` is not an input type")]
InputType {
name: String,
ty: String,
},
#[error("custom scalar `{ty}` does not have an @specifiedBy directive")]
ScalarSpecificationURL {
ty: String,
},
#[error("missing query root operation type in schema definition")]
QueryRootOperationType,
#[error("built-in scalars must be omitted for brevity")]
BuiltInScalarDefinition,
#[error("type `{ty}` for variable `${name}` is not an input type")]
VariableInputType {
name: String,
ty: String,
},
#[error("variable `{name}` is unused")]
UnusedVariable { name: String },
#[error("field `{name}` does not return an object type")]
ObjectType {
name: String,
ty: &'static str,
},
#[error("directive `@{name}` can not be used on {dir_loc}")]
UnsupportedDirectiveLocation {
name: String,
dir_loc: DirectiveLocation,
directive_def: DiagnosticLocation,
},
#[error("`{value}` cannot be assigned to type `{ty}`")]
UnsupportedValueType {
value: String,
ty: String,
},
#[error("int cannot represent non 32-bit signed integer value")]
IntCoercionError {
value: String,
},
#[error("non-repeatable directive {name} can only be used once per location")]
UniqueDirective {
name: String,
original_call: DiagnosticLocation,
conflicting_call: DiagnosticLocation,
},
#[error("subscription operation uses introspection field `{field}` as a root field")]
IntrospectionField {
field: String,
},
#[error("field `{field}` has a subselection but its type `{ty}` is not a composite type")]
DisallowedSubselection {
field: String,
ty: String,
},
#[error("field `{field}` selects a composite type `{ty}` but does not have a subselection")]
MissingSubselection {
field: String,
ty: String,
},
#[error("operation selects different types into the same field name `{field}`")]
ConflictingField {
field: String,
original_selection: DiagnosticLocation,
redefined_selection: DiagnosticLocation,
},
#[error("fragments must be specified on types that exist in the schema")]
InvalidFragment {
ty: Option<String>,
},
#[error("type condition `{ty}` is not a composite type")]
InvalidFragmentTarget {
ty: String,
},
#[error(
"{} cannot be applied to type `{type_name}`",
name.as_ref().map(|name| format!("fragment `{name}`"))
.unwrap_or_else(|| "anonymous fragment".to_string()),
)]
InvalidFragmentSpread {
name: Option<String>,
type_name: String,
},
#[error("fragment `{name}` is unused")]
UnusedFragment {
name: String,
},
#[error(
"variable `{var_name}` cannot be used for argument `{arg_name}` as their types mismatch"
)]
DisallowedVariableUsage {
var_name: String,
arg_name: String,
},
}
impl DiagnosticData {
pub fn is_error(&self) -> bool {
!self.is_warning() && !self.is_advice()
}
pub fn is_warning(&self) -> bool {
matches!(self, Self::CapitalizedValue { .. })
}
pub fn is_advice(&self) -> bool {
matches!(self, Self::ScalarSpecificationURL { .. })
}
}
type AriadneSpan = (FileId, std::ops::Range<usize>);
impl ApolloDiagnostic {
fn map_location(&self, location: DiagnosticLocation) -> AriadneSpan {
let id = location.file_id();
let source = self.cache.get_source(id).unwrap();
let start = source.map_index(location.offset);
let end = source.map_index(location.offset + location.length);
(id, start..end)
}
pub fn to_report(&self) -> ariadne::Report<'static, AriadneSpan> {
use ariadne::{ColorGenerator, Report, ReportKind};
let severity = if self.data.is_advice() {
ReportKind::Advice
} else if self.data.is_warning() {
ReportKind::Warning
} else {
ReportKind::Error
};
let span = self.map_location(self.location);
let mut colors = ColorGenerator::new();
let mut builder =
Report::build(severity, self.location.file_id(), span.1.start).with_message(&self.data);
builder.add_labels(self.labels.iter().map(|label| {
ariadne::Label::new(self.map_location(label.location))
.with_message(&label.text)
.with_color(colors.next())
}));
if let Some(help) = &self.help {
builder = builder.with_help(help);
}
builder.finish()
}
pub fn to_json(&self) -> GraphQLError {
let mut locations = vec![];
if let Some(location) = self.get_line_column() {
locations.push(location);
}
GraphQLError {
message: self.data.to_string(),
locations,
}
}
}