async_graphql/validation/rules/
known_argument_names.rs

1use async_graphql_value::Value;
2use indexmap::map::IndexMap;
3
4use crate::{
5    Name, Positioned,
6    parser::types::{Directive, Field},
7    registry::MetaInputValue,
8    validation::{
9        suggestion::make_suggestion,
10        visitor::{Visitor, VisitorContext},
11    },
12};
13
14enum ArgsType<'a> {
15    Directive(&'a str),
16    Field {
17        field_name: &'a str,
18        type_name: &'a str,
19    },
20}
21
22#[derive(Default)]
23pub struct KnownArgumentNames<'a> {
24    current_args: Option<(&'a IndexMap<String, MetaInputValue>, ArgsType<'a>)>,
25}
26
27impl KnownArgumentNames<'_> {
28    fn get_suggestion(&self, name: &str) -> String {
29        make_suggestion(
30            " Did you mean",
31            self.current_args
32                .iter()
33                .map(|(args, _)| args.iter().map(|arg| arg.0.as_str()))
34                .flatten(),
35            name,
36        )
37        .unwrap_or_default()
38    }
39}
40
41impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
42    fn enter_directive(
43        &mut self,
44        ctx: &mut VisitorContext<'a>,
45        directive: &'a Positioned<Directive>,
46    ) {
47        self.current_args = ctx
48            .registry
49            .directives
50            .get(directive.node.name.node.as_str())
51            .map(|d| (&d.args, ArgsType::Directive(&directive.node.name.node)));
52    }
53
54    fn exit_directive(
55        &mut self,
56        _ctx: &mut VisitorContext<'a>,
57        _directive: &'a Positioned<Directive>,
58    ) {
59        self.current_args = None;
60    }
61
62    fn enter_argument(
63        &mut self,
64        ctx: &mut VisitorContext<'a>,
65        name: &'a Positioned<Name>,
66        _value: &'a Positioned<Value>,
67    ) {
68        if let Some((args, arg_type)) = &self.current_args {
69            if !args.contains_key(name.node.as_str()) {
70                match arg_type {
71                    ArgsType::Field {
72                        field_name,
73                        type_name,
74                    } => {
75                        ctx.report_error(
76                            vec![name.pos],
77                            format!(
78                                "Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}",
79                                name,
80                                field_name,
81                                type_name,
82                                if ctx.registry.enable_suggestions {
83                                    self.get_suggestion(name.node.as_str())
84                                } else {
85                                    String::new()
86                                }
87                            ),
88                        );
89                    }
90                    ArgsType::Directive(directive_name) => {
91                        ctx.report_error(
92                            vec![name.pos],
93                            format!(
94                                "Unknown argument \"{}\" on directive \"{}\".{}",
95                                name,
96                                directive_name,
97                                self.get_suggestion(name.node.as_str())
98                            ),
99                        );
100                    }
101                }
102            }
103        }
104    }
105
106    fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) {
107        if let Some(parent_type) = ctx.parent_type() {
108            if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) {
109                self.current_args = Some((
110                    &schema_field.args,
111                    ArgsType::Field {
112                        field_name: &field.node.name.node,
113                        type_name: ctx.parent_type().unwrap().name(),
114                    },
115                ));
116            }
117        }
118    }
119
120    fn exit_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) {
121        self.current_args = None;
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    pub fn factory<'a>() -> KnownArgumentNames<'a> {
130        KnownArgumentNames::default()
131    }
132
133    #[test]
134    fn single_arg_is_known() {
135        expect_passes_rule!(
136            factory,
137            r#"
138          fragment argOnRequiredArg on Dog {
139            doesKnowCommand(dogCommand: SIT)
140          }
141          { __typename }
142        "#,
143        );
144    }
145
146    #[test]
147    fn multiple_args_are_known() {
148        expect_passes_rule!(
149            factory,
150            r#"
151          fragment multipleArgs on ComplicatedArgs {
152            multipleReqs(req1: 1, req2: 2)
153          }
154          { __typename }
155        "#,
156        );
157    }
158
159    #[test]
160    fn ignores_args_of_unknown_fields() {
161        expect_passes_rule!(
162            factory,
163            r#"
164          fragment argOnUnknownField on Dog {
165            unknownField(unknownArg: SIT)
166          }
167          { __typename }
168        "#,
169        );
170    }
171
172    #[test]
173    fn multiple_args_in_reverse_order_are_known() {
174        expect_passes_rule!(
175            factory,
176            r#"
177          fragment multipleArgsReverseOrder on ComplicatedArgs {
178            multipleReqs(req2: 2, req1: 1)
179          }
180          { __typename }
181        "#,
182        );
183    }
184
185    #[test]
186    fn no_args_on_optional_arg() {
187        expect_passes_rule!(
188            factory,
189            r#"
190          fragment noArgOnOptionalArg on Dog {
191            isHousetrained
192          }
193          { __typename }
194        "#,
195        );
196    }
197
198    #[test]
199    fn args_are_known_deeply() {
200        expect_passes_rule!(
201            factory,
202            r#"
203          {
204            dog {
205              doesKnowCommand(dogCommand: SIT)
206            }
207            human {
208              pet {
209                ... on Dog {
210                  doesKnowCommand(dogCommand: SIT)
211                }
212              }
213            }
214          }
215        "#,
216        );
217    }
218
219    #[test]
220    fn directive_args_are_known() {
221        expect_passes_rule!(
222            factory,
223            r#"
224          {
225            dog @skip(if: true)
226          }
227        "#,
228        );
229    }
230
231    #[test]
232    fn undirective_args_are_invalid() {
233        expect_fails_rule!(
234            factory,
235            r#"
236          {
237            dog @skip(unless: true)
238          }
239        "#,
240        );
241    }
242
243    #[test]
244    fn invalid_arg_name() {
245        expect_fails_rule!(
246            factory,
247            r#"
248          fragment invalidArgName on Dog {
249            doesKnowCommand(unknown: true)
250          }
251          { __typename }
252        "#,
253        );
254    }
255
256    #[test]
257    fn unknown_args_amongst_known_args() {
258        expect_fails_rule!(
259            factory,
260            r#"
261          fragment oneGoodArgOneInvalidArg on Dog {
262            doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
263          }
264          { __typename }
265        "#,
266        );
267    }
268
269    #[test]
270    fn unknown_args_deeply() {
271        expect_fails_rule!(
272            factory,
273            r#"
274          {
275            dog {
276              doesKnowCommand(unknown: true)
277            }
278            human {
279              pet {
280                ... on Dog {
281                  doesKnowCommand(unknown: true)
282                }
283              }
284            }
285          }
286        "#,
287        );
288    }
289}