onTooManyNumericLiteralCharacters = (maxCharacters, token) -> {
+ SourceLocation sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token);
+ throw new ParseCancelledTooManyNumericLiteralCharactersException(environment.getI18N(), sourceLocation, maxCharacters);
+ };
+ return new SafeTokenSource(
+ lexer,
+ maxTokens,
+ maxWhitespaceTokens,
+ maxNumericLiteralCharacters,
+ onTooManyTokens,
+ onTooManyNumericLiteralCharacters
+ );
}
private void setupParserListener(ParserEnvironment environment, MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) {
diff --git a/src/main/java/graphql/parser/ParserOptions.java b/src/main/java/graphql/parser/ParserOptions.java
index 2e04294427..4243136741 100644
--- a/src/main/java/graphql/parser/ParserOptions.java
+++ b/src/main/java/graphql/parser/ParserOptions.java
@@ -43,6 +43,16 @@ public class ParserOptions {
*/
public static final int MAX_WHITESPACE_TOKENS = 200_000;
+ /**
+ * A numeric literal is represented by a single token, regardless of how many characters it contains. Converting
+ * very large numeric literals into arbitrary precision numbers can consume excessive CPU and memory. To prevent
+ * this for most users, graphql-java limits numeric literals to 100 characters.
+ *
+ * If you want to allow more, then {@link #setDefaultParserOptions(ParserOptions)} allows you to change this
+ * JVM wide.
+ */
+ public static final int MAX_NUMERIC_LITERAL_CHARACTERS = 100;
+
/**
* A graphql hacking vector is to send nonsensical queries that have lots of grammar rule depth to them which
* can cause stack overflow exceptions during the query parsing. To prevent this for most users, graphql-java
@@ -61,6 +71,7 @@ public class ParserOptions {
.maxCharacters(MAX_QUERY_CHARACTERS)
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
+ .maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(MAX_RULE_DEPTH)
.redactTokenParserErrorMessages(false)
.build();
@@ -73,6 +84,7 @@ public class ParserOptions {
.maxCharacters(MAX_QUERY_CHARACTERS)
.maxTokens(MAX_QUERY_TOKENS) // to prevent a billion laughs style attacks, we set a default for graphql-java
.maxWhitespaceTokens(MAX_WHITESPACE_TOKENS)
+ .maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(MAX_RULE_DEPTH)
.redactTokenParserErrorMessages(false)
.build();
@@ -85,6 +97,7 @@ public class ParserOptions {
.maxCharacters(Integer.MAX_VALUE)
.maxTokens(Integer.MAX_VALUE) // we are less worried about a billion laughs with SDL parsing since the call path is not facing attackers
.maxWhitespaceTokens(Integer.MAX_VALUE)
+ .maxNumericLiteralCharacters(MAX_NUMERIC_LITERAL_CHARACTERS)
.maxRuleDepth(Integer.MAX_VALUE)
.redactTokenParserErrorMessages(false)
.build();
@@ -191,6 +204,7 @@ public static void setDefaultSdlParserOptions(ParserOptions options) {
private final int maxCharacters;
private final int maxTokens;
private final int maxWhitespaceTokens;
+ private final int maxNumericLiteralCharacters;
private final int maxRuleDepth;
private final boolean redactTokenParserErrorMessages;
private final ParsingListener parsingListener;
@@ -203,6 +217,7 @@ private ParserOptions(Builder builder) {
this.maxCharacters = builder.maxCharacters;
this.maxTokens = builder.maxTokens;
this.maxWhitespaceTokens = builder.maxWhitespaceTokens;
+ this.maxNumericLiteralCharacters = builder.maxNumericLiteralCharacters;
this.maxRuleDepth = builder.maxRuleDepth;
this.redactTokenParserErrorMessages = builder.redactTokenParserErrorMessages;
this.parsingListener = builder.parsingListener;
@@ -288,6 +303,17 @@ public int getMaxWhitespaceTokens() {
return maxWhitespaceTokens;
}
+ /**
+ * A numeric literal is represented by a single token, regardless of how many characters it contains. Converting
+ * very large numeric literals into arbitrary precision numbers can consume excessive CPU and memory. This limit
+ * stops parsing before that conversion takes place.
+ *
+ * @return the maximum number of characters permitted in an integer or floating-point literal
+ */
+ public int getMaxNumericLiteralCharacters() {
+ return maxNumericLiteralCharacters;
+ }
+
/**
* A graphql hacking vector is to send nonsensical queries that have lots of rule depth to them which
* can cause stack overflow exceptions during the query parsing. To prevent this you can set a value
@@ -333,6 +359,7 @@ public static class Builder {
private int maxCharacters = MAX_QUERY_CHARACTERS;
private int maxTokens = MAX_QUERY_TOKENS;
private int maxWhitespaceTokens = MAX_WHITESPACE_TOKENS;
+ private int maxNumericLiteralCharacters = MAX_NUMERIC_LITERAL_CHARACTERS;
private int maxRuleDepth = MAX_RULE_DEPTH;
private boolean redactTokenParserErrorMessages = false;
@@ -346,6 +373,7 @@ public static class Builder {
this.maxCharacters = parserOptions.maxCharacters;
this.maxTokens = parserOptions.maxTokens;
this.maxWhitespaceTokens = parserOptions.maxWhitespaceTokens;
+ this.maxNumericLiteralCharacters = parserOptions.maxNumericLiteralCharacters;
this.maxRuleDepth = parserOptions.maxRuleDepth;
this.redactTokenParserErrorMessages = parserOptions.redactTokenParserErrorMessages;
this.parsingListener = parserOptions.parsingListener;
@@ -386,6 +414,19 @@ public Builder maxWhitespaceTokens(int maxWhitespaceTokens) {
return this;
}
+ /**
+ * Sets the maximum number of characters permitted in an integer or floating-point literal. Parsing is
+ * cancelled before converting a larger literal into an arbitrary precision number.
+ *
+ * @param maxNumericLiteralCharacters the maximum number of characters permitted in a numeric literal
+ *
+ * @return this builder
+ */
+ public Builder maxNumericLiteralCharacters(int maxNumericLiteralCharacters) {
+ this.maxNumericLiteralCharacters = maxNumericLiteralCharacters;
+ return this;
+ }
+
public Builder maxRuleDepth(int maxRuleDepth) {
this.maxRuleDepth = maxRuleDepth;
return this;
diff --git a/src/main/java/graphql/parser/SafeTokenSource.java b/src/main/java/graphql/parser/SafeTokenSource.java
index c92c76d916..c6f2e50ae4 100644
--- a/src/main/java/graphql/parser/SafeTokenSource.java
+++ b/src/main/java/graphql/parser/SafeTokenSource.java
@@ -1,6 +1,7 @@
package graphql.parser;
import graphql.Internal;
+import graphql.parser.antlr.GraphqlLexer;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenFactory;
@@ -25,14 +26,20 @@ public class SafeTokenSource implements TokenSource {
private final TokenSource lexer;
private final int maxTokens;
private final int maxWhitespaceTokens;
+ private final int maxNumericLiteralCharacters;
private final BiConsumer whenMaxTokensExceeded;
+ private final BiConsumer whenMaxNumericLiteralCharactersExceeded;
private final int channelCounts[];
- public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, BiConsumer whenMaxTokensExceeded) {
+ public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens, int maxNumericLiteralCharacters,
+ BiConsumer whenMaxTokensExceeded,
+ BiConsumer whenMaxNumericLiteralCharactersExceeded) {
this.lexer = lexer;
this.maxTokens = maxTokens;
this.maxWhitespaceTokens = maxWhitespaceTokens;
+ this.maxNumericLiteralCharacters = maxNumericLiteralCharacters;
this.whenMaxTokensExceeded = whenMaxTokensExceeded;
+ this.whenMaxNumericLiteralCharactersExceeded = whenMaxNumericLiteralCharactersExceeded;
// this could be a Map however we want it to be faster as possible.
// we only have 3 channels - but they are 0,2 and 3 so use 5 for safety - still faster than a map get/put
// if we ever add another channel beyond 5 it will IOBEx during tests so future changes will be handled before release!
@@ -44,6 +51,7 @@ public SafeTokenSource(TokenSource lexer, int maxTokens, int maxWhitespaceTokens
public Token nextToken() {
Token token = lexer.nextToken();
if (token != null) {
+ callbackIfNumericLiteralTooLong(token);
int channel = token.getChannel();
int currentCount = ++channelCounts[channel];
if (channel == Parser.CHANNEL_WHITESPACE) {
@@ -56,6 +64,18 @@ public Token nextToken() {
return token;
}
+ private void callbackIfNumericLiteralTooLong(Token token) {
+ int tokenType = token.getType();
+ if (tokenType != GraphqlLexer.IntValue && tokenType != GraphqlLexer.FloatValue) {
+ return;
+ }
+
+ int characterCount = token.getStopIndex() - token.getStartIndex() + 1;
+ if (characterCount > maxNumericLiteralCharacters) {
+ whenMaxNumericLiteralCharactersExceeded.accept(maxNumericLiteralCharacters, token);
+ }
+ }
+
private void callbackIfMaxExceeded(int maxCount, int currentCount, Token token) {
if (currentCount > maxCount) {
whenMaxTokensExceeded.accept(maxCount, token);
diff --git a/src/main/java/graphql/parser/StringValueParsing.java b/src/main/java/graphql/parser/StringValueParsing.java
index 56b1f1ec88..046f4ed8b9 100644
--- a/src/main/java/graphql/parser/StringValueParsing.java
+++ b/src/main/java/graphql/parser/StringValueParsing.java
@@ -6,9 +6,6 @@
import graphql.language.SourceLocation;
import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
/**
* Contains parsing code for the StringValue types in the grammar
@@ -44,45 +41,25 @@ public static String removeIndentation(String rawValue) {
}
}
}
- List lineList = new ArrayList<>(Arrays.asList(lines));
- if (commonIndent != null) {
- for (int i = 0; i < lineList.size(); i++) {
- String line = lineList.get(i);
- if (i == 0) {
- continue;
- }
- if (line.length() > commonIndent) {
- line = line.substring(commonIndent);
- lineList.set(i, line);
- }
- }
+ int firstLine = 0;
+ while (firstLine < lines.length && containsOnlyWhiteSpace(lines[firstLine])) {
+ firstLine++;
}
- while (!lineList.isEmpty()) {
- String line = lineList.get(0);
- if (containsOnlyWhiteSpace(line)) {
- lineList.remove(0);
- } else {
- break;
- }
+ int lastLine = lines.length;
+ while (lastLine > firstLine && containsOnlyWhiteSpace(lines[lastLine - 1])) {
+ lastLine--;
}
- while (!lineList.isEmpty()) {
- int endIndex = lineList.size() - 1;
- String line = lineList.get(endIndex);
- if (containsOnlyWhiteSpace(line)) {
- lineList.remove(endIndex);
- } else {
- break;
+
+ StringBuilder formatted = new StringBuilder(rawValue.length());
+ for (int i = firstLine; i < lastLine; i++) {
+ String line = lines[i];
+ if (commonIndent != null && i > 0 && line.length() > commonIndent) {
+ line = line.substring(commonIndent);
}
- }
- StringBuilder formatted = new StringBuilder();
- for (int i = 0; i < lineList.size(); i++) {
- String line = lineList.get(i);
- if (i == 0) {
- formatted.append(line);
- } else {
- formatted.append("\n");
- formatted.append(line);
+ if (i > firstLine) {
+ formatted.append('\n');
}
+ formatted.append(line);
}
return formatted.toString();
}
diff --git a/src/main/java/graphql/parser/exceptions/ParseCancelledTooManyNumericLiteralCharactersException.java b/src/main/java/graphql/parser/exceptions/ParseCancelledTooManyNumericLiteralCharactersException.java
new file mode 100644
index 0000000000..83a60e4027
--- /dev/null
+++ b/src/main/java/graphql/parser/exceptions/ParseCancelledTooManyNumericLiteralCharactersException.java
@@ -0,0 +1,17 @@
+package graphql.parser.exceptions;
+
+import graphql.Internal;
+import graphql.i18n.I18n;
+import graphql.language.SourceLocation;
+import graphql.parser.InvalidSyntaxException;
+import org.jspecify.annotations.NonNull;
+
+@Internal
+public class ParseCancelledTooManyNumericLiteralCharactersException extends InvalidSyntaxException {
+
+ @Internal
+ public ParseCancelledTooManyNumericLiteralCharactersException(@NonNull I18n i18N, @NonNull SourceLocation sourceLocation, int maxCharacters) {
+ super(i18N.msg("ParseCancelled.tooManyNumericLiteralCharacters", maxCharacters),
+ sourceLocation, null, null, null);
+ }
+}
diff --git a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java
index b9cfce9485..eb13820559 100644
--- a/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java
+++ b/src/main/java/graphql/schema/DataFetchingEnvironmentImpl.java
@@ -458,11 +458,15 @@ public Builder queryDirectives(QueryDirectives queryDirectives) {
return this;
}
- public Builder deferredCallContext(AlternativeCallContext alternativeCallContext) {
+ public Builder alternativeCallContext(AlternativeCallContext alternativeCallContext) {
this.alternativeCallContext = alternativeCallContext;
return this;
}
+ public Builder deferredCallContext(AlternativeCallContext alternativeCallContext) {
+ return alternativeCallContext(alternativeCallContext);
+ }
+
public DataFetchingEnvironment build() {
return new DataFetchingEnvironmentImpl(this);
}
@@ -499,10 +503,14 @@ public DataLoaderDispatchStrategy getDataLoaderDispatchStrategy() {
return dataLoaderDispatchStrategy;
}
- public AlternativeCallContext getDeferredCallContext() {
+ public AlternativeCallContext getAlternativeCallContext() {
return alternativeCallContext;
}
+ public AlternativeCallContext getDeferredCallContext() {
+ return getAlternativeCallContext();
+ }
+
public Profiler getProfiler() {
return profiler;
}
diff --git a/src/main/java/graphql/schema/DataLoaderWithContext.java b/src/main/java/graphql/schema/DataLoaderWithContext.java
index 3d4224b364..af4f74c693 100644
--- a/src/main/java/graphql/schema/DataLoaderWithContext.java
+++ b/src/main/java/graphql/schema/DataLoaderWithContext.java
@@ -68,11 +68,11 @@ private void newDataLoaderInvocation() {
DataFetchingEnvironmentImpl dfeImpl = (DataFetchingEnvironmentImpl) dfe;
DataFetchingEnvironmentImpl.DFEInternalState dfeInternalState = (DataFetchingEnvironmentImpl.DFEInternalState) dfeImpl.toInternal();
if (dfeInternalState.getDataLoaderDispatchStrategy() instanceof PerLevelDataLoaderDispatchStrategy) {
- AlternativeCallContext alternativeCallContext = dfeInternalState.getDeferredCallContext();
+ AlternativeCallContext alternativeCallContext = dfeInternalState.getAlternativeCallContext();
int level = dfeImpl.getLevel();
((PerLevelDataLoaderDispatchStrategy) dfeInternalState.dataLoaderDispatchStrategy).newDataLoaderInvocation(level, delegate, alternativeCallContext);
} else if (dfeInternalState.getDataLoaderDispatchStrategy() instanceof ExhaustedDataLoaderDispatchStrategy) {
- AlternativeCallContext alternativeCallContext = dfeInternalState.getDeferredCallContext();
+ AlternativeCallContext alternativeCallContext = dfeInternalState.getAlternativeCallContext();
((ExhaustedDataLoaderDispatchStrategy) dfeInternalState.dataLoaderDispatchStrategy).newDataLoaderInvocation(alternativeCallContext);
}
}
diff --git a/src/main/java/graphql/schema/idl/SchemaTypeDirectivesChecker.java b/src/main/java/graphql/schema/idl/SchemaTypeDirectivesChecker.java
index 4c3e373e37..18011537ff 100644
--- a/src/main/java/graphql/schema/idl/SchemaTypeDirectivesChecker.java
+++ b/src/main/java/graphql/schema/idl/SchemaTypeDirectivesChecker.java
@@ -30,11 +30,16 @@
import graphql.schema.idl.errors.MissingTypeError;
import graphql.schema.idl.errors.NotAnInputTypeError;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import static graphql.Assert.assertNotNull;
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
import static graphql.introspection.Introspection.DirectiveLocation.ENUM;
import static graphql.introspection.Introspection.DirectiveLocation.ENUM_VALUE;
@@ -182,6 +187,9 @@ private static boolean isNoNullArgWithoutDefaultValue(InputValueDefinition defin
}
private void commonCheck(Collection directiveDefinitions, List errors) {
+ List directiveDefinitionsList = new ArrayList<>(directiveDefinitions);
+ Map directiveDefinitionsByName = getByName(directiveDefinitionsList, DirectiveDefinition::getName, mergeFirst());
+
directiveDefinitions.forEach(directiveDefinition -> {
assertTypeName(directiveDefinition, errors);
directiveDefinition.getInputValueDefinitions().forEach(inputValueDefinition -> {
@@ -192,6 +200,73 @@ private void commonCheck(Collection directiveDefinitions, L
}
});
});
+ checkIndirectDirectiveCycles(directiveDefinitionsByName, errors);
+ }
+
+ private static Map directiveReferences(DirectiveDefinition directiveDefinition) {
+ Map result = new LinkedHashMap<>();
+ for (InputValueDefinition inputValueDefinition : directiveDefinition.getInputValueDefinitions()) {
+ recordDirectiveReferences(directiveDefinition, result, inputValueDefinition);
+ }
+ return result;
+ }
+
+ private static void recordDirectiveReferences(DirectiveDefinition directiveDefinition,
+ Map result,
+ InputValueDefinition inputValueDefinition) {
+ for (Directive directive : inputValueDefinition.getDirectives()) {
+ if (directive.getName().equals(directiveDefinition.getName())) {
+ continue;
+ }
+ result.putIfAbsent(directive.getName(), inputValueDefinition);
+ }
+ }
+
+ private static void checkIndirectDirectiveCycles(
+ Map directiveDefinitionsByName,
+ List errors) {
+ Set checked = new LinkedHashSet<>();
+ // Insertion order records the active recursion path for the error message.
+ LinkedHashSet currentPath = new LinkedHashSet<>();
+ for (DirectiveDefinition directiveDefinition : directiveDefinitionsByName.values()) {
+ checkDirectiveReferencesForCycles(directiveDefinition, directiveDefinitionsByName, checked, currentPath, errors);
+ }
+ }
+
+ private static void checkDirectiveReferencesForCycles(DirectiveDefinition directiveDefinition,
+ Map directiveDefinitionsByName,
+ Set checked,
+ LinkedHashSet currentPath,
+ List errors) {
+ String directiveName = directiveDefinition.getName();
+ if (checked.contains(directiveName)) {
+ return;
+ }
+
+ currentPath.add(directiveName);
+ for (Map.Entry reference : directiveReferences(directiveDefinition).entrySet()) {
+ String referencedDirectiveName = reference.getKey();
+ if (currentPath.contains(referencedDirectiveName)) {
+ DirectiveDefinition repeatedDirective = assertNotNull(directiveDefinitionsByName.get(referencedDirectiveName));
+ String cyclePath = directiveCyclePath(referencedDirectiveName, currentPath);
+ errors.add(new DirectiveIllegalReferenceError(repeatedDirective, reference.getValue(), cyclePath));
+ continue;
+ }
+ DirectiveDefinition referencedDirective = directiveDefinitionsByName.get(referencedDirectiveName);
+ if (referencedDirective != null) {
+ checkDirectiveReferencesForCycles(referencedDirective, directiveDefinitionsByName, checked, currentPath, errors);
+ }
+ }
+ currentPath.remove(directiveName);
+ checked.add(directiveName);
+ }
+
+ private static String directiveCyclePath(String repeatedDirectiveName, LinkedHashSet currentPath) {
+ List pathList = new ArrayList<>(currentPath);
+ int cycleStart = pathList.indexOf(repeatedDirectiveName);
+ List cyclePath = new ArrayList<>(pathList.subList(cycleStart, pathList.size()));
+ cyclePath.add(repeatedDirectiveName);
+ return String.join(" -> ", cyclePath);
}
private static void assertTypeName(NamedNode> node, List errors) {
@@ -224,4 +299,4 @@ private static TypeDefinition> findTypeDefFromRegistry(String typeName, TypeDe
}
return typeRegistry.scalars().get(typeName);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/graphql/schema/idl/SchemaTypeExtensionsChecker.java b/src/main/java/graphql/schema/idl/SchemaTypeExtensionsChecker.java
index c80bdcce01..0003a4f100 100644
--- a/src/main/java/graphql/schema/idl/SchemaTypeExtensionsChecker.java
+++ b/src/main/java/graphql/schema/idl/SchemaTypeExtensionsChecker.java
@@ -16,6 +16,7 @@
import graphql.language.TypeDefinition;
import graphql.language.TypeName;
import graphql.language.UnionTypeDefinition;
+import graphql.language.UnionTypeExtensionDefinition;
import graphql.schema.idl.errors.MissingTypeError;
import graphql.schema.idl.errors.NonUniqueArgumentError;
import graphql.schema.idl.errors.NonUniqueNameError;
@@ -158,26 +159,64 @@ private void checkUnionTypeExtensions(List errors, TypeDefinitionR
typeRegistry.unionTypeExtensions()
.forEach((name, extensions) -> {
checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, UnionTypeDefinition.class);
+ Set previousMemberTypes = unionMemberTypes(typeRegistry, name);
- extensions.forEach(extension -> {
- List memberTypes = extension.getMemberTypes().stream()
- .map(t -> TypeInfo.typeInfo(t).getTypeName()).collect(Collectors.toList());
-
- checkNamedUniqueness(errors, memberTypes, TypeName::getName,
- (namedMember, memberType) -> new NonUniqueNameError(extension, namedMember));
-
- memberTypes.forEach(
- memberType -> {
- ObjectTypeDefinition unionTypeDefinition = typeRegistry.getTypeOrNull(memberType, ObjectTypeDefinition.class);
- if (unionTypeDefinition == null) {
- errors.add(new MissingTypeError("union member", extension, memberType));
- }
- }
- );
- });
+ extensions.forEach(extension -> checkUnionTypeExtension(errors, typeRegistry, previousMemberTypes, extension));
});
}
+ private void checkUnionTypeExtension(List errors, TypeDefinitionRegistry typeRegistry, Set previousMemberTypes, UnionTypeExtensionDefinition extension) {
+ List memberTypes = extension.getMemberTypes().stream()
+ .map(t -> TypeInfo.typeInfo(t).getTypeName()).collect(Collectors.toList());
+
+ checkNamedUniqueness(errors, memberTypes, TypeName::getName,
+ (namedMember, memberType) -> new NonUniqueNameError(extension, namedMember));
+
+ memberTypes.forEach(memberType -> checkUnionMemberTypeExists(errors, typeRegistry, extension, memberType));
+ checkUnionMemberTypesAreNew(errors, previousMemberTypes, extension, memberTypes);
+ }
+
+ private void checkUnionMemberTypeExists(List errors, TypeDefinitionRegistry typeRegistry, UnionTypeExtensionDefinition extension, TypeName memberType) {
+ ObjectTypeDefinition unionTypeDefinition = typeRegistry.getTypeOrNull(memberType, ObjectTypeDefinition.class);
+ if (unionTypeDefinition != null) {
+ return;
+ }
+ errors.add(new MissingTypeError("union member", extension, memberType));
+ }
+
+ private void checkUnionMemberTypesAreNew(List errors, Set previousMemberTypes, UnionTypeExtensionDefinition extension, List memberTypes) {
+ Set duplicateMemberTypes = duplicateMemberTypes(memberTypes);
+ memberTypes.stream()
+ .filter(memberType -> !duplicateMemberTypes.contains(memberType.getName()))
+ .filter(memberType -> previousMemberTypes.contains(memberType.getName()))
+ .forEach(memberType -> errors.add(new NonUniqueNameError(extension, memberType.getName())));
+
+ memberTypes.forEach(memberType -> previousMemberTypes.add(memberType.getName()));
+ }
+
+ private Set duplicateMemberTypes(List memberTypes) {
+ Set seen = new HashSet<>();
+ Set duplicates = new HashSet<>();
+ memberTypes.forEach(memberType -> {
+ if (!seen.add(memberType.getName())) {
+ duplicates.add(memberType.getName());
+ }
+ });
+ return duplicates;
+ }
+
+ private Set unionMemberTypes(TypeDefinitionRegistry typeRegistry, String name) {
+ Set memberTypes = new HashSet<>();
+ UnionTypeDefinition baseTypeDef = typeRegistry.getTypeOrNull(name, UnionTypeDefinition.class);
+ if (baseTypeDef == null) {
+ return memberTypes;
+ }
+ baseTypeDef.getMemberTypes().stream()
+ .map(t -> TypeInfo.typeInfo(t).getTypeName().getName())
+ .forEach(memberTypes::add);
+ return memberTypes;
+ }
+
/*
* Enum type extensions have the potential to be invalid if incorrectly defined.
*
diff --git a/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java b/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java
index 37bd404e4e..996bcb5c2b 100644
--- a/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java
+++ b/src/main/java/graphql/schema/idl/TypeDefinitionRegistry.java
@@ -37,9 +37,9 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
+import java.util.stream.Stream;
import static graphql.Assert.assertNotNull;
import static graphql.schema.idl.SchemaExtensionsChecker.defineOperationDefs;
@@ -719,18 +719,7 @@ public Map getTypesMap(Class targetClas
public List getAllImplementationsOf(InterfaceTypeDefinition targetInterface) {
return ImmutableKit.filter(
getTypes(ImplementingTypeDefinition.class),
- implementingTypeDefinition -> {
- List> implementsList = implementingTypeDefinition.getImplements();
- for (Type iFace : implementsList) {
- InterfaceTypeDefinition interfaceTypeDef = getTypeOrNull(iFace, InterfaceTypeDefinition.class);
- if (interfaceTypeDef != null) {
- if (interfaceTypeDef.getName().equals(targetInterface.getName())) {
- return true;
- }
- }
- }
- return false;
- });
+ implementingTypeDefinition -> implementsInterface(implementingTypeDefinition, targetInterface));
}
/**
@@ -765,37 +754,42 @@ public boolean isPossibleType(Type abstractType, Type possibleType) {
if (!isObjectTypeOrInterface(possibleType)) {
return false;
}
- TypeDefinition targetObjectTypeDef = Objects.requireNonNull(getTypeOrNull(possibleType));
- TypeDefinition abstractTypeDef = Objects.requireNonNull(getTypeOrNull(abstractType));
+ TypeDefinition possibleTypeDef = assertNotNull(getTypeOrNull(possibleType));
+ TypeDefinition abstractTypeDef = assertNotNull(getTypeOrNull(abstractType));
if (abstractTypeDef instanceof UnionTypeDefinition) {
- List memberTypes = ((UnionTypeDefinition) abstractTypeDef).getMemberTypes();
- for (Type memberType : memberTypes) {
- ObjectTypeDefinition checkType = getTypeOrNull(memberType, ObjectTypeDefinition.class);
- if (checkType != null) {
- if (checkType.getName().equals(targetObjectTypeDef.getName())) {
- return true;
- }
- }
- }
- return false;
- } else {
- InterfaceTypeDefinition iFace = (InterfaceTypeDefinition) abstractTypeDef;
- for (TypeDefinition> t : types.values()) {
- if (t instanceof ImplementingTypeDefinition) {
- if (t.getName().equals(targetObjectTypeDef.getName())) {
- ImplementingTypeDefinition> itd = (ImplementingTypeDefinition>) t;
-
- for (Type implementsType : itd.getImplements()) {
- TypeDefinition> matchingInterface = types.get(typeName(implementsType));
- if (matchingInterface != null && matchingInterface.getName().equals(iFace.getName())) {
- return true;
- }
- }
- }
- }
- }
- return false;
+ return isUnionMember((UnionTypeDefinition) abstractTypeDef, possibleTypeDef);
+ }
+ return implementsInterface(
+ (ImplementingTypeDefinition>) possibleTypeDef,
+ (InterfaceTypeDefinition) abstractTypeDef);
+ }
+
+ private boolean implementsInterface(
+ ImplementingTypeDefinition> implementingType,
+ InterfaceTypeDefinition targetInterface) {
+ return Stream.concat(
+ Stream.of(implementingType),
+ getImplementingTypeExtensions(implementingType).stream())
+ .flatMap(type -> type.getImplements().stream())
+ .map(TypeInfo::typeName)
+ .anyMatch(targetInterface.getName()::equals);
+ }
+
+ private List extends ImplementingTypeDefinition>> getImplementingTypeExtensions(
+ ImplementingTypeDefinition> implementingType) {
+ if (implementingType instanceof InterfaceTypeDefinition) {
+ return interfaceTypeExtensions.getOrDefault(implementingType.getName(), List.of());
}
+ return objectTypeExtensions.getOrDefault(implementingType.getName(), List.of());
+ }
+
+ private boolean isUnionMember(UnionTypeDefinition unionType, TypeDefinition> possibleType) {
+ return Stream.concat(
+ unionType.getMemberTypes().stream(),
+ unionTypeExtensions.getOrDefault(unionType.getName(), List.of()).stream()
+ .flatMap(extension -> extension.getMemberTypes().stream()))
+ .map(TypeInfo::typeName)
+ .anyMatch(possibleType.getName()::equals);
}
/**
diff --git a/src/main/java/graphql/schema/idl/UnionTypesChecker.java b/src/main/java/graphql/schema/idl/UnionTypesChecker.java
index f2134b2d54..dbaba5e05d 100644
--- a/src/main/java/graphql/schema/idl/UnionTypesChecker.java
+++ b/src/main/java/graphql/schema/idl/UnionTypesChecker.java
@@ -13,9 +13,9 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Stream;
import static java.lang.String.format;
+import static java.util.Collections.emptyList;
/**
* UnionType check, details in https://spec.graphql.org/June2018/#sec-Type-System.
@@ -33,18 +33,15 @@ class UnionTypesChecker {
void checkUnionType(List errors, TypeDefinitionRegistry typeRegistry) {
List unionTypes = typeRegistry.getTypes(UnionTypeDefinition.class);
- List unionTypeExtensions = typeRegistry.getTypes(UnionTypeExtensionDefinition.class);
- Stream.concat(unionTypes.stream(), unionTypeExtensions.stream())
- .forEach(type -> checkUnionType(typeRegistry, type, errors));
+ unionTypes.forEach(type -> checkUnionType(typeRegistry, type, errors));
}
private void checkUnionType(TypeDefinitionRegistry typeRegistry, UnionTypeDefinition unionTypeDefinition, List errors) {
assertTypeName(unionTypeDefinition, errors);
- //noinspection rawtypes
List memberTypes = unionTypeDefinition.getMemberTypes();
- if (memberTypes == null || memberTypes.isEmpty()) {
+ if (!hasMemberTypes(typeRegistry, unionTypeDefinition)) {
errors.add(new UnionTypeError(unionTypeDefinition, format("Union type '%s' must include one or more member types.", unionTypeDefinition.getName())));
return;
}
@@ -66,6 +63,16 @@ private void checkUnionType(TypeDefinitionRegistry typeRegistry, UnionTypeDefini
}
}
+ private boolean hasMemberTypes(TypeDefinitionRegistry typeRegistry, UnionTypeDefinition unionTypeDefinition) {
+ if (!unionTypeDefinition.getMemberTypes().isEmpty()) {
+ return true;
+ }
+
+ List extensions = typeRegistry.unionTypeExtensions()
+ .getOrDefault(unionTypeDefinition.getName(), emptyList());
+ return extensions.stream().anyMatch(extension -> !extension.getMemberTypes().isEmpty());
+ }
+
private void assertTypeName(UnionTypeDefinition unionTypeDefinition, List errors) {
if (unionTypeDefinition.getName().length() >= 2 && unionTypeDefinition.getName().startsWith("__")) {
errors.add((new UnionTypeError(unionTypeDefinition, String.format("'%s' must not begin with '__', which is reserved by GraphQL introspection.", unionTypeDefinition.getName()))));
diff --git a/src/main/java/graphql/schema/idl/errors/DirectiveIllegalReferenceError.java b/src/main/java/graphql/schema/idl/errors/DirectiveIllegalReferenceError.java
index 44fb541e53..2bcd34db39 100644
--- a/src/main/java/graphql/schema/idl/errors/DirectiveIllegalReferenceError.java
+++ b/src/main/java/graphql/schema/idl/errors/DirectiveIllegalReferenceError.java
@@ -12,4 +12,11 @@ public DirectiveIllegalReferenceError(DirectiveDefinition directive, NamedNode l
directive.getName(), location.getName(), lineCol(location)
));
}
-}
\ No newline at end of file
+
+ public DirectiveIllegalReferenceError(DirectiveDefinition directive, NamedNode location, String cyclePath) {
+ super(directive,
+ String.format("'%s' must not reference itself via directive cycle '%s' on '%s''%s'",
+ directive.getName(), cyclePath, location.getName(), lineCol(location)
+ ));
+ }
+}
diff --git a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java
index 0ce7026426..d1ab9d1d45 100644
--- a/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java
+++ b/src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java
@@ -27,7 +27,6 @@
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
-import java.util.function.Supplier;
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
import static graphql.util.TraversalControl.CONTINUE;
@@ -43,8 +42,9 @@
* themselves are not collected - only concrete type instances are stored in the result map.
*
* Because type references are not followed, this visitor also tracks "indirect strong references"
- * - types that are directly referenced (not via type reference) by fields, arguments, and input
- * fields. This handles edge cases where schema transformations replace type references with
+ * - types that are directly referenced (not via type reference) by fields, arguments,
+ * input fields, implemented interfaces, and union members. This handles edge cases where
+ * schema transformations replace type references with
* actual types, which would otherwise be missed during traversal.
*
* @see SchemaUtil#visitPartiallySchema
@@ -77,6 +77,7 @@ public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, Traverser
public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext context) {
assertTypeUniqueness(node, result);
save(node.getName(), node);
+ saveIndirectStrongReferences(node.getInterfaces());
return CONTINUE;
}
@@ -91,6 +92,7 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node,
public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, TraverserContext context) {
assertTypeUniqueness(node, result);
save(node.getName(), node);
+ saveIndirectStrongReferences(node.getInterfaces());
return CONTINUE;
}
@@ -98,40 +100,47 @@ public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, Tra
public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserContext