Skip to content

Commit

Permalink
Implemented NoUndefinedVariablesRule
Browse files Browse the repository at this point in the history
  • Loading branch information
sogko committed Nov 12, 2015
1 parent 9e62e2d commit 4c94ee3
Show file tree
Hide file tree
Showing 3 changed files with 363 additions and 2 deletions.
90 changes: 90 additions & 0 deletions rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var SpecifiedRules = []ValidationRuleFn{
KnownTypeNamesRule,
LoneAnonymousOperationRule,
NoFragmentCyclesRule,
NoUndefinedVariablesRule,
}

type ValidationRuleInstance struct {
Expand Down Expand Up @@ -615,6 +616,95 @@ func NoFragmentCyclesRule(context *ValidationContext) *ValidationRuleInstance {
}
}

/**
* NoUndefinedVariables
* No undefined variables
*
* A GraphQL operation is only valid if all variables encountered, both directly
* and via fragment spreads, are defined by that operation.
*/
func NoUndefinedVariablesRule(context *ValidationContext) *ValidationRuleInstance {
var operation *ast.OperationDefinition
var visitedFragmentNames = map[string]bool{}
var definedVariableNames = map[string]bool{}
visitorOpts := &visitor.VisitorOptions{
KindFuncMap: map[string]visitor.NamedVisitFuncs{
kinds.OperationDefinition: visitor.NamedVisitFuncs{
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
if node, ok := p.Node.(*ast.OperationDefinition); ok && node != nil {
operation = node
visitedFragmentNames = map[string]bool{}
definedVariableNames = map[string]bool{}
}
return visitor.ActionNoChange, nil
},
},
kinds.VariableDefinition: visitor.NamedVisitFuncs{
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
if node, ok := p.Node.(*ast.VariableDefinition); ok && node != nil {
variableName := ""
if node.Variable != nil && node.Variable.Name != nil {
variableName = node.Variable.Name.Value
}
definedVariableNames[variableName] = true
}
return visitor.ActionNoChange, nil
},
},
kinds.Variable: visitor.NamedVisitFuncs{
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
if variable, ok := p.Node.(*ast.Variable); ok && variable != nil {
variableName := ""
if variable.Name != nil {
variableName = variable.Name.Value
}
if val, _ := definedVariableNames[variableName]; !val {
withinFragment := false
for _, node := range p.Ancestors {
if node.GetKind() == kinds.FragmentDefinition {
withinFragment = true
break
}
}
if withinFragment == true && operation != nil && operation.Name != nil {
return newValidationRuleError(
fmt.Sprintf(`Variable "$%v" is not defined by operation "%v".`, variableName, operation.Name.Value),
[]ast.Node{variable, operation},
)
}
return newValidationRuleError(
fmt.Sprintf(`Variable "$%v" is not defined.`, variableName),
[]ast.Node{variable},
)
}
}
return visitor.ActionNoChange, nil
},
},
kinds.FragmentSpread: visitor.NamedVisitFuncs{
Kind: func(p visitor.VisitFuncParams) (string, interface{}) {
if node, ok := p.Node.(*ast.FragmentSpread); ok && node != nil {
// Only visit fragments of a particular name once per operation
fragmentName := ""
if node.Name != nil {
fragmentName = node.Name.Value
}
if val, ok := visitedFragmentNames[fragmentName]; ok && val == true {
return visitor.ActionSkip, nil
}
visitedFragmentNames[fragmentName] = true
}
return visitor.ActionNoChange, nil
},
},
},
}
return &ValidationRuleInstance{
VisitSpreadFragments: true,
VisitorOpts: visitorOpts,
}
}

/**
* Utility for validators which determines if a value literal AST is valid given
* an input type.
Expand Down
270 changes: 270 additions & 0 deletions rules/no_undefined_variables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package rules_test

import (
"testing"

"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/gqlerrors"
)

func TestValidate_NoUndefinedVariables_AllVariablesDefined(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c)
}
`)
}
func TestValidate_NoUndefinedVariables_AllVariablesDeeplyDefined(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String, $c: String) {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
`)
}
func TestValidate_NoUndefinedVariables_AllVariablesDeeplyDefinedInInlineFragmentsDefined(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String, $c: String) {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
`)
}
func TestValidate_NoUndefinedVariables_AllVariablesInFragmentsDeeplyDefined(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
`)
}
func TestValidate_NoUndefinedVariables_VariablesWithinSingleFragmentDefinedInMultipleOperations(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String) {
...FragA
}
query Bar($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a)
}
`)
}
func TestValidate_NoUndefinedVariables_VariableWithinFragmentsDefinedInOperations(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String) {
...FragA
}
query Bar($b: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
`)
}
func TestValidate_NoUndefinedVariables_VariableWithinRecursiveFragmentDefined(t *testing.T) {
expectPassesRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragA
}
}
`)
}
func TestValidate_NoUndefinedVariables_VariableNotDefined(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c, d: $d)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$d" is not defined.`, 3, 39),
})
}
func TestValidate_NoUndefinedVariables_VariableNotDefinedByUnnamedQuery(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
{
field(a: $a)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined.`, 3, 18),
})
}
func TestValidate_NoUndefinedVariables_MultipleVariablesNotDefined(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($b: String) {
field(a: $a, b: $b, c: $c)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined.`, 3, 18),
ruleError(`Variable "$c" is not defined.`, 3, 32),
})
}

func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByUnnamedQuery(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
{
...FragA
}
fragment FragA on Type {
field(a: $a)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined.`, 6, 18),
})
}

func TestValidate_NoUndefinedVariables_VariableInFragmentNotDefinedByOperation(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String, $b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$c" is not defined by operation "Foo".`, 16, 18, 2, 7),
})
}

func TestValidate_NoUndefinedVariables_MultipleVariablesInFragmentsNotDefined(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined by operation "Foo".`, 6, 18, 2, 7),
ruleError(`Variable "$c" is not defined by operation "Foo".`, 16, 18, 2, 7),
})
}

func TestValidate_NoUndefinedVariables_SingleVariableInFragmentNotDefinedByMultipleOperations(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($a: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$b" is not defined by operation "Foo".`, 9, 25, 2, 7),
ruleError(`Variable "$b" is not defined by operation "Bar".`, 9, 25, 5, 7),
})
}

func TestValidate_NoUndefinedVariables_VariablesInFragmentNotDefinedByMultipleOperations(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined by operation "Foo".`, 9, 18, 2, 7),
ruleError(`Variable "$b" is not defined by operation "Bar".`, 9, 25, 5, 7),
})
}
func TestValidate_NoUndefinedVariables_VariableInFragmentUsedByOtherOperation(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($b: String) {
...FragA
}
query Bar($a: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined by operation "Foo".`, 9, 18, 2, 7),
ruleError(`Variable "$b" is not defined by operation "Bar".`, 12, 18, 5, 7),
})
}

func TestValidate_NoUndefinedVariables_VaMultipleUndefinedVariablesProduceMultipleErrors(t *testing.T) {
expectFailsRule(t, graphql.NoUndefinedVariablesRule, `
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field1(a: $a, b: $b)
...FragC
field3(a: $a, b: $b)
}
fragment FragC on Type {
field2(c: $c)
}
`, []gqlerrors.FormattedError{
ruleError(`Variable "$a" is not defined by operation "Foo".`, 9, 19, 2, 7),
ruleError(`Variable "$c" is not defined by operation "Foo".`, 14, 19, 2, 7),
ruleError(`Variable "$a" is not defined by operation "Foo".`, 11, 19, 2, 7),
ruleError(`Variable "$b" is not defined by operation "Bar".`, 9, 26, 5, 7),
ruleError(`Variable "$c" is not defined by operation "Bar".`, 14, 19, 5, 7),
ruleError(`Variable "$b" is not defined by operation "Bar".`, 11, 26, 5, 7),
})
}
5 changes: 3 additions & 2 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul
// If any validation instances provide the flag `visitSpreadFragments`
// and this node is a fragment spread, visit the fragment definition
// from this point.
if result == nil && instance.VisitSpreadFragments == true && kind == kinds.FragmentSpread {
if action == visitor.ActionNoChange && result == nil &&
instance.VisitSpreadFragments == true && kind == kinds.FragmentSpread {
node, _ := node.(*ast.FragmentSpread)
name := node.Name
nameVal := ""
Expand All @@ -93,7 +94,7 @@ func visitUsingRules(schema *Schema, astDoc *ast.Document, rules []ValidationRul
}
}

// If the result is "false" (ie action === Action.Skip, we're not visiting any descendent nodes,
// If the result is "false" (ie action === Action.Skip), we're not visiting any descendent nodes,
// but need to update typeInfo.
if action == visitor.ActionSkip {
typeInfo.Leave(node)
Expand Down

0 comments on commit 4c94ee3

Please sign in to comment.