2020-05-09 09:55:04 +00:00
|
|
|
use crate::parser::ast::{
|
2020-04-05 08:00:26 +00:00
|
|
|
Definition, Document, FragmentDefinition, FragmentSpread, OperationDefinition,
|
|
|
|
};
|
2020-05-09 09:55:04 +00:00
|
|
|
use crate::validation::utils::{operation_name, Scope};
|
|
|
|
use crate::validation::visitor::{Visitor, VisitorContext};
|
2020-05-10 02:59:51 +00:00
|
|
|
use crate::{Pos, Positioned};
|
2020-04-05 08:00:26 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2020-03-09 10:05:52 +00:00
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct NoUnusedFragments<'a> {
|
2020-04-05 08:00:26 +00:00
|
|
|
spreads: HashMap<Scope<'a>, Vec<&'a str>>,
|
|
|
|
defined_fragments: HashSet<(&'a str, Pos)>,
|
|
|
|
current_scope: Option<Scope<'a>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> NoUnusedFragments<'a> {
|
|
|
|
fn find_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
|
|
|
|
if let Scope::Fragment(name) = *from {
|
|
|
|
if result.contains(name) {
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
result.insert(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(spreads) = self.spreads.get(from) {
|
|
|
|
for spread in spreads {
|
|
|
|
self.find_reachable_fragments(&Scope::Fragment(spread), result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Visitor<'a> for NoUnusedFragments<'a> {
|
2020-03-22 08:45:59 +00:00
|
|
|
fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a Document) {
|
2020-04-05 08:00:26 +00:00
|
|
|
let mut reachable = HashSet::new();
|
|
|
|
|
2020-05-12 08:27:06 +00:00
|
|
|
for def in doc.definitions() {
|
2020-05-09 09:55:04 +00:00
|
|
|
if let Definition::Operation(operation_definition) = &def.node {
|
2020-04-05 08:00:26 +00:00
|
|
|
let (name, _) = operation_name(operation_definition);
|
|
|
|
self.find_reachable_fragments(&Scope::Operation(name), &mut reachable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (fragment_name, pos) in &self.defined_fragments {
|
|
|
|
if !reachable.contains(fragment_name) {
|
|
|
|
ctx.report_error(
|
|
|
|
vec![*pos],
|
|
|
|
format!(r#"Fragment "{}" is never used"#, fragment_name),
|
|
|
|
);
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-05 08:00:26 +00:00
|
|
|
fn enter_operation_definition(
|
|
|
|
&mut self,
|
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-05-10 02:59:51 +00:00
|
|
|
operation_definition: &'a Positioned<OperationDefinition>,
|
2020-04-05 08:00:26 +00:00
|
|
|
) {
|
|
|
|
let (op_name, _) = operation_name(operation_definition);
|
|
|
|
self.current_scope = Some(Scope::Operation(op_name));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn enter_fragment_definition(
|
|
|
|
&mut self,
|
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-05-10 02:59:51 +00:00
|
|
|
fragment_definition: &'a Positioned<FragmentDefinition>,
|
2020-04-05 08:00:26 +00:00
|
|
|
) {
|
2020-05-12 08:27:06 +00:00
|
|
|
self.current_scope = Some(Scope::Fragment(fragment_definition.name.node));
|
2020-04-05 08:00:26 +00:00
|
|
|
self.defined_fragments.insert((
|
2020-05-12 08:27:06 +00:00
|
|
|
fragment_definition.name.node,
|
2020-05-09 09:55:04 +00:00
|
|
|
fragment_definition.position(),
|
2020-04-05 08:00:26 +00:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2020-03-09 10:05:52 +00:00
|
|
|
fn enter_fragment_spread(
|
|
|
|
&mut self,
|
2020-03-22 08:45:59 +00:00
|
|
|
_ctx: &mut VisitorContext<'a>,
|
2020-05-10 02:59:51 +00:00
|
|
|
fragment_spread: &'a Positioned<FragmentSpread>,
|
2020-03-09 10:05:52 +00:00
|
|
|
) {
|
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-05-12 08:27:06 +00:00
|
|
|
.push(fragment_spread.fragment_name.node);
|
2020-04-05 08:00:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::validation::test_harness::{expect_fails_rule, expect_passes_rule};
|
|
|
|
|
|
|
|
pub fn factory<'a>() -> NoUnusedFragments<'a> {
|
|
|
|
NoUnusedFragments::default()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_fragment_names_are_used() {
|
|
|
|
expect_passes_rule(
|
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
{
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields1
|
|
|
|
... on Human {
|
|
|
|
...HumanFields2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment HumanFields1 on Human {
|
|
|
|
name
|
|
|
|
...HumanFields3
|
|
|
|
}
|
|
|
|
fragment HumanFields2 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment HumanFields3 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn all_fragment_names_are_used_by_multiple_operations() {
|
|
|
|
expect_passes_rule(
|
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
query Bar {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment HumanFields1 on Human {
|
|
|
|
name
|
|
|
|
...HumanFields3
|
|
|
|
}
|
|
|
|
fragment HumanFields2 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment HumanFields3 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn contains_unknown_fragments() {
|
|
|
|
expect_fails_rule(
|
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
query Bar {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment HumanFields1 on Human {
|
|
|
|
name
|
|
|
|
...HumanFields3
|
|
|
|
}
|
|
|
|
fragment HumanFields2 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment HumanFields3 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment Unused1 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment Unused2 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn contains_unknown_fragments_with_ref_cycle() {
|
|
|
|
expect_fails_rule(
|
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
query Bar {
|
|
|
|
human(id: 4) {
|
|
|
|
...HumanFields2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment HumanFields1 on Human {
|
|
|
|
name
|
|
|
|
...HumanFields3
|
|
|
|
}
|
|
|
|
fragment HumanFields2 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment HumanFields3 on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
fragment Unused1 on Human {
|
|
|
|
name
|
|
|
|
...Unused2
|
|
|
|
}
|
|
|
|
fragment Unused2 on Human {
|
|
|
|
name
|
|
|
|
...Unused1
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn contains_unknown_and_undef_fragments() {
|
|
|
|
expect_fails_rule(
|
|
|
|
factory,
|
|
|
|
r#"
|
|
|
|
query Foo {
|
|
|
|
human(id: 4) {
|
|
|
|
...bar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fragment foo on Human {
|
|
|
|
name
|
|
|
|
}
|
|
|
|
"#,
|
|
|
|
);
|
2020-03-09 10:05:52 +00:00
|
|
|
}
|
|
|
|
}
|