2020-10-15 06:38:10 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
|
2020-09-06 05:38:31 +00:00
|
|
|
use crate::parser::types::{
|
2020-10-11 12:24:31 +00:00
|
|
|
ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition,
|
2020-04-05 08:00:26 +00:00
|
|
|
};
|
2020-09-06 05:38:31 +00:00
|
|
|
use crate::validation::utils::{referenced_variables, Scope};
|
2020-05-09 09:55:04 +00:00
|
|
|
use crate::validation::visitor::{Visitor, VisitorContext};
|
2020-10-11 12:24:31 +00:00
|
|
|
use crate::{Name, Pos, Positioned};
|
|
|
|
use async_graphql_value::Value;
|
2020-03-09 10:05:52 +00:00
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct NoUndefinedVariables<'a> {
|
2020-04-05 08:00:26 +00:00
|
|
|
defined_variables: HashMap<Option<&'a str>, (Pos, HashSet<&'a str>)>,
|
|
|
|
used_variables: HashMap<Scope<'a>, HashMap<&'a str, Pos>>,
|
|
|
|
current_scope: Option<Scope<'a>>,
|
|
|
|
spreads: HashMap<Scope<'a>, Vec<&'a str>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> NoUndefinedVariables<'a> {
|
|
|
|
fn find_undef_vars(
|
|
|
|
&'a self,
|
|
|
|
scope: &Scope<'a>,
|
|
|
|
defined: &HashSet<&'a str>,
|
|
|
|
unused: &mut Vec<(&'a str, Pos)>,
|
|
|
|
visited: &mut HashSet<Scope<'a>>,
|
|
|
|
) {
|
|
|
|
if visited.contains(scope) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
visited.insert(scope.clone());
|
|
|
|
|
|
|
|
if let Some(used_vars) = self.used_variables.get(scope) {
|
|
|
|
for (var, pos) in used_vars {
|
|
|
|
if !defined.contains(var) {
|
|
|
|
unused.push((*var, *pos));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(spreads) = self.spreads.get(scope) {
|
|
|
|
for spread in spreads {
|
|
|
|
self.find_undef_vars(&Scope::Fragment(spread), defined, unused, visited);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Visitor<'a> for NoUndefinedVariables<'a> {
|
2020-09-08 08:21:27 +00:00
|
|
|
fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) {
|
2020-04-05 08:00:26 +00:00
|
|
|
for (op_name, &(ref def_pos, ref def_vars)) in &self.defined_variables {
|
|
|
|
let mut unused = Vec::new();
|
|
|
|
let mut visited = HashSet::new();
|
|
|
|
self.find_undef_vars(
|
|
|
|
&Scope::Operation(*op_name),
|
|
|
|
def_vars,
|
|
|
|
&mut unused,
|
|
|
|
&mut visited,
|
|
|
|
);
|
|
|
|
|
|
|
|
for (var, pos) in unused {
|
|
|
|
if let Some(op_name) = op_name {
|
|
|
|
ctx.report_error(
|
|
|
|
vec![*def_pos, pos],
|
|
|
|
format!(
|
|
|
|
r#"Variable "${}" is not defined by operation "{}""#,
|
|
|
|
var, op_name
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
ctx.report_error(vec![pos], format!(r#"Variable "${}" is not defined"#, var));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-09 10:05:52 +00:00
|
|
|
fn enter_operation_definition(
|
|
|
|
&mut self,
|
2020-03-22 08:45:59 +00:00
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-09-22 18:59:48 +00:00
|
|
|
name: Option<&'a Name>,
|
2020-05-10 02:59:51 +00:00
|
|
|
operation_definition: &'a Positioned<OperationDefinition>,
|
2020-04-05 08:00:26 +00:00
|
|
|
) {
|
2020-09-22 18:59:48 +00:00
|
|
|
let name = name.map(|name| name.as_str());
|
2020-09-06 05:38:31 +00:00
|
|
|
self.current_scope = Some(Scope::Operation(name));
|
2020-04-05 08:00:26 +00:00
|
|
|
self.defined_variables
|
2020-09-06 05:38:31 +00:00
|
|
|
.insert(name, (operation_definition.pos, HashSet::new()));
|
2020-04-05 08:00:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn enter_fragment_definition(
|
|
|
|
&mut self,
|
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-09-22 18:59:48 +00:00
|
|
|
name: &'a Name,
|
|
|
|
_fragment_definition: &'a Positioned<FragmentDefinition>,
|
2020-03-09 10:05:52 +00:00
|
|
|
) {
|
2020-09-22 18:59:48 +00:00
|
|
|
self.current_scope = Some(Scope::Fragment(name));
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn enter_variable_definition(
|
|
|
|
&mut self,
|
2020-03-22 08:45:59 +00:00
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-05-10 02:59:51 +00:00
|
|
|
variable_definition: &'a Positioned<VariableDefinition>,
|
2020-03-09 10:05:52 +00:00
|
|
|
) {
|
2020-04-05 08:00:26 +00:00
|
|
|
if let Some(Scope::Operation(ref name)) = self.current_scope {
|
|
|
|
if let Some(&mut (_, ref mut vars)) = self.defined_variables.get_mut(name) {
|
2020-09-06 05:38:31 +00:00
|
|
|
vars.insert(&variable_definition.node.name.node);
|
2020-04-05 08:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
|
2020-03-09 12:39:46 +00:00
|
|
|
fn enter_argument(
|
|
|
|
&mut self,
|
2020-04-05 08:00:26 +00:00
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-09-08 08:21:27 +00:00
|
|
|
name: &'a Positioned<Name>,
|
2020-05-10 02:59:51 +00:00
|
|
|
value: &'a Positioned<Value>,
|
2020-03-09 12:39:46 +00:00
|
|
|
) {
|
2020-04-05 08:00:26 +00:00
|
|
|
if let Some(ref scope) = self.current_scope {
|
|
|
|
self.used_variables
|
|
|
|
.entry(scope.clone())
|
|
|
|
.or_insert_with(HashMap::new)
|
|
|
|
.extend(
|
2020-09-06 05:38:31 +00:00
|
|
|
referenced_variables(&value.node)
|
2020-04-05 08:00:26 +00:00
|
|
|
.into_iter()
|
2020-09-06 05:38:31 +00:00
|
|
|
.map(|n| (n, name.pos)),
|
2020-03-09 10:05:52 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-04-05 08:00:26 +00:00
|
|
|
|
|
|
|
fn enter_fragment_spread(
|
|
|
|
&mut self,
|
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-05-10 02:59:51 +00:00
|
|
|
fragment_spread: &'a Positioned<FragmentSpread>,
|
2020-04-05 08:00:26 +00:00
|
|
|
) {
|
|
|
|
if let Some(ref scope) = self.current_scope {
|
|
|
|
self.spreads
|
|
|
|
.entry(scope.clone())
|
|
|
|
.or_insert_with(Vec::new)
|
2020-09-06 05:38:31 +00:00
|
|
|
.push(&fragment_spread.node.fragment_name.node);
|
2020-04-05 08:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
pub fn factory<'a>() -> NoUndefinedVariables<'a> {
|
|
|
|
NoUndefinedVariables::default()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_variables_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String, $c: String) {
|
|
|
|
field(a: $a, b: $b, c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_variables_deeply_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String, $c: String) {
|
|
|
|
field(a: $a) {
|
|
|
|
field(b: $b) {
|
|
|
|
field(c: $c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_variables_deeply_defined_in_inline_fragments_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String, $c: String) {
|
|
|
|
... on Type {
|
|
|
|
field(a: $a) {
|
|
|
|
field(b: $b) {
|
|
|
|
... on Type {
|
|
|
|
field(c: $c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_variables_in_fragments_deeply_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String, $c: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a) {
|
|
|
|
...FragB
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragB on Type {
|
|
|
|
field(b: $b) {
|
|
|
|
...FragC
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragC on Type {
|
|
|
|
field(c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_within_single_fragment_defined_in_multiple_operations() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
query Bar($a: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_within_fragments_defined_in_operations() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
query Bar($b: String) {
|
|
|
|
...FragB
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a)
|
|
|
|
}
|
|
|
|
fragment FragB on Type {
|
|
|
|
field(b: $b)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_within_recursive_fragment_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_passes_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_not_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String, $c: String) {
|
|
|
|
field(a: $a, b: $b, c: $c, d: $d)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_not_defined_by_unnamed_query() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
{
|
|
|
|
field(a: $a)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn multiple_variables_not_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($b: String) {
|
|
|
|
field(a: $a, b: $b, c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_in_fragment_not_defined_by_unnamed_query() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
{
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_in_fragment_not_defined_by_operation() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String, $b: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a) {
|
|
|
|
...FragB
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragB on Type {
|
|
|
|
field(b: $b) {
|
|
|
|
...FragC
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragC on Type {
|
|
|
|
field(c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn multiple_variables_in_fragments_not_defined() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($b: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a) {
|
|
|
|
...FragB
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragB on Type {
|
|
|
|
field(b: $b) {
|
|
|
|
...FragC
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment FragC on Type {
|
|
|
|
field(c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn single_variable_in_fragment_not_defined_by_multiple_operations() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($a: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
query Bar($a: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
fragment FragAB on Type {
|
|
|
|
field(a: $a, b: $b)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variables_in_fragment_not_defined_by_multiple_operations() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($b: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
query Bar($a: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
fragment FragAB on Type {
|
|
|
|
field(a: $a, b: $b)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn variable_in_fragment_used_by_other_operation() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($b: String) {
|
|
|
|
...FragA
|
|
|
|
}
|
|
|
|
query Bar($a: String) {
|
|
|
|
...FragB
|
|
|
|
}
|
|
|
|
fragment FragA on Type {
|
|
|
|
field(a: $a)
|
|
|
|
}
|
|
|
|
fragment FragB on Type {
|
|
|
|
field(b: $b)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn multiple_undefined_variables_produce_multiple_errors() {
|
2020-05-29 09:29:15 +00:00
|
|
|
expect_fails_rule!(
|
2020-04-05 08:00:26 +00:00
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo($b: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
query Bar($a: String) {
|
|
|
|
...FragAB
|
|
|
|
}
|
|
|
|
fragment FragAB on Type {
|
|
|
|
field1(a: $a, b: $b)
|
|
|
|
...FragC
|
|
|
|
field3(a: $a, b: $b)
|
|
|
|
}
|
|
|
|
fragment FragC on Type {
|
|
|
|
field2(c: $c)
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|