From 0d9951e95275a49cd73ed3d901eb2e955b0c03e8 Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Mon, 16 Jul 2018 17:49:02 -0400
Subject: [PATCH 1/6] Fragment variables: enforce fragment variable validation

Treats fragments with variable definitions as their own top-level
"executable definition": this means a query cannot include a variable
definition found solely beneath a fragment-variable definition, and
a fragment must include all variable definitions that exist under it,
excluding other variables defined by other fragments.
---
 src/validation/ValidationContext.js           | 53 +++++++++++----
 .../__tests__/NoUndefinedVariables-test.js    | 38 +++++++++++
 .../__tests__/NoUnusedVariables-test.js       | 68 +++++++++++++++++++
 src/validation/__tests__/harness.js           | 12 +++-
 src/validation/rules/NoUndefinedVariables.js  | 52 ++++++++------
 src/validation/rules/NoUnusedVariables.js     | 67 +++++++++++-------
 6 files changed, 229 insertions(+), 61 deletions(-)

diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js
index 1d6ef75741..7cf1718660 100644
--- a/src/validation/ValidationContext.js
+++ b/src/validation/ValidationContext.js
@@ -13,6 +13,7 @@ import { visit, visitWithTypeInfo } from '../language/visitor';
 import { Kind } from '../language/kinds';
 import type {
   DocumentNode,
+  ExecutableDefinitionNode,
   OperationDefinitionNode,
   VariableNode,
   SelectionSetNode,
@@ -50,12 +51,12 @@ export default class ValidationContext {
   _fragments: ObjMap<FragmentDefinitionNode>;
   _fragmentSpreads: Map<SelectionSetNode, $ReadOnlyArray<FragmentSpreadNode>>;
   _recursivelyReferencedFragments: Map<
-    OperationDefinitionNode,
+    ExecutableDefinitionNode,
     $ReadOnlyArray<FragmentDefinitionNode>,
   >;
   _variableUsages: Map<NodeWithSelectionSet, $ReadOnlyArray<VariableUsage>>;
   _recursiveVariableUsages: Map<
-    OperationDefinitionNode,
+    ExecutableDefinitionNode,
     $ReadOnlyArray<VariableUsage>,
   >;
 
@@ -129,14 +130,21 @@ export default class ValidationContext {
     return spreads;
   }
 
+  /*
+   * Finds all fragments referenced via the definition, recursively.
+   *
+   * NOTE: if experimentalFragmentVariables are being used, it excludes all
+   * fragments with their own variable definitions: these are considered their
+   * own "root" executable definition.
+   */
   getRecursivelyReferencedFragments(
-    operation: OperationDefinitionNode,
+    definition: ExecutableDefinitionNode,
   ): $ReadOnlyArray<FragmentDefinitionNode> {
-    let fragments = this._recursivelyReferencedFragments.get(operation);
+    let fragments = this._recursivelyReferencedFragments.get(definition);
     if (!fragments) {
       fragments = [];
       const collectedNames = Object.create(null);
-      const nodesToVisit: Array<SelectionSetNode> = [operation.selectionSet];
+      const nodesToVisit: Array<SelectionSetNode> = [definition.selectionSet];
       while (nodesToVisit.length !== 0) {
         const node = nodesToVisit.pop();
         const spreads = this.getFragmentSpreads(node);
@@ -145,14 +153,17 @@ export default class ValidationContext {
           if (collectedNames[fragName] !== true) {
             collectedNames[fragName] = true;
             const fragment = this.getFragment(fragName);
-            if (fragment) {
+            if (
+              fragment &&
+              !this.isExecutableDefinitionWithVariables(fragment)
+            ) {
               fragments.push(fragment);
               nodesToVisit.push(fragment.selectionSet);
             }
           }
         }
       }
-      this._recursivelyReferencedFragments.set(operation, fragments);
+      this._recursivelyReferencedFragments.set(definition, fragments);
     }
     return fragments;
   }
@@ -181,20 +192,27 @@ export default class ValidationContext {
     return usages;
   }
 
+  /*
+   * Finds all variables used by the definition, recursively.
+   *
+   * NOTE: if experimentalFragmentVariables are being used, it excludes all
+   * fragments with their own variable definitions: these are considered their
+   * own "root" executable definition.
+   */
   getRecursiveVariableUsages(
-    operation: OperationDefinitionNode,
+    definition: ExecutableDefinitionNode,
   ): $ReadOnlyArray<VariableUsage> {
-    let usages = this._recursiveVariableUsages.get(operation);
+    let usages = this._recursiveVariableUsages.get(definition);
     if (!usages) {
-      usages = this.getVariableUsages(operation);
-      const fragments = this.getRecursivelyReferencedFragments(operation);
+      usages = this.getVariableUsages(definition);
+      const fragments = this.getRecursivelyReferencedFragments(definition);
       for (let i = 0; i < fragments.length; i++) {
         Array.prototype.push.apply(
           usages,
           this.getVariableUsages(fragments[i]),
         );
       }
-      this._recursiveVariableUsages.set(operation, usages);
+      this._recursiveVariableUsages.set(definition, usages);
     }
     return usages;
   }
@@ -226,4 +244,15 @@ export default class ValidationContext {
   getArgument(): ?GraphQLArgument {
     return this._typeInfo.getArgument();
   }
+
+  // All OperationDefinitions, or FragmentDefinitions with variable definitions
+  isExecutableDefinitionWithVariables(
+    definition: ExecutableDefinitionNode,
+  ): boolean {
+    return (
+      definition.kind === Kind.OPERATION_DEFINITION ||
+      (definition.variableDefinitions != null &&
+        definition.variableDefinitions.length > 0)
+    );
+  }
 }
diff --git a/src/validation/__tests__/NoUndefinedVariables-test.js b/src/validation/__tests__/NoUndefinedVariables-test.js
index ff7e096ca1..67156e4e1d 100644
--- a/src/validation/__tests__/NoUndefinedVariables-test.js
+++ b/src/validation/__tests__/NoUndefinedVariables-test.js
@@ -329,4 +329,42 @@ describe('Validate: No undefined variables', () => {
       ],
     );
   });
+
+  // Experimental Fragment Variables
+  it('uses all variables in fragments with variable definitions', () => {
+    expectPassesRule(
+      NoUndefinedVariables,
+      `
+      fragment Foo($a: String, $b: String, $c: String) on Type {
+        ...FragA
+      }
+      fragment FragA on Type {
+        field(a: $a) {
+          ...FragB
+        }
+      }
+      fragment FragB on Type {
+        field(b: $b) {
+          ...FragC
+        }
+      }
+      fragment FragC on Type {
+        field(c: $c)
+      }
+    `,
+    );
+  });
+
+  it('variable used in fragment not defined', () => {
+    expectFailsRule(
+      NoUndefinedVariables,
+      `
+      fragment FragA($a: String) on Type {
+        field(a: $a)
+        field(b: $b)
+      }
+    `,
+      [undefVar('b', 4, 18, 'FragA', 2, 7)],
+    );
+  });
 });
diff --git a/src/validation/__tests__/NoUnusedVariables-test.js b/src/validation/__tests__/NoUnusedVariables-test.js
index fabe5844de..bbdff51ffc 100644
--- a/src/validation/__tests__/NoUnusedVariables-test.js
+++ b/src/validation/__tests__/NoUnusedVariables-test.js
@@ -237,4 +237,72 @@ describe('Validate: No unused variables', () => {
       [unusedVar('b', 'Foo', 2, 17), unusedVar('a', 'Bar', 5, 17)],
     );
   });
+
+  // Experimental Fragment Variables
+  it('uses all variables in fragments with variable definitions', () => {
+    expectPassesRule(
+      NoUnusedVariables,
+      `
+      fragment Foo($a: String, $b: String, $c: String) on Type {
+        ...FragA
+      }
+      fragment FragA on Type {
+        field(a: $a) {
+          ...FragB
+        }
+      }
+      fragment FragB on Type {
+        field(b: $b) {
+          ...FragC
+        }
+      }
+      fragment FragC on Type {
+        field(c: $c)
+      }
+    `,
+    );
+  });
+
+  it('variable not used by fragment', () => {
+    expectFailsRule(
+      NoUnusedVariables,
+      `
+      fragment FragA($a: String) on Type {
+        field
+      }
+    `,
+      [unusedVar('a', 'FragA', 2, 22)],
+    );
+  });
+
+  it('variable used in query defined by fragment', () => {
+    expectFailsRule(
+      NoUnusedVariables,
+      `
+      query Foo($a: String) {
+        field(a: $a)
+        ...FragA
+      }
+      fragment FragA($a: String) on Type {
+        field
+      }
+    `,
+      [unusedVar('a', 'FragA', 6, 22)],
+    );
+
+    it('variable defined in fragment used by query', () => {
+      expectFailsRule(
+        NoUnusedVariables,
+        `
+        query Foo($a: String) {
+          ...FragA
+        }
+        fragment FragA($a: String) on Type {
+          field(a: $a)
+        }
+      `,
+        [unusedVar('a', 'Foo', 1, 19)],
+      );
+    });
+  });
 });
diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js
index 6d428faa79..ca92c211c0 100644
--- a/src/validation/__tests__/harness.js
+++ b/src/validation/__tests__/harness.js
@@ -422,12 +422,20 @@ export const testSchema = new GraphQLSchema({
 });
 
 function expectValid(schema, rules, queryString) {
-  const errors = validate(schema, parse(queryString), rules);
+  const errors = validate(
+    schema,
+    parse(queryString, { experimentalFragmentVariables: true }),
+    rules,
+  );
   expect(errors).to.deep.equal([], 'Should validate');
 }
 
 function expectInvalid(schema, rules, queryString, expectedErrors) {
-  const errors = validate(schema, parse(queryString), rules);
+  const errors = validate(
+    schema,
+    parse(queryString, { experimentalFragmentVariables: true }),
+    rules,
+  );
   expect(errors).to.have.length.of.at.least(1, 'Should not validate');
   expect(errors).to.deep.equal(expectedErrors);
   return errors;
diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index d74a49265b..858d3d5ddd 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -10,6 +10,7 @@
 import type ValidationContext from '../ValidationContext';
 import { GraphQLError } from '../../error';
 import type { ASTVisitor } from '../../language/visitor';
+import type { ExecutableDefinitionNode } from '../../language';
 
 export function undefinedVarMessage(varName: string, opName: ?string): string {
   return opName
@@ -26,30 +27,39 @@ export function undefinedVarMessage(varName: string, opName: ?string): string {
 export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);
 
-  return {
-    OperationDefinition: {
-      enter() {
-        variableNameDefined = Object.create(null);
-      },
-      leave(operation) {
-        const usages = context.getRecursiveVariableUsages(operation);
+  const executableDefinitionVisitor = {
+    enter(definition: ExecutableDefinitionNode) {
+      if (!context.isExecutableDefinitionWithVariables(definition)) {
+        return;
+      }
+      variableNameDefined = Object.create(null);
+    },
+    leave(definition: ExecutableDefinitionNode) {
+      if (!context.isExecutableDefinitionWithVariables(definition)) {
+        return;
+      }
+      const usages = context.getRecursiveVariableUsages(definition);
 
-        usages.forEach(({ node }) => {
-          const varName = node.name.value;
-          if (variableNameDefined[varName] !== true) {
-            context.reportError(
-              new GraphQLError(
-                undefinedVarMessage(
-                  varName,
-                  operation.name && operation.name.value,
-                ),
-                [node, operation],
+      usages.forEach(({ node }) => {
+        const varName = node.name.value;
+        if (variableNameDefined[varName] !== true) {
+          context.reportError(
+            new GraphQLError(
+              undefinedVarMessage(
+                varName,
+                definition.name && definition.name.value,
               ),
-            );
-          }
-        });
-      },
+              [node, definition],
+            ),
+          );
+        }
+      });
     },
+  };
+
+  return {
+    OperationDefinition: executableDefinitionVisitor,
+    FragmentDefinition: executableDefinitionVisitor,
     VariableDefinition(node) {
       variableNameDefined[node.variable.name.value] = true;
     },
diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js
index c5637e22db..a0f7ee9940 100644
--- a/src/validation/rules/NoUnusedVariables.js
+++ b/src/validation/rules/NoUnusedVariables.js
@@ -9,6 +9,7 @@
 
 import type ValidationContext from '../ValidationContext';
 import { GraphQLError } from '../../error';
+import type { ExecutableDefinitionNode } from '../../language';
 import type { ASTVisitor } from '../../language/visitor';
 
 export function unusedVariableMessage(
@@ -23,38 +24,52 @@ export function unusedVariableMessage(
 /**
  * No unused variables
  *
- * A GraphQL operation is only valid if all variables defined by an operation
+ * A GraphQL executable tree is only valid if all variables defined by that tree
  * are used, either directly or within a spread fragment.
+ *
+ * NOTE: if experimentalFragmentVariables are used, then fragments with
+ * variables defined are considered an independent "executable tree":
+ * fragments defined under them do not count as "within a fragment spread".
  */
 export function NoUnusedVariables(context: ValidationContext): ASTVisitor {
   let variableDefs = [];
 
-  return {
-    OperationDefinition: {
-      enter() {
-        variableDefs = [];
-      },
-      leave(operation) {
-        const variableNameUsed = Object.create(null);
-        const usages = context.getRecursiveVariableUsages(operation);
-        const opName = operation.name ? operation.name.value : null;
-
-        usages.forEach(({ node }) => {
-          variableNameUsed[node.name.value] = true;
-        });
-
-        variableDefs.forEach(variableDef => {
-          const variableName = variableDef.variable.name.value;
-          if (variableNameUsed[variableName] !== true) {
-            context.reportError(
-              new GraphQLError(unusedVariableMessage(variableName, opName), [
-                variableDef,
-              ]),
-            );
-          }
-        });
-      },
+  const executableDefinitionVisitor = {
+    enter(definition: ExecutableDefinitionNode) {
+      if (!context.isExecutableDefinitionWithVariables(definition)) {
+        return;
+      }
+      variableDefs = [];
     },
+    leave(definition: ExecutableDefinitionNode) {
+      if (!context.isExecutableDefinitionWithVariables(definition)) {
+        return;
+      }
+
+      const variableNameUsed = Object.create(null);
+      const usages = context.getRecursiveVariableUsages(definition);
+      const opName = definition.name ? definition.name.value : null;
+
+      usages.forEach(({ node }) => {
+        variableNameUsed[node.name.value] = true;
+      });
+
+      variableDefs.forEach(variableDef => {
+        const variableName = variableDef.variable.name.value;
+        if (variableNameUsed[variableName] !== true) {
+          context.reportError(
+            new GraphQLError(unusedVariableMessage(variableName, opName), [
+              variableDef,
+            ]),
+          );
+        }
+      });
+    },
+  };
+
+  return {
+    OperationDefinition: executableDefinitionVisitor,
+    FragmentDefinition: executableDefinitionVisitor,
     VariableDefinition(def) {
       variableDefs.push(def);
     },

From cbd3abf87effbf5daa187be3c56c0baae9914d0a Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Mon, 16 Jul 2018 19:53:20 -0400
Subject: [PATCH 2/6] Modify comments to be a bit more accurate

---
 src/validation/ValidationContext.js          | 2 +-
 src/validation/rules/NoUndefinedVariables.js | 8 ++++++--
 src/validation/rules/NoUnusedVariables.js    | 8 ++++----
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js
index 7cf1718660..495e24fe9e 100644
--- a/src/validation/ValidationContext.js
+++ b/src/validation/ValidationContext.js
@@ -197,7 +197,7 @@ export default class ValidationContext {
    *
    * NOTE: if experimentalFragmentVariables are being used, it excludes all
    * fragments with their own variable definitions: these are considered their
-   * own "root" executable definition.
+   * own independent executable definition for the purposes of variable usage.
    */
   getRecursiveVariableUsages(
     definition: ExecutableDefinitionNode,
diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index 858d3d5ddd..4554e200c1 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -21,8 +21,12 @@ export function undefinedVarMessage(varName: string, opName: ?string): string {
 /**
  * No undefined variables
  *
- * A GraphQL operation is only valid if all variables encountered, both directly
- * and via fragment spreads, are defined by that operation.
+ * A GraphQL definition is only valid if all variables encountered, both
+ * directly and via fragment spreads, are defined by that definition.
+ *
+ * NOTE: if experimentalFragmentVariables are used, then fragments with
+ * variables defined are considered an independent "executable definitions":
+ * variables defined under them do not count as "within a fragment spread".
  */
 export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);
diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js
index a0f7ee9940..cf291babf6 100644
--- a/src/validation/rules/NoUnusedVariables.js
+++ b/src/validation/rules/NoUnusedVariables.js
@@ -24,12 +24,12 @@ export function unusedVariableMessage(
 /**
  * No unused variables
  *
- * A GraphQL executable tree is only valid if all variables defined by that tree
- * are used, either directly or within a spread fragment.
+ * A GraphQL definition is only valid if all variables defined by that
+ * definition are used, either directly or within a spread fragment.
  *
  * NOTE: if experimentalFragmentVariables are used, then fragments with
- * variables defined are considered an independent "executable tree":
- * fragments defined under them do not count as "within a fragment spread".
+ * variables defined are considered an independent "executable definitions":
+ * variables defined under them do not count as "within a fragment spread".
  */
 export function NoUnusedVariables(context: ValidationContext): ASTVisitor {
   let variableDefs = [];

From e9a7991b20e9a144801931c481aa8cc24a304628 Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Mon, 16 Jul 2018 20:23:30 -0400
Subject: [PATCH 3/6] More comment cleanup

---
 src/validation/rules/NoUndefinedVariables.js | 5 +++--
 src/validation/rules/NoUnusedVariables.js    | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index 4554e200c1..b3878de987 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -25,8 +25,9 @@ export function undefinedVarMessage(varName: string, opName: ?string): string {
  * directly and via fragment spreads, are defined by that definition.
  *
  * NOTE: if experimentalFragmentVariables are used, then fragments with
- * variables defined are considered an independent "executable definitions":
- * variables defined under them do not count as "within a fragment spread".
+ * variables defined are considered independent "executable definitions".
+ * If a fragment defines at least 1 variable, it must define all recursively
+ * vused ariables, excluding other fragments with variables defined.
  */
 export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);
diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js
index cf291babf6..3566ac77bd 100644
--- a/src/validation/rules/NoUnusedVariables.js
+++ b/src/validation/rules/NoUnusedVariables.js
@@ -28,8 +28,9 @@ export function unusedVariableMessage(
  * definition are used, either directly or within a spread fragment.
  *
  * NOTE: if experimentalFragmentVariables are used, then fragments with
- * variables defined are considered an independent "executable definitions":
- * variables defined under them do not count as "within a fragment spread".
+ * variables defined are considered independent "executable definitions".
+ * So `query Foo` must not define `$a` when `$a` is only used inside
+ * `fragment FragA($a: Type)`
  */
 export function NoUnusedVariables(context: ValidationContext): ASTVisitor {
   let variableDefs = [];

From 0e1c6de7b5b2013c36d92495e2f3d10896c1f144 Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Tue, 17 Jul 2018 13:36:27 -0400
Subject: [PATCH 4/6] Remove dependencies outside of ValidationContext on
 variableDefinitions + comments

---
 src/validation/ValidationContext.js          | 17 ++++++-----------
 src/validation/rules/NoUndefinedVariables.js | 11 ++++++-----
 src/validation/rules/NoUnusedVariables.js    | 11 ++++++-----
 3 files changed, 18 insertions(+), 21 deletions(-)

diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js
index 495e24fe9e..31fd0884b3 100644
--- a/src/validation/ValidationContext.js
+++ b/src/validation/ValidationContext.js
@@ -155,7 +155,7 @@ export default class ValidationContext {
             const fragment = this.getFragment(fragName);
             if (
               fragment &&
-              !this.isExecutableDefinitionWithVariables(fragment)
+              !isExperimentalFragmentWithVariableDefinitions(fragment)
             ) {
               fragments.push(fragment);
               nodesToVisit.push(fragment.selectionSet);
@@ -244,15 +244,10 @@ export default class ValidationContext {
   getArgument(): ?GraphQLArgument {
     return this._typeInfo.getArgument();
   }
+}
 
-  // All OperationDefinitions, or FragmentDefinitions with variable definitions
-  isExecutableDefinitionWithVariables(
-    definition: ExecutableDefinitionNode,
-  ): boolean {
-    return (
-      definition.kind === Kind.OPERATION_DEFINITION ||
-      (definition.variableDefinitions != null &&
-        definition.variableDefinitions.length > 0)
-    );
-  }
+function isExperimentalFragmentWithVariableDefinitions(
+  ast: FragmentDefinitionNode,
+): boolean %checks {
+  return ast.variableDefinitions != null && ast.variableDefinitions.length > 0;
 }
diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index b3878de987..478046d0e1 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -10,6 +10,7 @@
 import type ValidationContext from '../ValidationContext';
 import { GraphQLError } from '../../error';
 import type { ASTVisitor } from '../../language/visitor';
+import { Kind } from '../../language';
 import type { ExecutableDefinitionNode } from '../../language';
 
 export function undefinedVarMessage(varName: string, opName: ?string): string {
@@ -33,14 +34,14 @@ export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);
 
   const executableDefinitionVisitor = {
-    enter(definition: ExecutableDefinitionNode) {
-      if (!context.isExecutableDefinitionWithVariables(definition)) {
-        return;
-      }
+    enter() {
       variableNameDefined = Object.create(null);
     },
     leave(definition: ExecutableDefinitionNode) {
-      if (!context.isExecutableDefinitionWithVariables(definition)) {
+      if (
+        definition.kind === Kind.FRAGMENT_DEFINITION &&
+        Object.keys(variableNameDefined).length === 0
+      ) {
         return;
       }
       const usages = context.getRecursiveVariableUsages(definition);
diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js
index 3566ac77bd..a5f0bbeb83 100644
--- a/src/validation/rules/NoUnusedVariables.js
+++ b/src/validation/rules/NoUnusedVariables.js
@@ -9,6 +9,7 @@
 
 import type ValidationContext from '../ValidationContext';
 import { GraphQLError } from '../../error';
+import { Kind } from '../../language';
 import type { ExecutableDefinitionNode } from '../../language';
 import type { ASTVisitor } from '../../language/visitor';
 
@@ -36,14 +37,14 @@ export function NoUnusedVariables(context: ValidationContext): ASTVisitor {
   let variableDefs = [];
 
   const executableDefinitionVisitor = {
-    enter(definition: ExecutableDefinitionNode) {
-      if (!context.isExecutableDefinitionWithVariables(definition)) {
-        return;
-      }
+    enter() {
       variableDefs = [];
     },
     leave(definition: ExecutableDefinitionNode) {
-      if (!context.isExecutableDefinitionWithVariables(definition)) {
+      if (
+        definition.kind === Kind.FRAGMENT_DEFINITION &&
+        variableDefs.length === 0
+      ) {
         return;
       }
 

From 618349e6f7b7ce7ba94d723b74a1e50292fcdc73 Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Tue, 17 Jul 2018 13:39:44 -0400
Subject: [PATCH 5/6] typo

---
 src/validation/rules/NoUndefinedVariables.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index 478046d0e1..e81cdb718e 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -28,7 +28,7 @@ export function undefinedVarMessage(varName: string, opName: ?string): string {
  * NOTE: if experimentalFragmentVariables are used, then fragments with
  * variables defined are considered independent "executable definitions".
  * If a fragment defines at least 1 variable, it must define all recursively
- * vused ariables, excluding other fragments with variables defined.
+ * used variables, excluding other fragments with variables defined.
  */
 export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);

From f8be13549e76c255cafe49596791dfe84db7db04 Mon Sep 17 00:00:00 2001
From: mmahoney <mmahoney@fb.com>
Date: Tue, 17 Jul 2018 18:38:21 -0400
Subject: [PATCH 6/6] Add an optional param for validation: whether to validate
 specially for fragment variables

---
 src/validation/ValidationContext.js           | 48 ++++++++++++++-----
 .../__tests__/NoUndefinedVariables-test.js    | 11 +++--
 .../__tests__/NoUnusedVariables-test.js       | 15 ++++--
 src/validation/__tests__/harness.js           | 34 +++++++++++--
 src/validation/rules/NoUndefinedVariables.js  |  5 +-
 src/validation/rules/NoUnusedVariables.js     |  2 +-
 src/validation/validate.js                    |  6 ++-
 7 files changed, 91 insertions(+), 30 deletions(-)

diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js
index 31fd0884b3..6df71fd0ed 100644
--- a/src/validation/ValidationContext.js
+++ b/src/validation/ValidationContext.js
@@ -38,6 +38,22 @@ type VariableUsage = {|
   +defaultValue: ?mixed,
 |};
 
+export type ValidationOptions = {
+  /**
+   * EXPERIMENTAL:
+   *
+   * If enabled, the validator will treat fragments with variable definitions
+   * as "independent" definitions, in the same way operations are "independent".
+   *
+   * This changes the behavior of "getRecursivelyReferencedFragments":
+   * it will not include fragments that have variable definitions in the result.
+   *
+   * Likewise, "getRecursiveVariableUsages" will not include variables used
+   * only by recursively referenced fragments with variable definitions.
+   */
+  experimentalFragmentVariables?: boolean,
+};
+
 /**
  * An instance of this class is passed as the "this" context to all validators,
  * allowing access to commonly useful contextual information from within a
@@ -59,11 +75,13 @@ export default class ValidationContext {
     ExecutableDefinitionNode,
     $ReadOnlyArray<VariableUsage>,
   >;
+  _options: ValidationOptions;
 
   constructor(
     schema: GraphQLSchema,
     ast: DocumentNode,
     typeInfo: TypeInfo,
+    options: ValidationOptions = {},
   ): void {
     this._schema = schema;
     this._ast = ast;
@@ -73,6 +91,7 @@ export default class ValidationContext {
     this._recursivelyReferencedFragments = new Map();
     this._variableUsages = new Map();
     this._recursiveVariableUsages = new Map();
+    this._options = options;
   }
 
   reportError(error: GraphQLError): void {
@@ -133,14 +152,14 @@ export default class ValidationContext {
   /*
    * Finds all fragments referenced via the definition, recursively.
    *
-   * NOTE: if experimentalFragmentVariables are being used, it excludes all
-   * fragments with their own variable definitions: these are considered their
-   * own "root" executable definition.
+   * If the experimentalFragmentVariables option is used, this will not visit
+   * fragment definitions that include variable definitions.
    */
   getRecursivelyReferencedFragments(
     definition: ExecutableDefinitionNode,
   ): $ReadOnlyArray<FragmentDefinitionNode> {
     let fragments = this._recursivelyReferencedFragments.get(definition);
+
     if (!fragments) {
       fragments = [];
       const collectedNames = Object.create(null);
@@ -153,16 +172,25 @@ export default class ValidationContext {
           if (collectedNames[fragName] !== true) {
             collectedNames[fragName] = true;
             const fragment = this.getFragment(fragName);
+
+            if (!fragment) {
+              continue;
+            }
+
             if (
-              fragment &&
-              !isExperimentalFragmentWithVariableDefinitions(fragment)
+              this._options.experimentalFragmentVariables &&
+              fragment.variableDefinitions &&
+              fragment.variableDefinitions.length > 0
             ) {
-              fragments.push(fragment);
-              nodesToVisit.push(fragment.selectionSet);
+              continue;
             }
+
+            fragments.push(fragment);
+            nodesToVisit.push(fragment.selectionSet);
           }
         }
       }
+
       this._recursivelyReferencedFragments.set(definition, fragments);
     }
     return fragments;
@@ -245,9 +273,3 @@ export default class ValidationContext {
     return this._typeInfo.getArgument();
   }
 }
-
-function isExperimentalFragmentWithVariableDefinitions(
-  ast: FragmentDefinitionNode,
-): boolean %checks {
-  return ast.variableDefinitions != null && ast.variableDefinitions.length > 0;
-}
diff --git a/src/validation/__tests__/NoUndefinedVariables-test.js b/src/validation/__tests__/NoUndefinedVariables-test.js
index 67156e4e1d..a3e707ff77 100644
--- a/src/validation/__tests__/NoUndefinedVariables-test.js
+++ b/src/validation/__tests__/NoUndefinedVariables-test.js
@@ -6,7 +6,12 @@
  */
 
 import { describe, it } from 'mocha';
-import { expectPassesRule, expectFailsRule } from './harness';
+import {
+  expectPassesRule,
+  expectFailsRule,
+  expectPassesRuleWithFragmentVariables,
+  expectFailsRuleWithFragmentVariables,
+} from './harness';
 import {
   NoUndefinedVariables,
   undefinedVarMessage,
@@ -332,7 +337,7 @@ describe('Validate: No undefined variables', () => {
 
   // Experimental Fragment Variables
   it('uses all variables in fragments with variable definitions', () => {
-    expectPassesRule(
+    expectPassesRuleWithFragmentVariables(
       NoUndefinedVariables,
       `
       fragment Foo($a: String, $b: String, $c: String) on Type {
@@ -356,7 +361,7 @@ describe('Validate: No undefined variables', () => {
   });
 
   it('variable used in fragment not defined', () => {
-    expectFailsRule(
+    expectFailsRuleWithFragmentVariables(
       NoUndefinedVariables,
       `
       fragment FragA($a: String) on Type {
diff --git a/src/validation/__tests__/NoUnusedVariables-test.js b/src/validation/__tests__/NoUnusedVariables-test.js
index bbdff51ffc..b9762f43d6 100644
--- a/src/validation/__tests__/NoUnusedVariables-test.js
+++ b/src/validation/__tests__/NoUnusedVariables-test.js
@@ -6,7 +6,12 @@
  */
 
 import { describe, it } from 'mocha';
-import { expectPassesRule, expectFailsRule } from './harness';
+import {
+  expectPassesRule,
+  expectFailsRule,
+  expectPassesRuleWithFragmentVariables,
+  expectFailsRuleWithFragmentVariables,
+} from './harness';
 import {
   NoUnusedVariables,
   unusedVariableMessage,
@@ -240,7 +245,7 @@ describe('Validate: No unused variables', () => {
 
   // Experimental Fragment Variables
   it('uses all variables in fragments with variable definitions', () => {
-    expectPassesRule(
+    expectPassesRuleWithFragmentVariables(
       NoUnusedVariables,
       `
       fragment Foo($a: String, $b: String, $c: String) on Type {
@@ -264,7 +269,7 @@ describe('Validate: No unused variables', () => {
   });
 
   it('variable not used by fragment', () => {
-    expectFailsRule(
+    expectFailsRuleWithFragmentVariables(
       NoUnusedVariables,
       `
       fragment FragA($a: String) on Type {
@@ -276,7 +281,7 @@ describe('Validate: No unused variables', () => {
   });
 
   it('variable used in query defined by fragment', () => {
-    expectFailsRule(
+    expectFailsRuleWithFragmentVariables(
       NoUnusedVariables,
       `
       query Foo($a: String) {
@@ -291,7 +296,7 @@ describe('Validate: No unused variables', () => {
     );
 
     it('variable defined in fragment used by query', () => {
-      expectFailsRule(
+      expectFailsRuleWithFragmentVariables(
         NoUnusedVariables,
         `
         query Foo($a: String) {
diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js
index ca92c211c0..ba3664e60f 100644
--- a/src/validation/__tests__/harness.js
+++ b/src/validation/__tests__/harness.js
@@ -421,20 +421,30 @@ export const testSchema = new GraphQLSchema({
   ],
 });
 
-function expectValid(schema, rules, queryString) {
+function expectValid(schema, rules, queryString, options = {}) {
   const errors = validate(
     schema,
-    parse(queryString, { experimentalFragmentVariables: true }),
+    parse(queryString, options),
     rules,
+    null, // default TypeInfo
+    options,
   );
   expect(errors).to.deep.equal([], 'Should validate');
 }
 
-function expectInvalid(schema, rules, queryString, expectedErrors) {
+function expectInvalid(
+  schema,
+  rules,
+  queryString,
+  expectedErrors,
+  options = {},
+) {
   const errors = validate(
     schema,
-    parse(queryString, { experimentalFragmentVariables: true }),
+    parse(queryString, options),
     rules,
+    null, // default TypeInfo
+    options,
   );
   expect(errors).to.have.length.of.at.least(1, 'Should not validate');
   expect(errors).to.deep.equal(expectedErrors);
@@ -456,3 +466,19 @@ export function expectPassesRuleWithSchema(schema, rule, queryString, errors) {
 export function expectFailsRuleWithSchema(schema, rule, queryString, errors) {
   return expectInvalid(schema, [rule], queryString, errors);
 }
+
+export function expectPassesRuleWithFragmentVariables(rule, queryString) {
+  return expectValid(testSchema, [rule], queryString, {
+    experimentalFragmentVariables: true,
+  });
+}
+
+export function expectFailsRuleWithFragmentVariables(
+  rule,
+  queryString,
+  errors,
+) {
+  return expectInvalid(testSchema, [rule], queryString, errors, {
+    experimentalFragmentVariables: true,
+  });
+}
diff --git a/src/validation/rules/NoUndefinedVariables.js b/src/validation/rules/NoUndefinedVariables.js
index b7f78288ca..18ca4b6589 100644
--- a/src/validation/rules/NoUndefinedVariables.js
+++ b/src/validation/rules/NoUndefinedVariables.js
@@ -26,9 +26,8 @@ export function undefinedVarMessage(varName: string, opName: ?string): string {
  * directly and via fragment spreads, are defined by that definition.
  *
  * NOTE: if experimentalFragmentVariables are used, then fragments with
- * variables defined are considered independent "executable definitions".
- * If a fragment defines at least 1 variable, it must define all recursively
- * used variables, excluding other fragments with variables defined.
+ * variable definitions will validate independently, and a variable is not
+ * "encountered" if it's only used in a child with variable definitions.
  */
 export function NoUndefinedVariables(context: ValidationContext): ASTVisitor {
   let variableNameDefined = Object.create(null);
diff --git a/src/validation/rules/NoUnusedVariables.js b/src/validation/rules/NoUnusedVariables.js
index 39aa19fe66..36b3b15c0c 100644
--- a/src/validation/rules/NoUnusedVariables.js
+++ b/src/validation/rules/NoUnusedVariables.js
@@ -29,7 +29,7 @@ export function unusedVariableMessage(
  * definition are used, either directly or within a spread fragment.
  *
  * NOTE: if experimentalFragmentVariables are used, then fragments with
- * variables defined are considered independent "executable definitions".
+ * variable definitions will validate independently.
  * So `query Foo` must not define `$a` when `$a` is only used inside
  * `fragment FragA($a: Type)`
  */
diff --git a/src/validation/validate.js b/src/validation/validate.js
index 61213f8b86..0b8c383a85 100644
--- a/src/validation/validate.js
+++ b/src/validation/validate.js
@@ -17,6 +17,7 @@ import { assertValidSchema } from '../type/validate';
 import { TypeInfo } from '../utilities/TypeInfo';
 import { specifiedRules } from './specifiedRules';
 import ValidationContext from './ValidationContext';
+import type { ValidationOptions } from './ValidationContext';
 
 /**
  * Implements the "Validation" section of the spec.
@@ -39,6 +40,7 @@ export function validate(
   ast: DocumentNode,
   rules?: $ReadOnlyArray<any>,
   typeInfo?: TypeInfo,
+  options?: ValidationOptions,
 ): $ReadOnlyArray<GraphQLError> {
   invariant(ast, 'Must provide document');
   // If the schema used for validation is invalid, throw an error.
@@ -48,6 +50,7 @@ export function validate(
     typeInfo || new TypeInfo(schema),
     ast,
     rules || specifiedRules,
+    options || {},
   );
 }
 
@@ -62,8 +65,9 @@ function visitUsingRules(
   typeInfo: TypeInfo,
   documentAST: DocumentNode,
   rules: $ReadOnlyArray<(ValidationContext) => ASTVisitor>,
+  options: ValidationOptions,
 ): $ReadOnlyArray<GraphQLError> {
-  const context = new ValidationContext(schema, documentAST, typeInfo);
+  const context = new ValidationContext(schema, documentAST, typeInfo, options);
   const visitors = rules.map(rule => rule(context));
   // Visit the whole document with each instance of all provided rules.
   visit(documentAST, visitWithTypeInfo(typeInfo, visitInParallel(visitors)));