Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/graphql/validation/OperationValidationRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
* <li>{@link #UNIQUE_OBJECT_FIELD_NAME} - input object fields are unique</li>
* <li>{@link #DEFER_DIRECTIVE_LABEL} - defer labels are unique strings</li>
* <li>{@link #KNOWN_OPERATION_TYPES} - schema supports the operation type</li>
* <li>{@link #VARIABLES_NOT_ALLOWED_IN_DIRECTIVES_ON_VARIABLE_DEFINITIONS} - variable references not allowed in constant directive positions</li>
* </ul>
*
* <h3>Operation-Scoped Rules</h3>
Expand Down Expand Up @@ -184,4 +185,7 @@ public enum OperationValidationRule {

/** Defer directive must not be used in subscription operations. Requires operation context. */
DEFER_DIRECTIVE_ON_VALID_OPERATION,

/** Variable references must not appear in directive arguments on variable definitions (constant context). */
VARIABLES_NOT_ALLOWED_IN_DIRECTIVES_ON_VARIABLE_DEFINITIONS,
}
39 changes: 37 additions & 2 deletions src/main/java/graphql/validation/OperationValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
import static graphql.validation.ValidationErrorType.UnknownType;
import static graphql.validation.ValidationErrorType.UnusedFragment;
import static graphql.validation.ValidationErrorType.UnusedVariable;
import static graphql.validation.ValidationErrorType.VariableNotAllowed;
import static graphql.validation.ValidationErrorType.VariableTypeMismatch;
import static graphql.validation.ValidationErrorType.WrongType;
import static java.lang.System.arraycopy;
Expand Down Expand Up @@ -413,7 +414,7 @@ public void enter(Node node, List<Node> ancestors) {
} else if (node instanceof OperationDefinition) {
checkOperationDefinition((OperationDefinition) node);
} else if (node instanceof VariableReference) {
checkVariable((VariableReference) node);
checkVariable((VariableReference) node, ancestors);
} else if (node instanceof SelectionSet) {
checkSelectionSet();
} else if (node instanceof ObjectValue) {
Expand Down Expand Up @@ -685,7 +686,12 @@ private void checkOperationDefinition(OperationDefinition operationDefinition) {
}
}

private void checkVariable(VariableReference variableReference) {
private void checkVariable(VariableReference variableReference, List<Node> ancestors) {
if (shouldRunDocumentLevelRules()) {
if (isRuleEnabled(OperationValidationRule.VARIABLES_NOT_ALLOWED_IN_DIRECTIVES_ON_VARIABLE_DEFINITIONS)) {
validateVariableNotAllowedInConstantDirective(variableReference, ancestors);
}
}
if (shouldRunOperationScopedRules()) {
if (isRuleEnabled(OperationValidationRule.NO_UNDEFINED_VARIABLES)) {
validateNoUndefinedVariables(variableReference);
Expand Down Expand Up @@ -980,6 +986,35 @@ private void validateNoUndefinedVariables(VariableReference variableReference) {
}
}

// --- VariablesNotAllowedInDirectivesOnVariableDefinitions ---
/**
* Per the GraphQL spec, directives applied to variable definitions accept only constant values.
* Variable references like {@code $v} are not allowed in directive arguments on variable definitions.
*
* <p>For example, {@code query ($v:Int @dir(arg:$v)) { ... }} is invalid because {@code $v}
* is used in a directive argument on a variable definition.
*/
private void validateVariableNotAllowedInConstantDirective(VariableReference variableReference, List<Node> ancestors) {
// Walk the ancestor list to check if this variable reference is inside a directive
// that is applied to a variable definition.
// The ancestor path would be: ... > VariableDefinition > Directive > Argument > [ObjectValue > ObjectField >]* VariableReference
boolean inDirective = false;
for (int i = ancestors.size() - 1; i >= 0; i--) {
Node ancestor = ancestors.get(i);
if (ancestor instanceof Directive) {
inDirective = true;
} else if (ancestor instanceof VariableDefinition) {
if (inDirective) {
String message = i18n(VariableNotAllowed, "VariableNotAllowedInConstantDirective.variableNotAllowed", variableReference.getName());
addError(VariableNotAllowed, variableReference.getSourceLocation(), message);
}
return;
} else if (ancestor instanceof OperationDefinition || ancestor instanceof Document) {
return;
}
}
}

// --- NoUnusedFragments ---
private void validateNoUnusedFragments() {
Set<String> allUsedFragments = new HashSet<>();
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/i18n/Validation.properties
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ VariableDefaultValuesOfCorrectType.badDefault=Validation error ({0}) : Bad defau
#
VariablesAreInputTypes.wrongType=Validation error ({0}) : Input variable ''{1}'' type ''{2}'' is not an input type
#
VariableNotAllowedInConstantDirective.variableNotAllowed=Validation error ({0}) : Variable ''{1}'' is not allowed in directive arguments on variable definitions. Only constant values are allowed here
#
VariableTypesMatchRule.unexpectedType=Validation error ({0}) : Variable ''{1}'' of type ''{2}'' used in position expecting type ''{3}''
#
UniqueObjectFieldName.duplicateFieldName=Validation Error ({0}) : There can be only one field named ''{1}''
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package graphql.validation

import graphql.TestUtil
import graphql.i18n.I18n
import graphql.language.Document
import graphql.parser.Parser
import spock.lang.Specification

class VariablesNotAllowedInConstantDirectivesTest extends Specification {

ValidationErrorCollector errorCollector = new ValidationErrorCollector()

def schema = TestUtil.schema('''
directive @dir(arg: Int) on VARIABLE_DEFINITION
directive @strDir(arg: String) on VARIABLE_DEFINITION | FIELD
type Query {
field(arg: Int): String
x: Int
}
''')

def traverse(String query) {
Document document = new Parser().parseDocument(query)
I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH)
ValidationContext validationContext = new ValidationContext(schema, document, i18n)
OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector,
{ r -> r == OperationValidationRule.VARIABLES_NOT_ALLOWED_IN_DIRECTIVES_ON_VARIABLE_DEFINITIONS })
LanguageTraversal languageTraversal = new LanguageTraversal()
languageTraversal.traverse(document, operationValidator)
}

def "variable reference in directive argument on variable definition is rejected"() {
given:
def query = '''
query ($v: Int @dir(arg: $v)) { x }
'''

when:
traverse(query)

then:
!errorCollector.errors.isEmpty()
errorCollector.containsValidationError(ValidationErrorType.VariableNotAllowed)
errorCollector.errors[0].message.contains("Variable 'v' is not allowed in directive arguments on variable definitions")
}

def "constant value in directive argument on variable definition is accepted"() {
given:
def query = '''
query ($v: Int @dir(arg: 42)) { x }
'''

when:
traverse(query)

then:
errorCollector.errors.isEmpty()
}

def "variable reference in field directive argument is accepted"() {
given:
def query = '''
query ($v: Int) { x @strDir(arg: "hello") }
'''

when:
traverse(query)

then:
errorCollector.errors.isEmpty()
}

def "no directive on variable definition is accepted"() {
given:
def query = '''
query ($v: Int) { field(arg: $v) }
'''

when:
traverse(query)

then:
errorCollector.errors.isEmpty()
}

def "variable reference from another variable in directive is rejected"() {
given:
def query = '''
query ($v: Int, $w: Int @dir(arg: $v)) { x }
'''

when:
traverse(query)

then:
!errorCollector.errors.isEmpty()
errorCollector.containsValidationError(ValidationErrorType.VariableNotAllowed)
errorCollector.errors[0].message.contains("Variable 'v' is not allowed in directive arguments on variable definitions")
}

def "full validation rejects variable in directive on variable definition"() {
given:
def query = '''
query ($v: Int @dir(arg: $v)) { x }
'''
def document = TestUtil.parseQuery(query)
def validator = new Validator()

when:
def validationErrors = validator.validateDocument(schema, document, Locale.ENGLISH)

then:
validationErrors.any { it.validationErrorType == ValidationErrorType.VariableNotAllowed }
}

def "full validation accepts constant in directive on variable definition"() {
given:
def query = '''
query ($v: Int @dir(arg: 42)) { x }
'''
def document = TestUtil.parseQuery(query)
def validator = new Validator()

when:
def validationErrors = validator.validateDocument(schema, document, Locale.ENGLISH)

then:
!validationErrors.any { it.validationErrorType == ValidationErrorType.VariableNotAllowed }
}
}
Loading