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