1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
use crate::executable::{Cache, Error, Path, PathRoot, Rule, Visitor};
use bluejay_core::definition::{SchemaDefinition, TypeDefinitionReference};
use bluejay_core::executable::{
    ExecutableDocument, FragmentSpread, OperationDefinition, VariableDefinition,
};
use bluejay_core::{Argument, AsIter, ObjectValue, Value, ValueReference, Variable};
use std::collections::{HashMap, HashSet};
use std::ops::Not;

pub struct AllVariablesUsed<'a, E: ExecutableDocument, S: SchemaDefinition> {
    fragment_references: HashMap<PathRoot<'a, E>, HashSet<&'a E::FragmentDefinition>>,
    variable_usages: HashMap<PathRoot<'a, E>, HashSet<&'a str>>,
    cache: &'a Cache<'a, E, S>,
    executable_document: &'a E,
}

impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Visitor<'a, E, S>
    for AllVariablesUsed<'a, E, S>
{
    fn visit_variable_argument(
        &mut self,
        argument: &'a <E as ExecutableDocument>::Argument<false>,
        _: &'a <S as SchemaDefinition>::InputValueDefinition,
        path: &Path<'a, E>,
    ) {
        self.visit_value(argument.value(), *path.root());
    }

    fn visit_fragment_spread(
        &mut self,
        fragment_spread: &'a E::FragmentSpread,
        _: TypeDefinitionReference<'a, S::TypeDefinition>,
        path: &Path<'a, E>,
    ) {
        if let Some(fragment_definition) = self.cache.fragment_definition(fragment_spread.name()) {
            self.fragment_references
                .entry(*path.root())
                .or_default()
                .insert(fragment_definition);
        }
    }
}

impl<'a, E: ExecutableDocument, S: SchemaDefinition> AllVariablesUsed<'a, E, S> {
    fn visit_value(
        &mut self,
        value: &'a <E as ExecutableDocument>::Value<false>,
        root: PathRoot<'a, E>,
    ) {
        match value.as_ref() {
            ValueReference::Variable(v) => {
                self.variable_usages
                    .entry(root)
                    .or_default()
                    .insert(v.name());
            }
            ValueReference::List(l) => l.iter().for_each(|value| self.visit_value(value, root)),
            ValueReference::Object(o) => o
                .iter()
                .for_each(|(_, value)| self.visit_value(value, root)),
            _ => {}
        }
    }

    fn fragment_usages(
        &self,
        operation_definition: &'a E::OperationDefinition,
    ) -> impl Iterator<Item = &'a E::FragmentDefinition> {
        let mut references = HashSet::new();
        self.visit_fragment_references(&PathRoot::Operation(operation_definition), &mut references);
        references.into_iter()
    }

    fn visit_fragment_references(
        &self,
        executable_definition: &PathRoot<'a, E>,
        visited: &mut HashSet<&'a E::FragmentDefinition>,
    ) {
        if let Some(references) = self.fragment_references.get(executable_definition) {
            references.iter().for_each(|&reference| {
                if visited.insert(reference) {
                    self.visit_fragment_references(&PathRoot::Fragment(reference), visited);
                }
            });
        }
    }
}

impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> IntoIterator
    for AllVariablesUsed<'a, E, S>
{
    type Item = Error<'a, E, S>;
    type IntoIter = std::vec::IntoIter<Error<'a, E, S>>;

    fn into_iter(self) -> Self::IntoIter {
        self.executable_document
            .operation_definitions()
            .iter()
            .filter(|operation_definition| {
                operation_definition
                    .as_ref()
                    .variable_definitions()
                    .map_or(false, |variable_definitions| {
                        !variable_definitions.is_empty()
                    })
            })
            .flat_map(|operation_definition| {
                let variable_usages: HashSet<&'a str> = self
                    .fragment_usages(operation_definition)
                    .map(PathRoot::Fragment)
                    .chain(std::iter::once(PathRoot::Operation(operation_definition)))
                    .flat_map(|executable_definition| {
                        self.variable_usages
                            .get(&executable_definition)
                            .into_iter()
                            .flatten()
                            .copied()
                    })
                    .collect();

                operation_definition
                    .as_ref()
                    .variable_definitions()
                    .map(move |variable_definitions| {
                        variable_definitions
                            .iter()
                            .filter_map(move |variable_definition| {
                                variable_usages
                                    .contains(variable_definition.variable())
                                    .not()
                                    .then_some(Error::VariableDefinitionUnused {
                                        variable_definition,
                                    })
                            })
                    })
                    .into_iter()
                    .flatten()
            })
            .collect::<Vec<Error<'a, E, S>>>()
            .into_iter()
    }
}

impl<'a, E: ExecutableDocument + 'a, S: SchemaDefinition + 'a> Rule<'a, E, S>
    for AllVariablesUsed<'a, E, S>
{
    type Error = Error<'a, E, S>;

    fn new(executable_document: &'a E, _: &'a S, cache: &'a Cache<'a, E, S>) -> Self {
        Self {
            fragment_references: HashMap::new(),
            variable_usages: HashMap::new(),
            cache,
            executable_document,
        }
    }
}