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, Hash, PartialEq, Eq)]
pub struct DiagnosticLocation {
file_id: FileId,
offset: usize,
length: usize,
}
impl ariadne::Span for DiagnosticLocation {
type SourceId = FileId;
fn source(&self) -> &FileId {
&self.file_id
}
fn start(&self) -> usize {
self.offset
}
fn end(&self) -> usize {
self.offset + self.length
}
}
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
}
}
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 must not contain {kind}")]
ExecutableDefinition { kind: &'static str },
#[error("the {ty} `{name}` is defined multiple times in the document")]
UniqueDefinition {
ty: &'static str,
name: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("the argument `{name}` is defined multiple times")]
UniqueArgument {
name: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("the value `{name}` is defined multiple times")]
UniqueInputValue {
name: String,
original_value: DiagnosticLocation,
redefined_value: DiagnosticLocation,
},
#[error("subscription operations can only have one root field")]
SingleRootField {
fields: usize,
subscription: DiagnosticLocation,
},
#[error("{ty} root operation type 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")]
UndefinedArgument { name: String },
#[error("cannot find type `{name}` in this document")]
UndefinedDefinition {
name: String,
},
#[error("cannot find directive `{name}` in this document")]
UndefinedDirective {
name: String,
},
#[error("variable `{name}` is not defined")]
UndefinedVariable {
name: String,
},
#[error("cannot find fragment `{name}` in this document")]
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("`{name}` directive definition cannot reference itself")]
RecursiveDirectiveDefinition { name: String },
#[error("interface {name} cannot implement itself")]
RecursiveInterfaceDefinition { name: String },
#[error("`{name}` input object cannot reference itself")]
RecursiveInputObjectDefinition { name: String },
#[error("`{name}` fragment cannot reference itself")]
RecursiveFragmentDefinition { name: String },
#[error("values in an Enum Definition should be capitalized")]
CapitalizedValue { value: String },
#[error("fields must be unique in a definition")]
UniqueField {
field: String,
original_definition: DiagnosticLocation,
redefined_definition: DiagnosticLocation,
},
#[error("missing `{field}` field")]
MissingField {
field: String,
},
#[error("the required argument `{name}` is not provided")]
RequiredArgument { name: String },
#[error("type `{ty}` can only implement interface `{interface}` once")]
DuplicateImplementsInterface { ty: String, interface: String },
#[error(
"Transitively implemented interfaces must also be defined on an implementing interface or object"
)]
TransitiveImplementedInterfaces {
missing_interface: String,
},
#[error("`{name}` field must return an output type")]
OutputType {
name: String,
ty: &'static str,
},
#[error("`${name}` variable must be of an input type")]
InputType {
name: String,
ty: &'static str,
},
#[error(
"custom scalars should provide a scalar specification URL via the @specifiedBy directive"
)]
ScalarSpecificationURL,
#[error("missing query root operation type in schema definition")]
QueryRootOperationType,
#[error("built-in scalars must be omitted for brevity")]
BuiltInScalarDefinition,
#[error("unused variable: `{name}`")]
UnusedVariable { name: String },
#[error("`{name}` field must return an object type")]
ObjectType {
name: String,
ty: &'static str,
},
#[error("{name} directive is not supported for {dir_loc} location")]
UnsupportedLocation {
name: String,
dir_loc: DirectiveLocation,
directive_def: DiagnosticLocation,
},
#[error("{ty} cannot be represented by a {value} value")]
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 operations can not have an introspection field as a root field")]
IntrospectionField {
field: String,
},
#[error("subselection set for scalar and enum types must be empty")]
DisallowedSubselection,
#[error("interface, union and object types must have a subselection set")]
MissingSubselection,
#[error("operation must not select different types using 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("fragments can not be declared on primitive types")]
InvalidFragmentTarget {
ty: String,
},
#[error("fragment cannot be applied to this type")]
InvalidFragmentSpread {
name: Option<String>,
type_name: String,
},
#[error("fragment `{name}` must be used in an operation")]
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)
}
}
impl From<Label> for ariadne::Label<DiagnosticLocation> {
fn from(label: Label) -> Self {
Self::new(label.location).with_message(label.text)
}
}
impl ApolloDiagnostic {
pub fn to_report(&self) -> ariadne::Report<'static, DiagnosticLocation> {
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 mut colors = ColorGenerator::new();
let mut builder = Report::build(severity, self.location.file_id(), self.location.offset())
.with_message(&self.data);
builder.add_labels(
self.labels
.iter()
.map(|label| ariadne::Label::from(label.clone()).with_color(colors.next())),
);
if let Some(help) = &self.help {
builder = builder.with_help(help);
}
builder.finish()
}
}