Skip to content

Commit 4709148

Browse files
committed
improvement: add unique_argument_definition_names validator
1 parent be9e561 commit 4709148

File tree

4 files changed

+240
-5
lines changed

4 files changed

+240
-5
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import "package:collection/collection.dart";
2+
import "package:gql/ast.dart";
3+
import "package:gql/document.dart";
4+
import "package:gql/src/validation/validating_visitor.dart";
5+
6+
class DuplicateArgumentDefinitionNameError extends ValidationError {
7+
const DuplicateArgumentDefinitionNameError(
8+
{required String parentName, required String argumentName, Node? node})
9+
: super(
10+
message:
11+
'Argument "${parentName}(${argumentName}:)" can only be defined once.',
12+
node: node,
13+
);
14+
}
15+
16+
/// Unique argument definition names
17+
///
18+
/// A GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments.
19+
/// A GraphQL Directive is only valid if all its arguments are uniquely named.
20+
class UniqueArgumentDefinitionNames extends ValidatingVisitor {
21+
@override
22+
List<ValidationError>? visitDirectiveDefinitionNode(
23+
DirectiveDefinitionNode node,
24+
) =>
25+
_checkArgumentUniqueness(
26+
parentName: "@${node.name.value}",
27+
argumentNodes: node.args,
28+
);
29+
30+
@override
31+
List<ValidationError>? visitInterfaceTypeDefinitionNode(
32+
InterfaceTypeDefinitionNode node,
33+
) =>
34+
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);
35+
36+
@override
37+
List<ValidationError>? visitInterfaceTypeExtensionNode(
38+
InterfaceTypeExtensionNode node,
39+
) =>
40+
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);
41+
42+
@override
43+
List<ValidationError>? visitObjectTypeDefinitionNode(
44+
ObjectTypeDefinitionNode node,
45+
) =>
46+
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);
47+
48+
@override
49+
List<ValidationError>? visitObjectTypeExtensionNode(
50+
ObjectTypeExtensionNode node,
51+
) =>
52+
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);
53+
54+
List<ValidationError> _checkArgumentUniquenessPerField({
55+
required NameNode name,
56+
required List<FieldDefinitionNode> fields,
57+
}) =>
58+
fields
59+
.expand(
60+
(e) => _checkArgumentUniqueness(
61+
parentName: "${name.value}.${e.name.value}",
62+
argumentNodes: e.args),
63+
)
64+
.toList();
65+
66+
List<ValidationError> _checkArgumentUniqueness({
67+
required String parentName,
68+
required List<InputValueDefinitionNode> argumentNodes,
69+
}) =>
70+
argumentNodes
71+
.groupListsBy((it) => it.name.value)
72+
.entries
73+
.map(
74+
(e) => e.value.length > 1
75+
? DuplicateArgumentDefinitionNameError(
76+
parentName: parentName,
77+
argumentName: e.key,
78+
)
79+
: null,
80+
)
81+
.nonNulls
82+
.toList();
83+
}

gql/lib/src/validation/validator.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "package:gql/ast.dart" as ast;
22
import "package:gql/src/validation/rules/lone_schema_definition.dart";
33
import "package:gql/src/validation/rules/missing_fragment_definitions.dart";
44
import "package:gql/src/validation/rules/possible_type_extensions.dart";
5+
import "package:gql/src/validation/rules/unique_argument_definition_names.dart";
56
import "package:gql/src/validation/rules/unique_argument_names.dart";
67
import "package:gql/src/validation/rules/unique_directive_names.dart";
78
import "package:gql/src/validation/rules/unique_enum_value_names.dart";
@@ -104,6 +105,7 @@ enum ValidationRule {
104105
uniqueArgumentNames,
105106
missingFragmentDefinition,
106107
possibleTypeExtensions,
108+
uniqueArgumentDefinitionNames,
107109
}
108110

109111
ValidatingVisitor? _mapRule(ValidationRule rule) {
@@ -128,6 +130,8 @@ ValidatingVisitor? _mapRule(ValidationRule rule) {
128130
return const MissingFragmentDefinition();
129131
case ValidationRule.possibleTypeExtensions:
130132
return PossibleTypeExtensions();
133+
case ValidationRule.uniqueArgumentDefinitionNames:
134+
return UniqueArgumentDefinitionNames();
131135
}
132136
}
133137

gql/test/validation/common.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@ Matcher errorOfType<T extends ValidationError>() => predicate(
66
(ValidationError error) => error is T,
77
);
88

9-
Matcher errorOfTypeWithMessage<T extends ValidationError>(String message) =>
10-
predicate(
11-
(ValidationError error) => error is T && message == error.message,
12-
);
13-
149
Iterable<ValidationError> Function(String) createValidator(
1510
Set<ValidationRule> rules,
1611
) =>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import "package:gql/src/validation/validator.dart";
2+
import "package:test/test.dart";
3+
4+
import "./common.dart";
5+
6+
final validate = createValidator({
7+
ValidationRule.uniqueArgumentDefinitionNames,
8+
});
9+
10+
void main() {
11+
group("Unique argument definition names", () {
12+
test("no arguments", () {
13+
expect(
14+
validate(
15+
"""
16+
type SomeObject {
17+
someField: String
18+
}
19+
20+
interface SomeInterface {
21+
someField: String
22+
}
23+
24+
directive @someDirective on QUERY
25+
""",
26+
).map((it) => it.toString()),
27+
equals([]),
28+
);
29+
});
30+
31+
test("one argument", () {
32+
expect(
33+
validate(
34+
"""
35+
type SomeObject {
36+
someField(foo: String): String
37+
}
38+
39+
interface SomeInterface {
40+
someField(foo: String): String
41+
}
42+
43+
extend type SomeObject {
44+
anotherField(foo: String): String
45+
}
46+
47+
extend interface SomeInterface {
48+
anotherField(foo: String): String
49+
}
50+
51+
directive @someDirective(foo: String) on QUERY
52+
""",
53+
).map((it) => it.toString()),
54+
equals([]),
55+
);
56+
});
57+
58+
test("multiple arguments", () {
59+
expect(
60+
validate(
61+
"""
62+
type SomeObject {
63+
someField(
64+
foo: String
65+
bar: String
66+
): String
67+
}
68+
69+
interface SomeInterface {
70+
someField(
71+
foo: String
72+
bar: String
73+
): String
74+
}
75+
76+
extend type SomeObject {
77+
anotherField(
78+
foo: String
79+
bar: String
80+
): String
81+
}
82+
83+
extend interface SomeInterface {
84+
anotherField(
85+
foo: String
86+
bar: String
87+
): String
88+
}
89+
90+
directive @someDirective(
91+
foo: String
92+
bar: String
93+
) on QUERY
94+
""",
95+
).map((it) => it.toString()),
96+
equals([]),
97+
);
98+
});
99+
100+
test("duplicating arguments", () {
101+
expect(
102+
validate(
103+
"""
104+
type SomeObject {
105+
someField(
106+
foo: String
107+
bar: String
108+
foo: String
109+
): String
110+
}
111+
112+
interface SomeInterface {
113+
someField(
114+
foo: String
115+
bar: String
116+
foo: String
117+
): String
118+
}
119+
120+
extend type SomeObject {
121+
anotherField(
122+
foo: String
123+
bar: String
124+
bar: String
125+
): String
126+
}
127+
128+
extend interface SomeInterface {
129+
anotherField(
130+
bar: String
131+
foo: String
132+
foo: String
133+
): String
134+
}
135+
136+
directive @someDirective(
137+
foo: String
138+
bar: String
139+
foo: String
140+
) on QUERY
141+
""",
142+
).map((it) => it.toString()),
143+
equals([
144+
'Argument "SomeObject.someField(foo:)" can only be defined once.',
145+
'Argument "SomeInterface.someField(foo:)" can only be defined once.',
146+
'Argument "SomeObject.anotherField(bar:)" can only be defined once.',
147+
'Argument "SomeInterface.anotherField(foo:)" can only be defined once.',
148+
'Argument "@someDirective(foo:)" can only be defined once.',
149+
]),
150+
);
151+
});
152+
});
153+
}

0 commit comments

Comments
 (0)