graphql_tools/validation/rules/
no_unused_variables.rs1use std::collections::{HashMap, HashSet};
2
3use super::ValidationRule;
4use crate::ast::{AstNodeWithName, OperationVisitor, OperationVisitorContext};
5use crate::static_graphql::query::{self, OperationDefinition};
6use crate::validation::utils::{ValidationError, ValidationErrorContext};
7
8pub struct NoUnusedVariables<'doc> {
15 current_scope: Option<NoUnusedVariablesScope<'doc>>,
16 defined_variables: HashMap<Option<&'doc str>, HashSet<&'doc str>>,
17 used_variables: HashMap<NoUnusedVariablesScope<'doc>, Vec<&'doc str>>,
18 spreads: HashMap<NoUnusedVariablesScope<'doc>, Vec<&'doc str>>,
19}
20
21impl Default for NoUnusedVariables<'_> {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl NoUnusedVariables<'_> {
28 pub fn new() -> Self {
29 Self {
30 current_scope: None,
31 defined_variables: HashMap::new(),
32 used_variables: HashMap::new(),
33 spreads: HashMap::new(),
34 }
35 }
36}
37
38impl<'doc> NoUnusedVariables<'doc> {
39 fn find_used_vars(
40 &self,
41 from: &NoUnusedVariablesScope<'doc>,
42 defined: &HashSet<&str>,
43 used: &mut HashSet<&'doc str>,
44 visited: &mut HashSet<NoUnusedVariablesScope<'doc>>,
45 ) {
46 if visited.contains(from) {
47 return;
48 }
49
50 visited.insert(from.clone());
51
52 if let Some(used_vars) = self.used_variables.get(from) {
53 for var in used_vars {
54 if defined.contains(var) {
55 used.insert(var);
56 }
57 }
58 }
59
60 if let Some(spreads) = self.spreads.get(from) {
61 for spread in spreads {
62 self.find_used_vars(
63 &NoUnusedVariablesScope::Fragment(spread),
64 defined,
65 used,
66 visited,
67 );
68 }
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
74pub enum NoUnusedVariablesScope<'doc> {
75 Operation(Option<&'doc str>),
76 Fragment(&'doc str),
77}
78
79impl<'doc> OperationVisitor<'doc, ValidationErrorContext> for NoUnusedVariables<'doc> {
80 fn enter_operation_definition(
81 &mut self,
82 _: &mut OperationVisitorContext,
83 _: &mut ValidationErrorContext,
84 operation_definition: &'doc OperationDefinition,
85 ) {
86 let op_name = operation_definition.node_name();
87 self.current_scope = Some(NoUnusedVariablesScope::Operation(op_name));
88 self.defined_variables.insert(op_name, HashSet::new());
89 }
90
91 fn enter_fragment_definition(
92 &mut self,
93 _: &mut OperationVisitorContext,
94 _: &mut ValidationErrorContext,
95 fragment_definition: &'doc query::FragmentDefinition,
96 ) {
97 self.current_scope = Some(NoUnusedVariablesScope::Fragment(&fragment_definition.name));
98 }
99
100 fn enter_fragment_spread(
101 &mut self,
102 _: &mut OperationVisitorContext,
103 _: &mut ValidationErrorContext,
104 fragment_spread: &'doc query::FragmentSpread,
105 ) {
106 if let Some(scope) = &self.current_scope {
107 self.spreads
108 .entry(scope.clone())
109 .or_default()
110 .push(&fragment_spread.fragment_name);
111 }
112 }
113
114 fn enter_variable_definition(
115 &mut self,
116 _: &mut OperationVisitorContext,
117 _: &mut ValidationErrorContext,
118 variable_definition: &'doc query::VariableDefinition,
119 ) {
120 if let Some(NoUnusedVariablesScope::Operation(ref name)) = self.current_scope {
121 if let Some(vars) = self.defined_variables.get_mut(name) {
122 vars.insert(&variable_definition.name);
123 }
124 }
125 }
126
127 fn enter_argument(
128 &mut self,
129 _: &mut OperationVisitorContext,
130 _: &mut ValidationErrorContext,
131 (_arg_name, arg_value): &'doc (String, query::Value),
132 ) {
133 if let Some(ref scope) = self.current_scope {
134 self.used_variables
135 .entry(scope.clone())
136 .or_default()
137 .append(&mut arg_value.variables_in_use());
138 }
139 }
140
141 fn leave_document(
142 &mut self,
143 _: &mut OperationVisitorContext,
144 user_context: &mut ValidationErrorContext,
145 _: &query::Document,
146 ) {
147 for (op_name, def_vars) in &self.defined_variables {
148 let mut used = HashSet::new();
149 let mut visited = HashSet::new();
150
151 self.find_used_vars(
152 &NoUnusedVariablesScope::Operation(*op_name),
153 def_vars,
154 &mut used,
155 &mut visited,
156 );
157
158 def_vars
159 .iter()
160 .filter(|var| !used.contains(*var))
161 .for_each(|var| {
162 user_context.report_error(ValidationError {
163 error_code: self.error_code(),
164 message: error_message(var, op_name),
165 locations: vec![],
166 })
167 })
168 }
169 }
170}
171
172fn error_message(var_name: &str, op_name: &Option<&str>) -> String {
173 if let Some(op_name) = op_name {
174 format!(
175 r#"Variable "${}" is never used in operation "{}"."#,
176 var_name, op_name
177 )
178 } else {
179 format!(r#"Variable "${}" is never used."#, var_name)
180 }
181}
182
183impl ValidationRule for NoUnusedVariables<'_> {
184 fn error_code(&self) -> &'static str {
185 "NoUnusedVariables"
186 }
187
188 fn visitor<'doc>(&self) -> super::ValidationVisitor<'doc> {
189 Box::new(NoUnusedVariables::new())
190 }
191}
192
193#[test]
194fn use_all_variables() {
195 use crate::validation::test_utils::*;
196
197 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
198 let errors = test_operation_with_schema(
199 "query ($a: String, $b: String, $c: String) {
200 field(a: $a, b: $b, c: $c)
201 }",
202 TEST_SCHEMA,
203 &mut plan,
204 );
205
206 assert_eq!(get_messages(&errors).len(), 0);
207}
208
209#[test]
210fn use_all_variables_deeply() {
211 use crate::validation::test_utils::*;
212
213 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
214 let errors = test_operation_with_schema(
215 "query Foo($a: String, $b: String, $c: String) {
216 field(a: $a) {
217 field(b: $b) {
218 field(c: $c)
219 }
220 }
221 }
222 ",
223 TEST_SCHEMA,
224 &mut plan,
225 );
226
227 assert_eq!(get_messages(&errors).len(), 0);
228}
229
230#[test]
231fn use_all_variables_deeply_in_inline_fragments() {
232 use crate::validation::test_utils::*;
233
234 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
235 let errors = test_operation_with_schema(
236 " query Foo($a: String, $b: String, $c: String) {
237 ... on Type {
238 field(a: $a) {
239 field(b: $b) {
240 ... on Type {
241 field(c: $c)
242 }
243 }
244 }
245 }
246 }
247 ",
248 TEST_SCHEMA,
249 &mut plan,
250 );
251
252 assert_eq!(get_messages(&errors).len(), 0);
253}
254
255#[test]
256fn use_all_variables_in_fragments() {
257 use crate::validation::test_utils::*;
258
259 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
260 let errors = test_operation_with_schema(
261 "query Foo($a: String, $b: String, $c: String) {
262 ...FragA
263 }
264 fragment FragA on Type {
265 field(a: $a) {
266 ...FragB
267 }
268 }
269 fragment FragB on Type {
270 field(b: $b) {
271 ...FragC
272 }
273 }
274 fragment FragC on Type {
275 field(c: $c)
276 }",
277 TEST_SCHEMA,
278 &mut plan,
279 );
280
281 assert_eq!(get_messages(&errors).len(), 0);
282}
283
284#[test]
285fn variables_used_by_fragment_in_multiple_operations() {
286 use crate::validation::test_utils::*;
287
288 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
289 let errors = test_operation_with_schema(
290 "query Foo($a: String) {
291 ...FragA
292 }
293 query Bar($b: String) {
294 ...FragB
295 }
296 fragment FragA on Type {
297 field(a: $a)
298 }
299 fragment FragB on Type {
300 field(b: $b)
301 }",
302 TEST_SCHEMA,
303 &mut plan,
304 );
305
306 assert_eq!(get_messages(&errors).len(), 0);
307}
308
309#[test]
310fn variables_used_by_recursive_fragment() {
311 use crate::validation::test_utils::*;
312
313 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
314 let errors = test_operation_with_schema(
315 "query Foo($a: String) {
316 ...FragA
317 }
318 fragment FragA on Type {
319 field(a: $a) {
320 ...FragA
321 }
322 }",
323 TEST_SCHEMA,
324 &mut plan,
325 );
326
327 assert_eq!(get_messages(&errors).len(), 0);
328}
329
330#[test]
331fn variables_not_used() {
332 use crate::validation::test_utils::*;
333
334 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
335 let errors = test_operation_with_schema(
336 "query ($a: String, $b: String, $c: String) {
337 field(a: $a, b: $b)
338 }",
339 TEST_SCHEMA,
340 &mut plan,
341 );
342
343 let messages = get_messages(&errors);
344
345 assert_eq!(messages.len(), 1);
346 assert!(messages.contains(&&"Variable \"$c\" is never used.".to_owned()));
347}
348
349#[test]
350fn multiple_variables_not_used() {
351 use crate::validation::test_utils::*;
352
353 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
354 let errors = test_operation_with_schema(
355 "query Foo($a: String, $b: String, $c: String) {
356 field(b: $b)
357 }",
358 TEST_SCHEMA,
359 &mut plan,
360 );
361
362 let messages = get_messages(&errors);
363
364 assert_eq!(messages.len(), 2);
365 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
366 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
367}
368
369#[test]
370fn variables_not_used_in_fragments() {
371 use crate::validation::test_utils::*;
372
373 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
374 let errors = test_operation_with_schema(
375 "query Foo($a: String, $b: String, $c: String) {
376 ...FragA
377 }
378 fragment FragA on Type {
379 field(a: $a) {
380 ...FragB
381 }
382 }
383 fragment FragB on Type {
384 field(b: $b) {
385 ...FragC
386 }
387 }
388 fragment FragC on Type {
389 field
390 }",
391 TEST_SCHEMA,
392 &mut plan,
393 );
394
395 let messages = get_messages(&errors);
396
397 assert_eq!(messages.len(), 1);
398 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
399}
400
401#[test]
402fn multiple_variables_not_used_in_fragments() {
403 use crate::validation::test_utils::*;
404
405 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
406 let errors = test_operation_with_schema(
407 "query Foo($a: String, $b: String, $c: String) {
408 ...FragA
409 }
410 fragment FragA on Type {
411 field {
412 ...FragB
413 }
414 }
415 fragment FragB on Type {
416 field(b: $b) {
417 ...FragC
418 }
419 }
420 fragment FragC on Type {
421 field
422 }",
423 TEST_SCHEMA,
424 &mut plan,
425 );
426
427 let messages = get_messages(&errors);
428
429 assert_eq!(messages.len(), 2);
430 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
431 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
432}
433
434#[test]
435fn variables_not_used_by_unreferences_fragment() {
436 use crate::validation::test_utils::*;
437
438 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
439 let errors = test_operation_with_schema(
440 "query Foo($b: String) {
441 ...FragA
442 }
443 fragment FragA on Type {
444 field(a: $a)
445 }
446 fragment FragB on Type {
447 field(b: $b)
448 }",
449 TEST_SCHEMA,
450 &mut plan,
451 );
452
453 let messages = get_messages(&errors);
454
455 assert_eq!(messages.len(), 1);
456 assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
457}
458
459#[test]
460fn variables_not_used_by_fragment_used_by_other_operation() {
461 use crate::validation::test_utils::*;
462
463 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
464 let errors = test_operation_with_schema(
465 "query Foo($b: String) {
466 ...FragA
467 }
468 query Bar($a: String) {
469 ...FragB
470 }
471 fragment FragA on Type {
472 field(a: $a)
473 }
474 fragment FragB on Type {
475 field(b: $b)
476 }",
477 TEST_SCHEMA,
478 &mut plan,
479 );
480
481 let messages = get_messages(&errors);
482
483 assert_eq!(messages.len(), 2);
484 assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
485 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Bar\".".to_owned()));
486}
487
488#[test]
489fn should_also_check_directives_usage() {
490 use crate::validation::test_utils::*;
491
492 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
493 let errors = test_operation_with_schema(
494 "query foo($skip: Boolean!) {
495 field @skip(if: $skip)
496 }
497 ",
498 TEST_SCHEMA,
499 &mut plan,
500 );
501
502 let messages = get_messages(&errors);
503 assert_eq!(messages.len(), 0);
504}
505
506#[test]
507fn nested_variable_should_work_as_well() {
508 use crate::validation::test_utils::*;
509
510 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
511 let errors = test_operation_with_schema(
512 "query foo($t: Boolean!) {
513 field(boop: { test: $t})
514 }
515 ",
516 TEST_SCHEMA,
517 &mut plan,
518 );
519
520 let messages = get_messages(&errors);
521 assert_eq!(messages.len(), 0);
522}