Skip to content

Commit

Permalink
validation: NoUnusedFragments
Browse files Browse the repository at this point in the history
  • Loading branch information
neelance committed May 23, 2017
1 parent da85f09 commit 490ad6b
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 17 deletions.
8 changes: 5 additions & 3 deletions internal/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type FragmentDecl struct {
Fragment
Name lexer.Ident
Directives common.DirectiveList
Loc errors.Location
}

type SelectionSet struct {
Expand Down Expand Up @@ -124,6 +125,7 @@ func parseDocument(l *lexer.Lexer) *Document {
continue
}

loc := l.Location()
switch x := l.ConsumeIdent(); x {
case "query":
d.Operations = append(d.Operations, parseOperation(l, Query))
Expand All @@ -135,7 +137,7 @@ func parseDocument(l *lexer.Lexer) *Document {
d.Operations = append(d.Operations, parseOperation(l, Subscription))

case "fragment":
d.Fragments = append(d.Fragments, parseFragment(l))
d.Fragments = append(d.Fragments, parseFragment(l, loc))

default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x))
Expand Down Expand Up @@ -163,8 +165,8 @@ func parseOperation(l *lexer.Lexer, opType OperationType) *Operation {
return op
}

func parseFragment(l *lexer.Lexer) *FragmentDecl {
f := &FragmentDecl{}
func parseFragment(l *lexer.Lexer, loc errors.Location) *FragmentDecl {
f := &FragmentDecl{Loc: loc}
f.Name = l.ConsumeIdentWithLoc()
l.ConsumeKeyword("on")
f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()}
Expand Down
2 changes: 1 addition & 1 deletion internal/tests/testdata/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ require('./src/validation/__tests__/KnownTypeNames-test');
require('./src/validation/__tests__/LoneAnonymousOperation-test');
// require('./src/validation/__tests__/NoFragmentCycles-test');
// require('./src/validation/__tests__/NoUndefinedVariables-test');
// require('./src/validation/__tests__/NoUnusedFragments-test');
require('./src/validation/__tests__/NoUnusedFragments-test');
// require('./src/validation/__tests__/NoUnusedVariables-test');
// require('./src/validation/__tests__/OverlappingFieldsCanBeMerged-test');
// require('./src/validation/__tests__/PossibleFragmentSpreads-test');
Expand Down
78 changes: 78 additions & 0 deletions internal/tests/testdata/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,84 @@
}
]
},
{
"name": "Validate: No unused fragments/all fragment names are used",
"rule": "NoUnusedFragments",
"query": "\n {\n human(id: 4) {\n ...HumanFields1\n ... on Human {\n ...HumanFields2\n }\n }\n }\n fragment HumanFields1 on Human {\n name\n ...HumanFields3\n }\n fragment HumanFields2 on Human {\n name\n }\n fragment HumanFields3 on Human {\n name\n }\n ",
"errors": []
},
{
"name": "Validate: No unused fragments/all fragment names are used by multiple operations",
"rule": "NoUnusedFragments",
"query": "\n query Foo {\n human(id: 4) {\n ...HumanFields1\n }\n }\n query Bar {\n human(id: 4) {\n ...HumanFields2\n }\n }\n fragment HumanFields1 on Human {\n name\n ...HumanFields3\n }\n fragment HumanFields2 on Human {\n name\n }\n fragment HumanFields3 on Human {\n name\n }\n ",
"errors": []
},
{
"name": "Validate: No unused fragments/contains unknown fragments",
"rule": "NoUnusedFragments",
"query": "\n query Foo {\n human(id: 4) {\n ...HumanFields1\n }\n }\n query Bar {\n human(id: 4) {\n ...HumanFields2\n }\n }\n fragment HumanFields1 on Human {\n name\n ...HumanFields3\n }\n fragment HumanFields2 on Human {\n name\n }\n fragment HumanFields3 on Human {\n name\n }\n fragment Unused1 on Human {\n name\n }\n fragment Unused2 on Human {\n name\n }\n ",
"errors": [
{
"message": "Fragment \"Unused1\" is never used.",
"locations": [
{
"line": 22,
"column": 7
}
]
},
{
"message": "Fragment \"Unused2\" is never used.",
"locations": [
{
"line": 25,
"column": 7
}
]
}
]
},
{
"name": "Validate: No unused fragments/contains unknown fragments with ref cycle",
"rule": "NoUnusedFragments",
"query": "\n query Foo {\n human(id: 4) {\n ...HumanFields1\n }\n }\n query Bar {\n human(id: 4) {\n ...HumanFields2\n }\n }\n fragment HumanFields1 on Human {\n name\n ...HumanFields3\n }\n fragment HumanFields2 on Human {\n name\n }\n fragment HumanFields3 on Human {\n name\n }\n fragment Unused1 on Human {\n name\n ...Unused2\n }\n fragment Unused2 on Human {\n name\n ...Unused1\n }\n ",
"errors": [
{
"message": "Fragment \"Unused1\" is never used.",
"locations": [
{
"line": 22,
"column": 7
}
]
},
{
"message": "Fragment \"Unused2\" is never used.",
"locations": [
{
"line": 26,
"column": 7
}
]
}
]
},
{
"name": "Validate: No unused fragments/contains unknown and undef fragments",
"rule": "NoUnusedFragments",
"query": "\n query Foo {\n human(id: 4) {\n ...bar\n }\n }\n fragment foo on Human {\n name\n }\n ",
"errors": [
{
"message": "Fragment \"foo\" is never used.",
"locations": [
{
"line": 7,
"column": 7
}
]
}
]
},
{
"name": "Validate: Provided required arguments/ignores unknown arguments",
"rule": "ProvidedNonNullArguments",
Expand Down
65 changes: 52 additions & 13 deletions internal/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (
)

type context struct {
schema *schema.Schema
doc *query.Document
errs []*errors.QueryError
schema *schema.Schema
doc *query.Document
errs []*errors.QueryError
usedFragments map[*query.FragmentDecl]struct{}
}

func (c *context) addErr(loc errors.Location, rule string, format string, a ...interface{}) {
Expand All @@ -30,8 +31,9 @@ func (c *context) addErr(loc errors.Location, rule string, format string, a ...i

func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError {
c := context{
schema: s,
doc: doc,
schema: s,
doc: doc,
usedFragments: make(map[*query.FragmentDecl]struct{}),
}

opNames := make(nameSet)
Expand Down Expand Up @@ -76,7 +78,7 @@ func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError {
default:
panic("unreachable")
}
c.validateSelectionSet(op.SelSet, entryPoint)
c.shallowValidateSelectionSet(op.SelSet, entryPoint)
}

fragNames := make(nameSet)
Expand All @@ -89,21 +91,30 @@ func Validate(s *schema.Schema, doc *query.Document) []*errors.QueryError {
c.addErr(frag.On.Loc, "FragmentsOnCompositeTypes", "Fragment %q cannot condition on non composite type %q.", frag.Name.Name, t)
continue
}
c.validateSelectionSet(frag.SelSet, t)
c.shallowValidateSelectionSet(frag.SelSet, t)
}

for _, op := range doc.Operations {
c.deepValidateSelectionSet(op.SelSet)
}

for _, frag := range doc.Fragments {
if _, ok := c.usedFragments[frag]; !ok {
c.addErr(frag.Loc, "NoUnusedFragments", "Fragment %q is never used.", frag.Name.Name)
}
}

sort.Slice(c.errs, func(i, j int) bool { return c.errs[i].Locations[0].Before(c.errs[j].Locations[0]) })
return c.errs
}

func (c *context) validateSelectionSet(selSet *query.SelectionSet, t common.Type) {
func (c *context) shallowValidateSelectionSet(selSet *query.SelectionSet, t common.Type) {
for _, sel := range selSet.Selections {
c.validateSelection(sel, t)
c.shallowValidateSelection(sel, t)
}
return
}

func (c *context) validateSelection(sel query.Selection, t common.Type) {
func (c *context) shallowValidateSelection(sel query.Selection, t common.Type) {
switch sel := sel.(type) {
case *query.Field:
c.validateDirectives("FIELD", sel.Directives)
Expand Down Expand Up @@ -165,7 +176,7 @@ func (c *context) validateSelection(sel query.Selection, t common.Type) {
}
}
if sel.SelSet != nil {
c.validateSelectionSet(sel.SelSet, ft)
c.shallowValidateSelectionSet(sel.SelSet, ft)
}

case *query.InlineFragment:
Expand All @@ -178,12 +189,40 @@ func (c *context) validateSelection(sel query.Selection, t common.Type) {
c.addErr(sel.On.Loc, "FragmentsOnCompositeTypes", "Fragment cannot condition on non composite type %q.", t)
return
}
c.validateSelectionSet(sel.SelSet, t)
c.shallowValidateSelectionSet(sel.SelSet, t)

case *query.FragmentSpread:
c.validateDirectives("FRAGMENT_SPREAD", sel.Directives)
if frag := c.doc.Fragments.Get(sel.Name.Name); frag == nil {
c.addErr(sel.Name.Loc, "KnownFragmentNames", "Unknown fragment %q.", sel.Name.Name)
return
}

default:
panic("unreachable")
}
}

func (c *context) deepValidateSelectionSet(selSet *query.SelectionSet) {
for _, sel := range selSet.Selections {
c.deepValidateSelection(sel)
}
}

func (c *context) deepValidateSelection(sel query.Selection) {
switch sel := sel.(type) {
case *query.Field:
if sel.SelSet != nil {
c.deepValidateSelectionSet(sel.SelSet)
}

case *query.InlineFragment:
c.deepValidateSelectionSet(sel.SelSet)

case *query.FragmentSpread:
if frag := c.doc.Fragments.Get(sel.Name.Name); frag != nil {
c.usedFragments[frag] = struct{}{}
c.deepValidateSelectionSet(frag.SelSet)
}

default:
Expand Down

0 comments on commit 490ad6b

Please sign in to comment.