use crate::cst;
use crate::cst::CstNode;
use crate::SyntaxNode;
use crate::TokenText;
use rowan::GreenToken;
use rowan::SyntaxKind;
use std::num::ParseFloatError;
use std::num::ParseIntError;
impl cst::Name {
pub fn text(&self) -> TokenText {
text_of_first_token(self.syntax())
}
}
impl cst::Variable {
pub fn text(&self) -> TokenText {
self.name()
.expect("Cannot get variable's NAME token")
.text()
}
}
impl cst::EnumValue {
pub fn text(&self) -> TokenText {
self.name()
.expect("Cannot get enum value's NAME token")
.text()
}
}
impl cst::DirectiveLocation {
pub fn text(self) -> Option<TokenText> {
let txt = if self.query_token().is_some() {
Some("QUERY")
} else if self.mutation_token().is_some() {
Some("MUTATION")
} else if self.subscription_token().is_some() {
Some("SUBSCRIPTION")
} else if self.field_token().is_some() {
Some("FIELD")
} else if self.fragment_definition_token().is_some() {
Some("FRAGMENT_DEFINITION")
} else if self.fragment_spread_token().is_some() {
Some("FRAGMENT_SPREAD")
} else if self.inline_fragment_token().is_some() {
Some("INLINE_FRAGMENT")
} else if self.variable_definition_token().is_some() {
Some("VARIABLE_DEFINITION")
} else if self.schema_token().is_some() {
Some("SCHEMA")
} else if self.scalar_token().is_some() {
Some("SCALAR")
} else if self.object_token().is_some() {
Some("OBJECT")
} else if self.field_definition_token().is_some() {
Some("FIELD_DEFINITION")
} else if self.argument_definition_token().is_some() {
Some("ARGUMENT_DEFINITION")
} else if self.interface_token().is_some() {
Some("INTERFACE")
} else if self.union_token().is_some() {
Some("UNION")
} else if self.enum_token().is_some() {
Some("ENUM")
} else if self.enum_value_token().is_some() {
Some("ENUM_VALUE")
} else if self.input_object_token().is_some() {
Some("INPUT_OBJECT")
} else if self.input_field_definition_token().is_some() {
Some("INPUT_FIELD_DEFINITION")
} else {
None
};
txt.map(|txt| {
TokenText(GreenToken::new(
SyntaxKind(crate::SyntaxKind::DIRECTIVE_LOCATION as u16),
txt,
))
})
}
}
impl cst::Definition {
pub fn name(&self) -> Option<cst::Name> {
match self {
Self::OperationDefinition(it) => it.name(),
Self::FragmentDefinition(it) => it.fragment_name()?.name(),
Self::DirectiveDefinition(it) => it.name(),
Self::SchemaDefinition(_) => None,
Self::ScalarTypeDefinition(it) => it.name(),
Self::ObjectTypeDefinition(it) => it.name(),
Self::InterfaceTypeDefinition(it) => it.name(),
Self::UnionTypeDefinition(it) => it.name(),
Self::EnumTypeDefinition(it) => it.name(),
Self::InputObjectTypeDefinition(it) => it.name(),
Self::SchemaExtension(_) => None,
Self::ScalarTypeExtension(it) => it.name(),
Self::ObjectTypeExtension(it) => it.name(),
Self::InterfaceTypeExtension(it) => it.name(),
Self::UnionTypeExtension(it) => it.name(),
Self::EnumTypeExtension(it) => it.name(),
Self::InputObjectTypeExtension(it) => it.name(),
}
}
pub fn kind(&self) -> &'static str {
match self {
cst::Definition::OperationDefinition(_) => "OperationDefinition",
cst::Definition::FragmentDefinition(_) => "FragmentDefinition",
cst::Definition::DirectiveDefinition(_) => "DirectiveDefinition",
cst::Definition::ScalarTypeDefinition(_) => "ScalarTypeDefinition",
cst::Definition::ObjectTypeDefinition(_) => "ObjectTypeDefinition",
cst::Definition::InterfaceTypeDefinition(_) => "InterfaceTypeDefinition",
cst::Definition::UnionTypeDefinition(_) => "UnionTypeDefinition",
cst::Definition::EnumTypeDefinition(_) => "EnumTypeDefinition",
cst::Definition::InputObjectTypeDefinition(_) => "InputObjectTypeDefinition",
cst::Definition::SchemaDefinition(_) => "SchemaDefinition",
cst::Definition::SchemaExtension(_) => "SchemaExtension",
cst::Definition::ScalarTypeExtension(_) => "ScalarTypeExtension",
cst::Definition::ObjectTypeExtension(_) => "ObjectTypeExtension",
cst::Definition::InterfaceTypeExtension(_) => "InterfaceTypeExtension",
cst::Definition::UnionTypeExtension(_) => "UnionTypeExtension",
cst::Definition::EnumTypeExtension(_) => "EnumTypeExtension",
cst::Definition::InputObjectTypeExtension(_) => "InputObjectTypeExtension",
}
}
pub fn is_executable_definition(&self) -> bool {
matches!(
self,
Self::OperationDefinition(_) | Self::FragmentDefinition(_)
)
}
pub fn is_extension_definition(&self) -> bool {
matches!(
self,
Self::SchemaExtension(_)
| Self::ScalarTypeExtension(_)
| Self::ObjectTypeExtension(_)
| Self::InterfaceTypeExtension(_)
| Self::UnionTypeExtension(_)
| Self::EnumTypeExtension(_)
| Self::InputObjectTypeExtension(_)
)
}
}
impl From<cst::StringValue> for String {
fn from(val: cst::StringValue) -> Self {
Self::from(&val)
}
}
fn unescape_string(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let mut iter = input.chars();
while let Some(c) = iter.next() {
match c {
'\\' => {
let Some(c2) = iter.next() else {
output.push(c);
break;
};
let mut unicode = || {
let value = iter.by_ref().take(4).fold(0, |acc, c| {
let digit = c.to_digit(16).unwrap();
(acc << 4) + digit
});
char::from_u32(value).unwrap()
};
match c2 {
'"' | '\\' | '/' => output.push(c2),
'b' => output.push('\u{0008}'),
'f' => output.push('\u{000c}'),
'n' => output.push('\n'),
'r' => output.push('\r'),
't' => output.push('\t'),
'u' => output.push(unicode()),
_ => (),
}
}
_ => output.push(c),
}
}
output
}
const ESCAPED_TRIPLE_QUOTE: &str = r#"\""""#;
const TRIPLE_QUOTE: &str = r#"""""#;
fn is_block_string(input: &str) -> bool {
input.starts_with(TRIPLE_QUOTE)
}
struct GraphQLLines<'a> {
input: &'a str,
finished: bool,
}
impl<'a> GraphQLLines<'a> {
fn new(input: &'a str) -> Self {
Self {
input,
finished: false,
}
}
}
impl<'a> Iterator for GraphQLLines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.finished {
return None;
}
let Some(index) = memchr::memchr2(b'\r', b'\n', self.input.as_bytes()) else {
self.finished = true;
return Some(self.input);
};
let line = &self.input[..index];
let rest = match self.input.get(index..=index + 1) {
Some("\r\n") => &self.input[index + 2..],
_ => &self.input[index + 1..],
};
self.input = rest;
Some(line)
}
}
fn split_lines(input: &str) -> impl Iterator<Item = &str> {
GraphQLLines::new(input)
}
fn replace_into(input: &str, pattern: &str, replace: &str, output: &mut String) {
let mut last_index = 0;
for index in memchr::memmem::find_iter(input.as_bytes(), pattern.as_bytes()) {
output.push_str(&input[last_index..index]);
output.push_str(replace);
last_index = index + pattern.len();
}
if last_index < input.len() {
output.push_str(&input[last_index..]);
}
}
fn unescape_block_string(raw_value: &str) -> String {
fn is_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t')
}
fn is_whitespace_line(line: &str) -> bool {
line.chars().all(is_whitespace)
}
fn count_indent(line: &str) -> usize {
line.chars().take_while(|&c| is_whitespace(c)).count()
}
let common_indent = split_lines(raw_value)
.skip(1)
.filter_map(|line| {
let length = line.len();
let indent = count_indent(line);
(indent < length).then_some(indent)
})
.min()
.unwrap_or(0);
let mut lines = split_lines(raw_value)
.enumerate()
.map(|(index, line)| {
if index == 0 {
line
} else {
&line[common_indent.min(line.len())..]
}
})
.skip_while(|line| is_whitespace_line(line));
let mut formatted = String::with_capacity(raw_value.len());
if let Some(line) = lines.next() {
replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
};
let mut final_char_index = formatted.len();
for line in lines {
formatted.push('\n');
replace_into(line, ESCAPED_TRIPLE_QUOTE, TRIPLE_QUOTE, &mut formatted);
if !is_whitespace_line(line) {
final_char_index = formatted.len();
}
}
formatted.truncate(final_char_index);
formatted
}
impl From<&'_ cst::StringValue> for String {
fn from(val: &'_ cst::StringValue) -> Self {
let text = text_of_first_token(val.syntax());
if is_block_string(&text) {
unescape_block_string(&text[3..text.len() - 3])
} else {
unescape_string(&text[1..text.len() - 1])
}
}
}
impl TryFrom<cst::IntValue> for i32 {
type Error = ParseIntError;
fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
Self::try_from(&val)
}
}
impl TryFrom<&'_ cst::IntValue> for i32 {
type Error = ParseIntError;
fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
let text = text_of_first_token(val.syntax());
text.parse()
}
}
impl TryFrom<cst::IntValue> for f64 {
type Error = ParseFloatError;
fn try_from(val: cst::IntValue) -> Result<Self, Self::Error> {
Self::try_from(&val)
}
}
impl TryFrom<&'_ cst::IntValue> for f64 {
type Error = ParseFloatError;
fn try_from(val: &'_ cst::IntValue) -> Result<Self, Self::Error> {
let text = text_of_first_token(val.syntax());
text.parse()
}
}
impl TryFrom<cst::FloatValue> for f64 {
type Error = ParseFloatError;
fn try_from(val: cst::FloatValue) -> Result<Self, Self::Error> {
Self::try_from(&val)
}
}
impl TryFrom<&'_ cst::FloatValue> for f64 {
type Error = ParseFloatError;
fn try_from(val: &'_ cst::FloatValue) -> Result<Self, Self::Error> {
let text = text_of_first_token(val.syntax());
text.parse()
}
}
impl TryFrom<cst::BooleanValue> for bool {
type Error = std::str::ParseBoolError;
fn try_from(val: cst::BooleanValue) -> Result<Self, Self::Error> {
Self::try_from(&val)
}
}
impl TryFrom<&'_ cst::BooleanValue> for bool {
type Error = std::str::ParseBoolError;
fn try_from(val: &'_ cst::BooleanValue) -> Result<Self, Self::Error> {
let text = text_of_first_token(val.syntax());
text.parse()
}
}
fn text_of_first_token(node: &SyntaxNode) -> TokenText {
let first_token = node
.green()
.children()
.next()
.and_then(|it| it.into_token())
.unwrap()
.to_owned();
TokenText(first_token)
}
#[cfg(test)]
mod string_tests {
use super::unescape_string;
#[test]
fn it_parses_strings() {
assert_eq!(unescape_string(r"simple"), "simple");
assert_eq!(unescape_string(r" white space "), " white space ");
}
#[test]
fn it_unescapes_strings() {
assert_eq!(unescape_string(r#"quote \""#), "quote \"");
assert_eq!(
unescape_string(r"escaped \n\r\b\t\f"),
"escaped \n\r\u{0008}\t\u{000c}"
);
assert_eq!(unescape_string(r"slashes \\ \/"), r"slashes \ /");
assert_eq!(
unescape_string("unescaped unicode outside BMP 😀"),
"unescaped unicode outside BMP 😀"
);
assert_eq!(
unescape_string(r"unicode \u1234\u5678\u90AB\uCDEF"),
"unicode \u{1234}\u{5678}\u{90AB}\u{CDEF}"
);
}
}
#[cfg(test)]
mod block_string_tests {
use super::split_lines;
use super::unescape_block_string;
#[test]
fn it_splits_lines_by_graphql_newline_definition() {
let plain_newlines: Vec<_> = split_lines(
r#"source text
with some
new
lines
"#,
)
.collect();
assert_eq!(
plain_newlines,
[
"source text",
" with some",
" new",
"",
"",
" lines",
" ",
]
);
let different_endings: Vec<_> =
split_lines("with\nand\r\nand\rall in the same\r\nstring").collect();
assert_eq!(
different_endings,
["with", "and", "and", "all in the same", "string",]
);
let empty_string: Vec<_> = split_lines("").collect();
assert_eq!(empty_string, [""]);
let empty_line: Vec<_> = split_lines("\n\r\r\n").collect();
assert_eq!(empty_line, ["", "", "", ""]);
}
#[test]
fn it_normalizes_block_string_newlines() {
assert_eq!(unescape_block_string("multi\nline"), "multi\nline");
assert_eq!(unescape_block_string("multi\r\nline"), "multi\nline");
assert_eq!(unescape_block_string("multi\rline"), "multi\nline");
}
#[test]
fn it_does_not_unescape_block_strings() {
assert_eq!(
unescape_block_string(r"escaped \n\r\b\t\f"),
r"escaped \n\r\b\t\f"
);
assert_eq!(unescape_block_string(r"slashes \\ \/"), r"slashes \\ \/");
assert_eq!(
unescape_block_string("unescaped unicode outside BMP \u{1f600}"),
"unescaped unicode outside BMP \u{1f600}"
);
}
#[test]
fn it_dedents_block_strings() {
assert_eq!(
unescape_block_string(" intact whitespace with one line "),
" intact whitespace with one line "
);
assert_eq!(
unescape_block_string(
r"
This is
indented
quite a lot
"
),
r"This is
indented
quite a lot"
);
assert_eq!(
unescape_block_string(
r"
spans
multiple
lines
"
),
r"spans
multiple
lines"
);
}
}