Skip to content

Commit b19be22

Browse files
committed
Validate directive definition cycles
1 parent 3e15aed commit b19be22

9 files changed

Lines changed: 630 additions & 9 deletions

src/main/java/graphql/schema/idl/SchemaTypeDirectivesChecker.java

Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
import graphql.language.Directive;
88
import graphql.language.DirectiveDefinition;
99
import graphql.language.EnumTypeDefinition;
10+
import graphql.language.EnumTypeExtensionDefinition;
1011
import graphql.language.EnumValueDefinition;
1112
import graphql.language.FieldDefinition;
1213
import graphql.language.InputObjectTypeDefinition;
14+
import graphql.language.InputObjectTypeExtensionDefinition;
1315
import graphql.language.InputValueDefinition;
1416
import graphql.language.InterfaceTypeDefinition;
1517
import graphql.language.NamedNode;
1618
import graphql.language.Node;
1719
import graphql.language.NonNullType;
1820
import graphql.language.ObjectTypeDefinition;
1921
import graphql.language.ScalarTypeDefinition;
22+
import graphql.language.ScalarTypeExtensionDefinition;
2023
import graphql.language.SchemaDefinition;
2124
import graphql.language.TypeDefinition;
2225
import graphql.language.TypeName;
@@ -30,10 +33,14 @@
3033
import graphql.schema.idl.errors.MissingTypeError;
3134
import graphql.schema.idl.errors.NotAnInputTypeError;
3235

36+
import java.util.ArrayList;
3337
import java.util.Collection;
38+
import java.util.Collections;
39+
import java.util.LinkedHashSet;
3440
import java.util.List;
3541
import java.util.Map;
3642
import java.util.Optional;
43+
import java.util.Set;
3744

3845
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
3946
import static graphql.introspection.Introspection.DirectiveLocation.ENUM;
@@ -184,16 +191,205 @@ private static boolean isNoNullArgWithoutDefaultValue(InputValueDefinition defin
184191
private void commonCheck(Collection<DirectiveDefinition> directiveDefinitions, List<GraphQLError> errors) {
185192
directiveDefinitions.forEach(directiveDefinition -> {
186193
assertTypeName(directiveDefinition, errors);
187-
directiveDefinition.getInputValueDefinitions().forEach(inputValueDefinition -> {
194+
boolean hasDirectSelfReference = false;
195+
for (InputValueDefinition inputValueDefinition : directiveDefinition.getInputValueDefinitions()) {
188196
assertTypeName(inputValueDefinition, errors);
189197
assertExistAndIsInputType(inputValueDefinition, errors);
190198
if (inputValueDefinition.hasDirective(directiveDefinition.getName())) {
191199
errors.add(new DirectiveIllegalReferenceError(directiveDefinition, inputValueDefinition));
200+
hasDirectSelfReference = true;
192201
}
193-
});
202+
}
203+
if (hasDirectSelfReference) {
204+
return;
205+
}
206+
207+
String cycle = findDirectiveCycle(directiveDefinition);
208+
if (cycle != null) {
209+
errors.add(new DirectiveIllegalReferenceError(directiveDefinition, cycle));
210+
}
194211
});
195212
}
196213

214+
private String findDirectiveCycle(DirectiveDefinition directiveDefinition) {
215+
List<String> path = new ArrayList<>();
216+
path.add(directiveName(directiveDefinition.getName()));
217+
218+
Set<String> visited = new LinkedHashSet<>();
219+
visited.add(directiveName(directiveDefinition.getName()));
220+
221+
return findCycleFromInputValueDefinitions(directiveDefinition.getInputValueDefinitions(), directiveDefinition.getName(), path, visited);
222+
}
223+
224+
private String findCycleFromInputValueDefinitions(List<InputValueDefinition> inputValueDefinitions, String startDirectiveName, List<String> path, Set<String> visited) {
225+
for (InputValueDefinition inputValueDefinition : inputValueDefinitions) {
226+
String cycle = findCycleFromDirectives(inputValueDefinition.getDirectives(), startDirectiveName, path, visited);
227+
if (cycle != null) {
228+
return cycle;
229+
}
230+
231+
cycle = findCycleFromInputType(inputValueDefinition.getType(), startDirectiveName, path, visited);
232+
if (cycle != null) {
233+
return cycle;
234+
}
235+
}
236+
return null;
237+
}
238+
239+
private String findCycleFromDirectives(List<Directive> directives, String startDirectiveName, List<String> path, Set<String> visited) {
240+
for (Directive directive : directives) {
241+
String cycle = findCycleFromDirectiveReference(directive.getName(), startDirectiveName, path, visited);
242+
if (cycle != null) {
243+
return cycle;
244+
}
245+
}
246+
return null;
247+
}
248+
249+
private String findCycleFromDirectiveReference(String directiveName, String startDirectiveName, List<String> path, Set<String> visited) {
250+
String displayName = directiveName(directiveName);
251+
if (directiveName.equals(startDirectiveName)) {
252+
return cyclePath(path, displayName);
253+
}
254+
if (visited.contains(displayName)) {
255+
return null;
256+
}
257+
258+
Optional<DirectiveDefinition> directiveDefinition = typeRegistry.getDirectiveDefinition(directiveName);
259+
if (directiveDefinition.isEmpty()) {
260+
return null;
261+
}
262+
263+
return findCycleFromInputValueDefinitions(
264+
directiveDefinition.get().getInputValueDefinitions(),
265+
startDirectiveName,
266+
addToPath(path, displayName),
267+
addToVisited(visited, displayName)
268+
);
269+
}
270+
271+
private String findCycleFromInputType(graphql.language.Type<?> type, String startDirectiveName, List<String> path, Set<String> visited) {
272+
TypeDefinition<?> typeDefinition = findTypeDefFromRegistry(TypeUtil.unwrapAll(type).getName(), typeRegistry);
273+
if (typeDefinition == null) {
274+
return null;
275+
}
276+
if (visited.contains(typeDefinition.getName())) {
277+
return null;
278+
}
279+
280+
List<String> nextPath = addToPath(path, typeDefinition.getName());
281+
Set<String> nextVisited = addToVisited(visited, typeDefinition.getName());
282+
283+
if (typeDefinition instanceof ScalarTypeDefinition) {
284+
return findCycleFromScalarType((ScalarTypeDefinition) typeDefinition, startDirectiveName, nextPath, nextVisited);
285+
}
286+
if (typeDefinition instanceof EnumTypeDefinition) {
287+
return findCycleFromEnumType((EnumTypeDefinition) typeDefinition, startDirectiveName, nextPath, nextVisited);
288+
}
289+
if (typeDefinition instanceof InputObjectTypeDefinition) {
290+
return findCycleFromInputObjectType((InputObjectTypeDefinition) typeDefinition, startDirectiveName, nextPath, nextVisited);
291+
}
292+
return null;
293+
}
294+
295+
private String findCycleFromScalarType(ScalarTypeDefinition typeDefinition, String startDirectiveName, List<String> path, Set<String> visited) {
296+
String cycle = findCycleFromDirectives(typeDefinition.getDirectives(), startDirectiveName, path, visited);
297+
if (cycle != null) {
298+
return cycle;
299+
}
300+
301+
List<ScalarTypeExtensionDefinition> extensions = typeRegistry.scalarTypeExtensions().getOrDefault(typeDefinition.getName(), Collections.emptyList());
302+
for (ScalarTypeExtensionDefinition extension : extensions) {
303+
cycle = findCycleFromDirectives(extension.getDirectives(), startDirectiveName, path, visited);
304+
if (cycle != null) {
305+
return cycle;
306+
}
307+
}
308+
return null;
309+
}
310+
311+
private String findCycleFromEnumType(EnumTypeDefinition typeDefinition, String startDirectiveName, List<String> path, Set<String> visited) {
312+
String cycle = findCycleFromDirectives(typeDefinition.getDirectives(), startDirectiveName, path, visited);
313+
if (cycle != null) {
314+
return cycle;
315+
}
316+
317+
cycle = findCycleFromEnumValues(typeDefinition.getEnumValueDefinitions(), startDirectiveName, path, visited);
318+
if (cycle != null) {
319+
return cycle;
320+
}
321+
322+
List<EnumTypeExtensionDefinition> extensions = typeRegistry.enumTypeExtensions().getOrDefault(typeDefinition.getName(), Collections.emptyList());
323+
for (EnumTypeExtensionDefinition extension : extensions) {
324+
cycle = findCycleFromDirectives(extension.getDirectives(), startDirectiveName, path, visited);
325+
if (cycle != null) {
326+
return cycle;
327+
}
328+
329+
cycle = findCycleFromEnumValues(extension.getEnumValueDefinitions(), startDirectiveName, path, visited);
330+
if (cycle != null) {
331+
return cycle;
332+
}
333+
}
334+
return null;
335+
}
336+
337+
private String findCycleFromEnumValues(List<EnumValueDefinition> enumValueDefinitions, String startDirectiveName, List<String> path, Set<String> visited) {
338+
for (EnumValueDefinition enumValueDefinition : enumValueDefinitions) {
339+
String cycle = findCycleFromDirectives(enumValueDefinition.getDirectives(), startDirectiveName, path, visited);
340+
if (cycle != null) {
341+
return cycle;
342+
}
343+
}
344+
return null;
345+
}
346+
347+
private String findCycleFromInputObjectType(InputObjectTypeDefinition typeDefinition, String startDirectiveName, List<String> path, Set<String> visited) {
348+
String cycle = findCycleFromDirectives(typeDefinition.getDirectives(), startDirectiveName, path, visited);
349+
if (cycle != null) {
350+
return cycle;
351+
}
352+
353+
cycle = findCycleFromInputValueDefinitions(typeDefinition.getInputValueDefinitions(), startDirectiveName, path, visited);
354+
if (cycle != null) {
355+
return cycle;
356+
}
357+
358+
List<InputObjectTypeExtensionDefinition> extensions = typeRegistry.inputObjectTypeExtensions().getOrDefault(typeDefinition.getName(), Collections.emptyList());
359+
for (InputObjectTypeExtensionDefinition extension : extensions) {
360+
cycle = findCycleFromDirectives(extension.getDirectives(), startDirectiveName, path, visited);
361+
if (cycle != null) {
362+
return cycle;
363+
}
364+
365+
cycle = findCycleFromInputValueDefinitions(extension.getInputValueDefinitions(), startDirectiveName, path, visited);
366+
if (cycle != null) {
367+
return cycle;
368+
}
369+
}
370+
return null;
371+
}
372+
373+
private List<String> addToPath(List<String> path, String element) {
374+
List<String> nextPath = new ArrayList<>(path);
375+
nextPath.add(element);
376+
return nextPath;
377+
}
378+
379+
private Set<String> addToVisited(Set<String> visited, String element) {
380+
Set<String> nextVisited = new LinkedHashSet<>(visited);
381+
nextVisited.add(element);
382+
return nextVisited;
383+
}
384+
385+
private String cyclePath(List<String> path, String cycleElement) {
386+
return String.join(" -> ", addToPath(path, cycleElement));
387+
}
388+
389+
private String directiveName(String directiveName) {
390+
return "@" + directiveName;
391+
}
392+
197393
private static void assertTypeName(NamedNode<?> node, List<GraphQLError> errors) {
198394
if (node.getName().length() >= 2 && node.getName().startsWith("__")) {
199395
errors.add((new IllegalNameError(node)));
@@ -224,4 +420,4 @@ private static TypeDefinition<?> findTypeDefFromRegistry(String typeName, TypeDe
224420
}
225421
return typeRegistry.scalars().get(typeName);
226422
}
227-
}
423+
}

src/main/java/graphql/schema/idl/errors/DirectiveIllegalReferenceError.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@ public DirectiveIllegalReferenceError(DirectiveDefinition directive, NamedNode l
1212
directive.getName(), location.getName(), lineCol(location)
1313
));
1414
}
15-
}
15+
16+
public DirectiveIllegalReferenceError(DirectiveDefinition directive, String cycle) {
17+
super(directive,
18+
String.format("'%s' must not form a circular reference: %s",
19+
directive.getName(), cycle
20+
));
21+
}
22+
}

0 commit comments

Comments
 (0)