Skip to content

Commit 4c546ce

Browse files
authored
feat: NoUnusedVariables rule (#25)
1 parent 7f6d749 commit 4c546ce

File tree

4 files changed

+354
-2
lines changed

4 files changed

+354
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ cargo add graphql-tools
5858
- [x] NoFragmentCycles
5959
- [ ] UniqueVariableNames
6060
- [x] NoUndefinedVariables
61-
- [ ] NoUnusedVariables
61+
- [x] NoUnusedVariables
6262
- [ ] KnownDirectives
6363
- [ ] UniqueDirectivesPerLocation
6464
- [x] KnownArgumentNames

src/validation/rules/defaults.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use super::{
55
KnownTypeNames, LeafFieldSelections, LoneAnonymousOperation, NoFragmentsCycle,
66
NoUndefinedVariables, NoUnusedFragments, OverlappingFieldsCanBeMerged, PossibleFragmentSpreads,
77
ProvidedRequiredArguments, SingleFieldSubscriptions, UniqueArgumentNames, UniqueFragmentNames,
8-
UniqueOperationNames, VariablesAreInputTypes,
8+
UniqueOperationNames, VariablesAreInputTypes, NoUnusedVariables,
99
};
1010

1111
pub fn default_rules_validation_plan() -> ValidationPlan {
@@ -25,6 +25,7 @@ pub fn default_rules_validation_plan() -> ValidationPlan {
2525
plan.add_rule(Box::new(OverlappingFieldsCanBeMerged {}));
2626
plan.add_rule(Box::new(NoFragmentsCycle {}));
2727
plan.add_rule(Box::new(PossibleFragmentSpreads {}));
28+
plan.add_rule(Box::new(NoUnusedVariables {}));
2829
plan.add_rule(Box::new(NoUndefinedVariables {}));
2930
plan.add_rule(Box::new(KnownArgumentNames {}));
3031
plan.add_rule(Box::new(UniqueArgumentNames {}));

src/validation/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod lone_anonymous_operation;
99
pub mod no_fragments_cycle;
1010
pub mod no_undefined_variables;
1111
pub mod no_unused_fragments;
12+
pub mod no_unused_variables;
1213
pub mod overlapping_fields_can_be_merged;
1314
pub mod possible_fragment_spreads;
1415
pub mod provided_required_arguments;
@@ -31,6 +32,7 @@ pub use self::lone_anonymous_operation::*;
3132
pub use self::no_fragments_cycle::*;
3233
pub use self::no_undefined_variables::*;
3334
pub use self::no_unused_fragments::*;
35+
pub use self::no_unused_variables::*;
3436
pub use self::overlapping_fields_can_be_merged::*;
3537
pub use self::possible_fragment_spreads::*;
3638
pub use self::provided_required_arguments::*;
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
use super::ValidationRule;
2+
use crate::ast::AstNodeWithName;
3+
use crate::static_graphql::query::OperationDefinition;
4+
use crate::validation::utils::{ValidationError, ValidationErrorContext};
5+
use crate::{
6+
ast::{ext::AstWithVariables, QueryVisitor},
7+
validation::utils::ValidationContext,
8+
};
9+
10+
/// No unused fragments
11+
///
12+
/// A GraphQL operation is only valid if all variables defined by an operation
13+
/// are used, either directly or within a spread fragment.
14+
///
15+
/// See https://spec.graphql.org/draft/#sec-All-Variables-Used
16+
pub struct NoUnusedVariables;
17+
18+
struct NoUnusedVariablesHelper<'a> {
19+
error_context: ValidationErrorContext<'a>,
20+
}
21+
22+
impl<'a> NoUnusedVariablesHelper<'a> {
23+
fn new(validation_context: &'a ValidationContext<'a>) -> Self {
24+
NoUnusedVariablesHelper {
25+
error_context: ValidationErrorContext::new(validation_context),
26+
}
27+
}
28+
}
29+
30+
impl<'a> QueryVisitor<NoUnusedVariablesHelper<'a>> for NoUnusedVariables {
31+
fn leave_operation_definition(
32+
&self,
33+
node: &OperationDefinition,
34+
visitor_context: &mut NoUnusedVariablesHelper<'a>,
35+
) {
36+
let variables = node.get_variables();
37+
let in_use = node.get_variables_in_use(&visitor_context.error_context.ctx.fragments);
38+
39+
variables
40+
.iter()
41+
.filter(|variable_name| !in_use.contains(&variable_name.name))
42+
.for_each(|unused_variable_name| {
43+
visitor_context.error_context.report_error(ValidationError {
44+
locations: vec![],
45+
message: match node.node_name() {
46+
Some(name) => format!(
47+
"Variable \"${}\" is never used in operation \"{}\".",
48+
unused_variable_name.name, name
49+
),
50+
None => {
51+
format!("Variable \"${}\" is never used.", unused_variable_name.name)
52+
}
53+
},
54+
});
55+
});
56+
}
57+
}
58+
59+
impl ValidationRule for NoUnusedVariables {
60+
fn validate<'a>(&self, ctx: &ValidationContext) -> Vec<ValidationError> {
61+
let mut helper = NoUnusedVariablesHelper::new(&ctx);
62+
self.visit_document(&ctx.operation.clone(), &mut helper);
63+
64+
helper.error_context.errors
65+
}
66+
}
67+
68+
#[test]
69+
fn use_all_variables() {
70+
use crate::validation::test_utils::*;
71+
72+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
73+
let errors = test_operation_without_schema(
74+
"query ($a: String, $b: String, $c: String) {
75+
field(a: $a, b: $b, c: $c)
76+
}",
77+
&mut plan,
78+
);
79+
80+
assert_eq!(get_messages(&errors).len(), 0);
81+
}
82+
83+
#[test]
84+
fn use_all_variables_deeply() {
85+
use crate::validation::test_utils::*;
86+
87+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
88+
let errors = test_operation_without_schema(
89+
"query Foo($a: String, $b: String, $c: String) {
90+
field(a: $a) {
91+
field(b: $b) {
92+
field(c: $c)
93+
}
94+
}
95+
}
96+
",
97+
&mut plan,
98+
);
99+
100+
assert_eq!(get_messages(&errors).len(), 0);
101+
}
102+
103+
#[test]
104+
fn use_all_variables_deeply_in_inline_fragments() {
105+
use crate::validation::test_utils::*;
106+
107+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
108+
let errors = test_operation_without_schema(
109+
" query Foo($a: String, $b: String, $c: String) {
110+
... on Type {
111+
field(a: $a) {
112+
field(b: $b) {
113+
... on Type {
114+
field(c: $c)
115+
}
116+
}
117+
}
118+
}
119+
}
120+
",
121+
&mut plan,
122+
);
123+
124+
assert_eq!(get_messages(&errors).len(), 0);
125+
}
126+
127+
#[test]
128+
fn use_all_variables_in_fragments() {
129+
use crate::validation::test_utils::*;
130+
131+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
132+
let errors = test_operation_without_schema(
133+
"query Foo($a: String, $b: String, $c: String) {
134+
...FragA
135+
}
136+
fragment FragA on Type {
137+
field(a: $a) {
138+
...FragB
139+
}
140+
}
141+
fragment FragB on Type {
142+
field(b: $b) {
143+
...FragC
144+
}
145+
}
146+
fragment FragC on Type {
147+
field(c: $c)
148+
}",
149+
&mut plan,
150+
);
151+
152+
assert_eq!(get_messages(&errors).len(), 0);
153+
}
154+
155+
#[test]
156+
fn variables_used_by_fragment_in_multiple_operations() {
157+
use crate::validation::test_utils::*;
158+
159+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
160+
let errors = test_operation_without_schema(
161+
"query Foo($a: String) {
162+
...FragA
163+
}
164+
query Bar($b: String) {
165+
...FragB
166+
}
167+
fragment FragA on Type {
168+
field(a: $a)
169+
}
170+
fragment FragB on Type {
171+
field(b: $b)
172+
}",
173+
&mut plan,
174+
);
175+
176+
assert_eq!(get_messages(&errors).len(), 0);
177+
}
178+
179+
#[test]
180+
fn variables_used_by_recursive_fragment() {
181+
use crate::validation::test_utils::*;
182+
183+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
184+
let errors = test_operation_without_schema(
185+
"query Foo($a: String) {
186+
...FragA
187+
}
188+
fragment FragA on Type {
189+
field(a: $a) {
190+
...FragA
191+
}
192+
}",
193+
&mut plan,
194+
);
195+
196+
assert_eq!(get_messages(&errors).len(), 0);
197+
}
198+
199+
#[test]
200+
fn variables_not_used() {
201+
use crate::validation::test_utils::*;
202+
203+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
204+
let errors = test_operation_without_schema(
205+
"query ($a: String, $b: String, $c: String) {
206+
field(a: $a, b: $b)
207+
}",
208+
&mut plan,
209+
);
210+
211+
let messages = get_messages(&errors);
212+
213+
assert_eq!(messages.len(), 1);
214+
assert!(messages.contains(&&"Variable \"$c\" is never used.".to_owned()));
215+
}
216+
217+
#[test]
218+
fn multiple_variables_not_used() {
219+
use crate::validation::test_utils::*;
220+
221+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
222+
let errors = test_operation_without_schema(
223+
"query Foo($a: String, $b: String, $c: String) {
224+
field(b: $b)
225+
}",
226+
&mut plan,
227+
);
228+
229+
let messages = get_messages(&errors);
230+
231+
assert_eq!(messages.len(), 2);
232+
assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
233+
assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
234+
}
235+
236+
#[test]
237+
fn variables_not_used_in_fragments() {
238+
use crate::validation::test_utils::*;
239+
240+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
241+
let errors = test_operation_without_schema(
242+
"query Foo($a: String, $b: String, $c: String) {
243+
...FragA
244+
}
245+
fragment FragA on Type {
246+
field(a: $a) {
247+
...FragB
248+
}
249+
}
250+
fragment FragB on Type {
251+
field(b: $b) {
252+
...FragC
253+
}
254+
}
255+
fragment FragC on Type {
256+
field
257+
}",
258+
&mut plan,
259+
);
260+
261+
let messages = get_messages(&errors);
262+
263+
assert_eq!(messages.len(), 1);
264+
assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
265+
}
266+
267+
#[test]
268+
fn multiple_variables_not_used_in_fragments() {
269+
use crate::validation::test_utils::*;
270+
271+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
272+
let errors = test_operation_without_schema(
273+
"query Foo($a: String, $b: String, $c: String) {
274+
...FragA
275+
}
276+
fragment FragA on Type {
277+
field {
278+
...FragB
279+
}
280+
}
281+
fragment FragB on Type {
282+
field(b: $b) {
283+
...FragC
284+
}
285+
}
286+
fragment FragC on Type {
287+
field
288+
}",
289+
&mut plan,
290+
);
291+
292+
let messages = get_messages(&errors);
293+
294+
assert_eq!(messages.len(), 2);
295+
assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Foo\".".to_owned()));
296+
assert!(messages.contains(&&"Variable \"$c\" is never used in operation \"Foo\".".to_owned()));
297+
}
298+
299+
#[test]
300+
fn variables_not_used_by_unreferences_fragment() {
301+
use crate::validation::test_utils::*;
302+
303+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
304+
let errors = test_operation_without_schema(
305+
"query Foo($b: String) {
306+
...FragA
307+
}
308+
fragment FragA on Type {
309+
field(a: $a)
310+
}
311+
fragment FragB on Type {
312+
field(b: $b)
313+
}",
314+
&mut plan,
315+
);
316+
317+
let messages = get_messages(&errors);
318+
319+
assert_eq!(messages.len(), 1);
320+
assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
321+
}
322+
323+
#[test]
324+
fn variables_not_used_by_fragment_used_by_other_operation() {
325+
use crate::validation::test_utils::*;
326+
327+
let mut plan = create_plan_from_rule(Box::new(NoUnusedVariables {}));
328+
let errors = test_operation_without_schema(
329+
"query Foo($b: String) {
330+
...FragA
331+
}
332+
query Bar($a: String) {
333+
...FragB
334+
}
335+
fragment FragA on Type {
336+
field(a: $a)
337+
}
338+
fragment FragB on Type {
339+
field(b: $b)
340+
}",
341+
&mut plan,
342+
);
343+
344+
let messages = get_messages(&errors);
345+
346+
assert_eq!(messages.len(), 2);
347+
assert!(messages.contains(&&"Variable \"$b\" is never used in operation \"Foo\".".to_owned()));
348+
assert!(messages.contains(&&"Variable \"$a\" is never used in operation \"Bar\".".to_owned()));
349+
}

0 commit comments

Comments
 (0)