rusty_gql/validation/rules/
no_unused_fragments.rs1use std::collections::{HashMap, HashSet};
2
3use graphql_parser::{
4 query::{Definition, FragmentDefinition, FragmentSpread, OperationDefinition},
5 Pos,
6};
7
8use crate::validation::{
9 utils::Scope,
10 visitor::{ValidationContext, Visitor},
11};
12
13#[derive(Default)]
14pub struct NoUnusedFragment<'a> {
15 current_scope: Option<Scope<'a>>,
16 fragment_spreads: HashMap<Scope<'a>, Vec<&'a str>>,
17 fragment_definitions: HashSet<(&'a str, Pos)>,
18}
19
20impl<'a> NoUnusedFragment<'a> {
21 fn get_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
22 if let Scope::Fragment(name) = from {
23 if result.contains(name) {
24 return;
25 } else {
26 result.insert(name);
27 }
28 }
29
30 if let Some(spreads) = self.fragment_spreads.get(from) {
31 for spread in spreads {
32 self.get_reachable_fragments(&Scope::Fragment(spread), result)
33 }
34 }
35 }
36}
37
38impl<'a> Visitor<'a> for NoUnusedFragment<'a> {
39 fn exit_document(
40 &mut self,
41 ctx: &mut ValidationContext<'a>,
42 doc: &'a graphql_parser::query::Document<'a, String>,
43 ) {
44 let mut reachable = HashSet::new();
45
46 for definition in &doc.definitions {
47 if let Definition::Operation(_) = definition {
48 self.get_reachable_fragments(&Scope::Operation(None), &mut reachable)
49 }
50 }
51
52 for (name, pos) in &self.fragment_definitions {
53 if !reachable.contains(name) {
54 ctx.add_error(format!("{} is unused fragment.", name), vec![*pos])
55 }
56 }
57 }
58
59 fn enter_operation_definition(
60 &mut self,
61 _ctx: &mut ValidationContext<'a>,
62 name: Option<&'a str>,
63 _operation_definition: &'a OperationDefinition<'a, String>,
64 ) {
65 self.current_scope = Some(Scope::Operation(name));
66 }
67
68 fn enter_fragment_definition(
69 &mut self,
70 _ctx: &mut ValidationContext,
71 name: &'a str,
72 fragment_definition: &'a FragmentDefinition<'a, String>,
73 ) {
74 self.current_scope = Some(Scope::Fragment(name));
75 self.fragment_definitions
76 .insert((name, fragment_definition.position));
77 }
78
79 fn enter_fragment_spread(
80 &mut self,
81 _ctx: &mut ValidationContext,
82 fragment_spread: &'a FragmentSpread<'a, String>,
83 ) {
84 if let Some(scope) = &self.current_scope {
85 self.fragment_spreads
86 .entry(*scope)
87 .or_insert_with(Vec::new)
88 .push(&fragment_spread.fragment_name)
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use crate::{check_fails_rule, check_passes_rule};
96
97 use super::*;
98
99 fn factory<'a>() -> NoUnusedFragment<'a> {
100 NoUnusedFragment::default()
101 }
102
103 #[test]
104 fn all_fragment_used() {
105 let query_doc = r#"
106 {
107 hero {
108 ...Frag1
109 ... on Human {
110 ...Frag2
111 }
112
113 }
114 }
115 fragment Frag1 on Human {
116 name
117 ...Frag3
118 }
119 fragment Frag2 on Human {
120 name
121 }
122 fragment Frag3 on Human {
123 name
124 }
125 "#;
126 check_passes_rule!(query_doc, factory);
127 }
128
129 #[test]
130 fn with_unused_fragment() {
131 let query_doc = r#"
132 {
133 hero {
134 ...Frag1
135 ... on Human {
136 ...Frag2
137 }
138
139 }
140 }
141 fragment Frag1 on Human {
142 name
143 ...Frag3
144 }
145 fragment Frag2 on Human {
146 name
147 }
148 fragment Frag3 on Human {
149 name
150 }
151 fragment UnusedFrag1 on Human {
152 name
153 }
154 "#;
155 check_fails_rule!(query_doc, factory);
156 }
157
158 #[test]
159 fn with_unused_fragment_ref_cycle() {
160 let query_doc = r#"
161 {
162 hero {
163 ...Frag1
164 ... on Human {
165 ...Frag2
166 }
167
168 }
169 }
170 fragment Frag1 on Human {
171 name
172 ...Frag3
173 }
174 fragment Frag2 on Human {
175 name
176 }
177 fragment Frag3 on Human {
178 name
179 }
180 fragment UnusedFrag1 on Human {
181 name
182 ...UnusedFrag2
183 }
184 fragment UnusedFrag2 on Human {
185 name
186 ...UnusedFrag1
187 }
188 "#;
189 check_fails_rule!(query_doc, factory);
190 }
191
192 #[test]
193 fn with_unknown_and_unused_fragments() {
194 let query_doc = r#"
195 {
196 hero {
197 ...Frag1
198
199 }
200 }
201 fragment UnusedFrag1 on Human {
202 name
203 }
204 "#;
205 check_fails_rule!(query_doc, factory);
206 }
207}