use apollo_compiler::{parser::LineColumn, validation::DiagnosticList};
use apollo_federation_types::composition::Issue;
use std::fmt;
use std::ops::Range;
const SYNTAX_CODE: &str = "SYNTAX";
const BUILD_CODE: &str = "BUILD";
const VALIDATION_CODE: &str = "VALIDATION";
fn map_diagnostic_list_to_lsp_diagnostics(
maybe_diagnostic_list: Option<&DiagnosticList>,
code: String,
severity: lsp::DiagnosticSeverity,
) -> Vec<lsp::Diagnostic> {
maybe_diagnostic_list
.map(|diagnostic_list| {
diagnostic_list
.iter()
.map(|e| {
let range = e
.line_column_range()
.map(lsp_range_from_line_column_range)
.unwrap_or_else(|| {
tracing::error!("No line column range for diagnostic: {:?}", e);
lsp::Range {
start: lsp::Position {
line: 0,
character: 0,
},
end: lsp::Position {
line: 0,
character: 1,
},
}
});
lsp::Diagnostic {
range,
message: e.error.to_string(),
code: Some(lsp::NumberOrString::String(code.clone())),
severity: Some(severity),
..Default::default()
}
})
.collect()
})
.unwrap_or_default()
}
pub fn map_diagnostics_for_lsp(
parse_errors: Option<&DiagnosticList>,
build_errors: Option<&DiagnosticList>,
validation_errors: Option<&DiagnosticList>,
document: String,
) -> LspDiagnostics {
let doc_diagnostics = map_diagnostic_list_to_lsp_diagnostics(
parse_errors,
SYNTAX_CODE.to_string(),
lsp::DiagnosticSeverity::ERROR,
);
let build_diagnostics = map_diagnostic_list_to_lsp_diagnostics(
build_errors,
BUILD_CODE.to_string(),
lsp::DiagnosticSeverity::ERROR,
);
let validation_diagnostics = map_diagnostic_list_to_lsp_diagnostics(
validation_errors,
VALIDATION_CODE.to_string(),
lsp::DiagnosticSeverity::ERROR,
);
LspDiagnostics {
diagnostics: [doc_diagnostics, build_diagnostics, validation_diagnostics]
.into_iter()
.flatten()
.collect(),
document,
}
}
pub(crate) fn lsp_range_from_line_column_range(line_column: Range<LineColumn>) -> lsp::Range {
lsp::Range {
start: lsp::Position {
line: line_column.start.line as u32 - 1,
character: line_column.start.column as u32 - 1,
},
end: lsp::Position {
line: line_column.end.line as u32 - 1,
character: line_column.end.column as u32 - 1,
},
}
}
pub fn reformat_satisfiability_error(issue: &Issue) -> String {
let query_start = issue.message.find('{').unwrap();
let query_end = issue.message.rfind('}').unwrap();
let first_part = &issue.message[..query_start];
let query_part = &issue.message[query_start..query_end + 1];
let last_part = &issue.message[query_end + 1..];
let query_part = query_part.replace(" ", "\u{2003}\u{2003}");
format!("{first_part}{query_part}{last_part}")
}
#[derive(Default, Clone)]
pub struct LspDiagnostics {
diagnostics: Vec<lsp::Diagnostic>,
document: String,
}
impl LspDiagnostics {
pub fn new(diagnostics: Vec<lsp::Diagnostic>, document: String) -> Self {
Self {
diagnostics,
document,
}
}
pub fn push(&mut self, diagnostic: lsp::Diagnostic) {
self.diagnostics.push(diagnostic);
}
pub fn len(&self) -> usize {
self.diagnostics.len()
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
}
impl IntoIterator for LspDiagnostics {
type Item = lsp::Diagnostic;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.into_iter()
}
}
impl From<LspDiagnostics> for Vec<lsp::Diagnostic> {
fn from(diagnostics: LspDiagnostics) -> Vec<lsp::Diagnostic> {
diagnostics.diagnostics
}
}
impl fmt::Debug for LspDiagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut display = String::new();
let document = if self.document.ends_with('\n') {
format!("{} ", self.document)
} else {
self.document.clone()
};
for (line_index, line) in document.lines().enumerate() {
let line_length = line.len();
let relevant_diagnostics = self
.diagnostics
.iter()
.filter(|d| {
let start_line = d.range.start.line as usize;
let end_line = d.range.end.line as usize;
line_index >= start_line && line_index <= end_line
})
.collect::<Vec<_>>();
if relevant_diagnostics.is_empty() {
display.push_str(line);
display.push('\n');
continue;
}
for (character_index, character) in line.chars().enumerate() {
display.push(character);
for diagnostic in &relevant_diagnostics {
let start_line = diagnostic.range.start.line as usize;
let start_character = diagnostic.range.start.character as usize;
let end_line = diagnostic.range.end.line as usize;
let end_character = diagnostic.range.end.character as usize;
if line_index == start_line && line_index == end_line {
if (character_index >= start_character && character_index < end_character)
|| (character_index == start_character
&& character_index == end_character)
{
display.push('\u{0330}');
break;
} else if character_index == line_length - 1
&& start_character >= line_length
{
display.push_str(" \u{0330}");
break;
}
} else {
todo!("multi-line diagnostics not yet supported");
}
}
}
display.push('\n');
}
write!(f, "{}", display)
}
}
#[cfg(test)]
mod tests {
use crate::{diagnostics::reformat_satisfiability_error, testing::*};
use apollo_federation_types::composition::{Issue, Severity};
use insta::assert_snapshot;
fn get_base_text() -> Option<String> {
Some("type Query {\n hello: String\n}\n\n".to_string())
}
#[test]
fn expected_definition() {
let expected_errors = ["syntax error: expected definition"];
let source_texts = ["typ", "typ ", "typ\n"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn expected_type_name() {
let expected_errors = ["syntax error: expected a name"];
let source_texts = ["type", "type ", "type\n"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn expected_field_definition() {
let expected_errors = [
"syntax error: expected Field Definition",
"`Query` has no fields",
];
let source_texts = ["type Query {}", "type Query { }"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
None,
));
}
#[test]
fn expected_rcurly() {
let expected_errors = ["syntax error: expected R_CURLY, got EOF"];
let source_texts = [
"type Query {\n field: String",
"type Query {\n field: String ",
"type Query {\n field: String\n",
];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
None,
));
}
#[test]
fn multiple_field_definitions() {
let expected_errors = ["duplicate definitions for the `field` field of object type `A`"];
let source_texts = ["type A {\n field: String!\n field: String!\n}"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn missing_root_operation_type_definition() {
let expected_errors = [
"syntax error: expected Root Operation Type Definition",
"missing query root operation type in schema definition",
];
let source_texts = ["schema {}", "schema { }"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
None,
));
}
#[test]
fn missing_root_operation_type_or_directive() {
let expected_errors =
["syntax error: expected directives or Root Operation Type Definition"];
let source_texts = ["extend schema", "extend schema {}"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn missing_directive_name() {
let expected_errors = ["syntax error: expected a Name"];
let source_texts = ["type TypeName @{ field: String }"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn multiple_diagnostics() {
let expected_errors = [
"syntax error: expected Field Definition",
"syntax error: expected Field Definition",
"`A` has no fields",
"`B` has no fields",
];
let source_texts = ["type A {}\ntype B {}"];
assert_snapshot!(collect_diagnostic_comparisons(
&expected_errors,
&source_texts,
get_base_text()
));
}
#[test]
fn federation_simple() {
let subgraph = r#"
schema @link(
url: "https://specs.apollo.dev/federation/v2.7",
import: ["@key", "@override"]
) {
query: Query
}
type Query {
a: A
}
type A @key(fields: "a") {
a: ID @override(from: "abc")
b: String!
}
"#;
assert!(get_diagnostics(subgraph).is_none());
}
#[test]
fn test_reformat_satisfiability_error() {
let issue = Issue {
message: r#"The following supergraph API query:
{
order(id: "<any id>") {
items {
coffee {
temperature
}
}
}
}
cannot be satisfied by the subgraphs because:
- from subgraph "file:///packages/orders/src/schema.graphql":
- cannot find field "Coffee.temperature".
- cannot move to subgraph "file:///connectors/coffees.graphql", which has field "Coffee.temperature", because type "Coffee" has no @key defined in subgraph "file:///connectors/coffees.graphql".
- cannot move to subgraph "file:///connectors/coffees.graphql", which has field "Coffee.temperature", because type "Coffee" has no @key defined in subgraph "file:///connectors/coffees.graphql".
- cannot move to subgraph "file:///connectors/coffees.graphql", which has field "Coffee.temperature", because type "Coffee" has no @key defined in subgraph "file:///connectors/coffees.graphql"."#.to_string(),
code: "SATISFIABILITY".to_string(),
severity: Severity::Error,
locations: vec![],
};
assert_snapshot!(reformat_satisfiability_error(&issue));
}
}