graphql_tools/validation/rules/
no_unused_variables.rs1use std::collections::{HashMap, HashSet};
2
3use super::ValidationRule;
4use crate::ast::{visit_document, AstNodeWithName, OperationVisitor, OperationVisitorContext};
5use crate::static_graphql::query::{self, OperationDefinition};
6use crate::validation::utils::{ValidationError, ValidationErrorContext};
7
8pub struct NoUnusedVariables<'a> {
15 current_scope: Option<NoUnusedVariablesScope<'a>>,
16 defined_variables: HashMap<Option<&'a str>, HashSet<&'a str>>,
17 used_variables: HashMap<NoUnusedVariablesScope<'a>, Vec<&'a str>>,
18 spreads: HashMap<NoUnusedVariablesScope<'a>, Vec<&'a str>>,
19}
20
21impl<'a> Default for NoUnusedVariables<'a> {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl<'a> NoUnusedVariables<'a> {
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<'a> NoUnusedVariables<'a> {
39 fn find_used_vars(
40 &self,
41 from: &NoUnusedVariablesScope<'a>,
42 defined: &HashSet<&str>,
43 used: &mut HashSet<&'a str>,
44 visited: &mut HashSet<NoUnusedVariablesScope<'a>>,
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<'a> {
75 Operation(Option<&'a str>),
76 Fragment(&'a str),
77}
78
79impl<'a> OperationVisitor<'a, ValidationErrorContext> for NoUnusedVariables<'a> {
80 fn enter_operation_definition(
81 &mut self,
82 _: &mut OperationVisitorContext,
83 _: &mut ValidationErrorContext,
84 operation_definition: &'a 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: &'a 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: &'a 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: &'a 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): &'a (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<'n> ValidationRule for NoUnusedVariables<'n> {
184 fn error_code<'a>(&self) -> &'a str {
185 "NoUnusedVariables"
186 }
187
188 fn validate(
189 &self,
190 ctx: &mut OperationVisitorContext,
191 error_collector: &mut ValidationErrorContext,
192 ) {
193 visit_document(
194 &mut NoUnusedVariables::new(),
195 ctx.operation,
196 ctx,
197 error_collector,
198 );
199 }
200}
201
202#[test]
203fn use_all_variables() {
204 use crate::validation::test_utils::*;
205
206 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
207 let errors = test_operation_with_schema(
208 "query ($a: String, $b: String, $c: String) {
209 field(a: $a, b: $b, c: $c)
210 }",
211 TEST_SCHEMA,
212 &mut plan,
213 );
214
215 assert_eq!(get_messages(&errors).len(), 0);
216}
217
218#[test]
219fn use_all_variables_deeply() {
220 use crate::validation::test_utils::*;
221
222 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
223 let errors = test_operation_with_schema(
224 "query Foo($a: String, $b: String, $c: String) {
225 field(a: $a) {
226 field(b: $b) {
227 field(c: $c)
228 }
229 }
230 }
231 ",
232 TEST_SCHEMA,
233 &mut plan,
234 );
235
236 assert_eq!(get_messages(&errors).len(), 0);
237}
238
239#[test]
240fn use_all_variables_deeply_in_inline_fragments() {
241 use crate::validation::test_utils::*;
242
243 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
244 let errors = test_operation_with_schema(
245 " query Foo($a: String, $b: String, $c: String) {
246 ... on Type {
247 field(a: $a) {
248 field(b: $b) {
249 ... on Type {
250 field(c: $c)
251 }
252 }
253 }
254 }
255 }
256 ",
257 TEST_SCHEMA,
258 &mut plan,
259 );
260
261 assert_eq!(get_messages(&errors).len(), 0);
262}
263
264#[test]
265fn use_all_variables_in_fragments() {
266 use crate::validation::test_utils::*;
267
268 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
269 let errors = test_operation_with_schema(
270 "query Foo($a: String, $b: String, $c: String) {
271 ...FragA
272 }
273 fragment FragA on Type {
274 field(a: $a) {
275 ...FragB
276 }
277 }
278 fragment FragB on Type {
279 field(b: $b) {
280 ...FragC
281 }
282 }
283 fragment FragC on Type {
284 field(c: $c)
285 }",
286 TEST_SCHEMA,
287 &mut plan,
288 );
289
290 assert_eq!(get_messages(&errors).len(), 0);
291}
292
293#[test]
294fn variables_used_by_fragment_in_multiple_operations() {
295 use crate::validation::test_utils::*;
296
297 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
298 let errors = test_operation_with_schema(
299 "query Foo($a: String) {
300 ...FragA
301 }
302 query Bar($b: String) {
303 ...FragB
304 }
305 fragment FragA on Type {
306 field(a: $a)
307 }
308 fragment FragB on Type {
309 field(b: $b)
310 }",
311 TEST_SCHEMA,
312 &mut plan,
313 );
314
315 assert_eq!(get_messages(&errors).len(), 0);
316}
317
318#[test]
319fn variables_used_by_recursive_fragment() {
320 use crate::validation::test_utils::*;
321
322 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
323 let errors = test_operation_with_schema(
324 "query Foo($a: String) {
325 ...FragA
326 }
327 fragment FragA on Type {
328 field(a: $a) {
329 ...FragA
330 }
331 }",
332 TEST_SCHEMA,
333 &mut plan,
334 );
335
336 assert_eq!(get_messages(&errors).len(), 0);
337}
338
339#[test]
340fn variables_not_used() {
341 use crate::validation::test_utils::*;
342
343 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
344 let errors = test_operation_with_schema(
345 "query ($a: String, $b: String, $c: String) {
346 field(a: $a, b: $b)
347 }",
348 TEST_SCHEMA,
349 &mut plan,
350 );
351
352 let messages = get_messages(&errors);
353
354 assert_eq!(messages.len(), 1);
355 assert!(messages.contains(&&"Variable \"$c\" is never used.".to_owned()));
356}
357
358#[test]
359fn multiple_variables_not_used() {
360 use crate::validation::test_utils::*;
361
362 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
363 let errors = test_operation_with_schema(
364 "query Foo($a: String, $b: String, $c: String) {
365 field(b: $b)
366 }",
367 TEST_SCHEMA,
368 &mut plan,
369 );
370
371 let messages = get_messages(&errors);
372
373 assert_eq!(messages.len(), 2);
374 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
375 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
376}
377
378#[test]
379fn variables_not_used_in_fragments() {
380 use crate::validation::test_utils::*;
381
382 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
383 let errors = test_operation_with_schema(
384 "query Foo($a: String, $b: String, $c: String) {
385 ...FragA
386 }
387 fragment FragA on Type {
388 field(a: $a) {
389 ...FragB
390 }
391 }
392 fragment FragB on Type {
393 field(b: $b) {
394 ...FragC
395 }
396 }
397 fragment FragC on Type {
398 field
399 }",
400 TEST_SCHEMA,
401 &mut plan,
402 );
403
404 let messages = get_messages(&errors);
405
406 assert_eq!(messages.len(), 1);
407 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
408}
409
410#[test]
411fn multiple_variables_not_used_in_fragments() {
412 use crate::validation::test_utils::*;
413
414 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
415 let errors = test_operation_with_schema(
416 "query Foo($a: String, $b: String, $c: String) {
417 ...FragA
418 }
419 fragment FragA on Type {
420 field {
421 ...FragB
422 }
423 }
424 fragment FragB on Type {
425 field(b: $b) {
426 ...FragC
427 }
428 }
429 fragment FragC on Type {
430 field
431 }",
432 TEST_SCHEMA,
433 &mut plan,
434 );
435
436 let messages = get_messages(&errors);
437
438 assert_eq!(messages.len(), 2);
439 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
440 assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
441}
442
443#[test]
444fn variables_not_used_by_unreferences_fragment() {
445 use crate::validation::test_utils::*;
446
447 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
448 let errors = test_operation_with_schema(
449 "query Foo($b: String) {
450 ...FragA
451 }
452 fragment FragA on Type {
453 field(a: $a)
454 }
455 fragment FragB on Type {
456 field(b: $b)
457 }",
458 TEST_SCHEMA,
459 &mut plan,
460 );
461
462 let messages = get_messages(&errors);
463
464 assert_eq!(messages.len(), 1);
465 assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
466}
467
468#[test]
469fn variables_not_used_by_fragment_used_by_other_operation() {
470 use crate::validation::test_utils::*;
471
472 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
473 let errors = test_operation_with_schema(
474 "query Foo($b: String) {
475 ...FragA
476 }
477 query Bar($a: String) {
478 ...FragB
479 }
480 fragment FragA on Type {
481 field(a: $a)
482 }
483 fragment FragB on Type {
484 field(b: $b)
485 }",
486 TEST_SCHEMA,
487 &mut plan,
488 );
489
490 let messages = get_messages(&errors);
491
492 assert_eq!(messages.len(), 2);
493 assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
494 assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Bar\".".to_owned()));
495}
496
497#[test]
498fn should_also_check_directives_usage() {
499 use crate::validation::test_utils::*;
500
501 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
502 let errors = test_operation_with_schema(
503 "query foo($skip: Boolean!) {
504 field @skip(if: $skip)
505 }
506 ",
507 TEST_SCHEMA,
508 &mut plan,
509 );
510
511 let messages = get_messages(&errors);
512 assert_eq!(messages.len(), 0);
513}
514
515#[test]
516fn nested_variable_should_work_as_well() {
517 use crate::validation::test_utils::*;
518
519 let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables::new()));
520 let errors = test_operation_with_schema(
521 "query foo($t: Boolean!) {
522 field(boop: { test: $t})
523 }
524 ",
525 TEST_SCHEMA,
526 &mut plan,
527 );
528
529 let messages = get_messages(&errors);
530 assert_eq!(messages.len(), 0);
531}