From 67162e5f2aeb642c436a21a664fe57b0220ede3f Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Fri, 13 Jul 2018 19:52:06 +0300
Subject: [PATCH] Fix memory leak in buildSchema/extendSchema

---
 .eslintrc              |   2 +-
 src/type/definition.js | 140 +++++++++++++++++++++--------------------
 2 files changed, 72 insertions(+), 70 deletions(-)

diff --git a/.eslintrc b/.eslintrc
index f0988229eb..c65d4a32c5 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -42,7 +42,7 @@
   "rules": {
     "prettier/prettier": 2,
 
-    "flowtype/space-after-type-colon": [2, "always"],
+    "flowtype/space-after-type-colon": 0,
     "flowtype/space-before-type-colon": [2, "never"],
     "flowtype/space-before-generic-bracket": [2, "never"],
     "flowtype/union-intersection-spacing": [2, "always"],
diff --git a/src/type/definition.js b/src/type/definition.js
index b74dcde582..fd288f3c42 100644
--- a/src/type/definition.js
+++ b/src/type/definition.js
@@ -649,9 +649,8 @@ export class GraphQLObjectType {
   extensionASTNodes: ?$ReadOnlyArray<ObjectTypeExtensionNode>;
   isTypeOf: ?GraphQLIsTypeOfFn<*, *>;
 
-  _typeConfig: GraphQLObjectTypeConfig<*, *>;
-  _fields: GraphQLFieldMap<*, *>;
-  _interfaces: Array<GraphQLInterfaceType>;
+  _fields: Thunk<GraphQLFieldMap<*, *>>;
+  _interfaces: Thunk<Array<GraphQLInterfaceType>>;
 
   constructor(config: GraphQLObjectTypeConfig<*, *>): void {
     this.name = config.name;
@@ -659,7 +658,8 @@ export class GraphQLObjectType {
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes;
     this.isTypeOf = config.isTypeOf;
-    this._typeConfig = config;
+    this._fields = defineFieldMap.bind(undefined, config);
+    this._interfaces = defineInterfaces.bind(undefined, config);
     invariant(typeof config.name === 'string', 'Must provide name.');
     if (config.isTypeOf) {
       invariant(
@@ -670,17 +670,17 @@ export class GraphQLObjectType {
   }
 
   getFields(): GraphQLFieldMap<*, *> {
-    return (
-      this._fields ||
-      (this._fields = defineFieldMap(this, this._typeConfig.fields))
-    );
+    if (typeof this._fields === 'function') {
+      this._fields = this._fields();
+    }
+    return this._fields;
   }
 
   getInterfaces(): Array<GraphQLInterfaceType> {
-    return (
-      this._interfaces ||
-      (this._interfaces = defineInterfaces(this, this._typeConfig.interfaces))
-    );
+    if (typeof this._interfaces === 'function') {
+      this._interfaces = this._interfaces();
+    }
+    return this._interfaces;
   }
 
   toString(): string {
@@ -693,26 +693,26 @@ defineToStringTag(GraphQLObjectType);
 defineToJSON(GraphQLObjectType);
 
 function defineInterfaces(
-  type: GraphQLObjectType,
-  interfacesThunk: Thunk<?Array<GraphQLInterfaceType>>,
+  config: GraphQLObjectTypeConfig<*, *>,
 ): Array<GraphQLInterfaceType> {
-  const interfaces = resolveThunk(interfacesThunk) || [];
+  const interfaces = resolveThunk(config.interfaces) || [];
   invariant(
     Array.isArray(interfaces),
-    `${type.name} interfaces must be an Array or a function which returns ` +
+    `${config.name} interfaces must be an Array or a function which returns ` +
       'an Array.',
   );
   return interfaces;
 }
 
 function defineFieldMap<TSource, TContext>(
-  type: GraphQLNamedType,
-  fieldsThunk: Thunk<GraphQLFieldConfigMap<TSource, TContext>>,
+  config:
+    | GraphQLObjectTypeConfig<TSource, TContext>
+    | GraphQLInterfaceTypeConfig<TSource, TContext>,
 ): GraphQLFieldMap<TSource, TContext> {
-  const fieldMap = resolveThunk(fieldsThunk) || {};
+  const fieldMap = resolveThunk(config.fields) || {};
   invariant(
     isPlainObj(fieldMap),
-    `${type.name} fields must be an object with field names as keys or a ` +
+    `${config.name} fields must be an object with field names as keys or a ` +
       'function which returns such an object.',
   );
 
@@ -721,12 +721,12 @@ function defineFieldMap<TSource, TContext>(
     const fieldConfig = fieldMap[fieldName];
     invariant(
       isPlainObj(fieldConfig),
-      `${type.name}.${fieldName} field config must be an object`,
+      `${config.name}.${fieldName} field config must be an object`,
     );
     invariant(
       !fieldConfig.hasOwnProperty('isDeprecated'),
-      `${type.name}.${fieldName} should provide "deprecationReason" instead ` +
-        'of "isDeprecated".',
+      `${config.name}.${fieldName} should provide "deprecationReason" ` +
+        'instead of "isDeprecated".',
     );
     const field = {
       ...fieldConfig,
@@ -735,7 +735,7 @@ function defineFieldMap<TSource, TContext>(
     };
     invariant(
       isValidResolver(field.resolve),
-      `${type.name}.${fieldName} field resolver must be a function if ` +
+      `${config.name}.${fieldName} field resolver must be a function if ` +
         `provided, but got: ${inspect(field.resolve)}.`,
     );
     const argsConfig = fieldConfig.args;
@@ -744,7 +744,7 @@ function defineFieldMap<TSource, TContext>(
     } else {
       invariant(
         isPlainObj(argsConfig),
-        `${type.name}.${fieldName} args must be an object with argument ` +
+        `${config.name}.${fieldName} args must be an object with argument ` +
           'names as keys.',
       );
       field.args = Object.keys(argsConfig).map(argName => {
@@ -903,8 +903,7 @@ export class GraphQLInterfaceType {
   extensionASTNodes: ?$ReadOnlyArray<InterfaceTypeExtensionNode>;
   resolveType: ?GraphQLTypeResolver<*, *>;
 
-  _typeConfig: GraphQLInterfaceTypeConfig<*, *>;
-  _fields: GraphQLFieldMap<*, *>;
+  _fields: Thunk<GraphQLFieldMap<*, *>>;
 
   constructor(config: GraphQLInterfaceTypeConfig<*, *>): void {
     this.name = config.name;
@@ -912,7 +911,7 @@ export class GraphQLInterfaceType {
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes;
     this.resolveType = config.resolveType;
-    this._typeConfig = config;
+    this._fields = defineFieldMap.bind(undefined, config);
     invariant(typeof config.name === 'string', 'Must provide name.');
     if (config.resolveType) {
       invariant(
@@ -923,10 +922,10 @@ export class GraphQLInterfaceType {
   }
 
   getFields(): GraphQLFieldMap<*, *> {
-    return (
-      this._fields ||
-      (this._fields = defineFieldMap(this, this._typeConfig.fields))
-    );
+    if (typeof this._fields === 'function') {
+      this._fields = this._fields();
+    }
+    return this._fields;
   }
 
   toString(): string {
@@ -982,8 +981,7 @@ export class GraphQLUnionType {
   extensionASTNodes: ?$ReadOnlyArray<UnionTypeExtensionNode>;
   resolveType: ?GraphQLTypeResolver<*, *>;
 
-  _typeConfig: GraphQLUnionTypeConfig<*, *>;
-  _types: Array<GraphQLObjectType>;
+  _types: Thunk<Array<GraphQLObjectType>>;
 
   constructor(config: GraphQLUnionTypeConfig<*, *>): void {
     this.name = config.name;
@@ -991,7 +989,7 @@ export class GraphQLUnionType {
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes;
     this.resolveType = config.resolveType;
-    this._typeConfig = config;
+    this._types = defineTypes.bind(undefined, config);
     invariant(typeof config.name === 'string', 'Must provide name.');
     if (config.resolveType) {
       invariant(
@@ -1002,9 +1000,10 @@ export class GraphQLUnionType {
   }
 
   getTypes(): Array<GraphQLObjectType> {
-    return (
-      this._types || (this._types = defineTypes(this, this._typeConfig.types))
-    );
+    if (typeof this._types === 'function') {
+      this._types = this._types();
+    }
+    return this._types;
   }
 
   toString(): string {
@@ -1017,14 +1016,13 @@ defineToStringTag(GraphQLUnionType);
 defineToJSON(GraphQLUnionType);
 
 function defineTypes(
-  unionType: GraphQLUnionType,
-  typesThunk: Thunk<Array<GraphQLObjectType>>,
+  config: GraphQLUnionTypeConfig<*, *>,
 ): Array<GraphQLObjectType> {
-  const types = resolveThunk(typesThunk) || [];
+  const types = resolveThunk(config.types) || [];
   invariant(
     Array.isArray(types),
     'Must provide Array of types or a function which returns ' +
-      `such an array for Union ${unionType.name}.`,
+      `such an array for Union ${config.name}.`,
   );
   return types;
 }
@@ -1216,43 +1214,22 @@ export class GraphQLInputObjectType {
   astNode: ?InputObjectTypeDefinitionNode;
   extensionASTNodes: ?$ReadOnlyArray<InputObjectTypeExtensionNode>;
 
-  _typeConfig: GraphQLInputObjectTypeConfig;
-  _fields: GraphQLInputFieldMap;
+  _fields: Thunk<GraphQLInputFieldMap>;
 
   constructor(config: GraphQLInputObjectTypeConfig): void {
     this.name = config.name;
     this.description = config.description;
     this.astNode = config.astNode;
     this.extensionASTNodes = config.extensionASTNodes;
-    this._typeConfig = config;
+    this._fields = defineInputFieldMap.bind(undefined, config);
     invariant(typeof config.name === 'string', 'Must provide name.');
   }
 
   getFields(): GraphQLInputFieldMap {
-    return this._fields || (this._fields = this._defineFieldMap());
-  }
-
-  _defineFieldMap(): GraphQLInputFieldMap {
-    const fieldMap: any = resolveThunk(this._typeConfig.fields) || {};
-    invariant(
-      isPlainObj(fieldMap),
-      `${this.name} fields must be an object with field names as keys or a ` +
-        'function which returns such an object.',
-    );
-    const resultFieldMap = Object.create(null);
-    Object.keys(fieldMap).forEach(fieldName => {
-      const field = {
-        ...fieldMap[fieldName],
-        name: fieldName,
-      };
-      invariant(
-        !field.hasOwnProperty('resolve'),
-        `${this.name}.${fieldName} field type has a resolve property, but ` +
-          'Input Types cannot define resolvers.',
-      );
-      resultFieldMap[fieldName] = field;
-    });
-    return resultFieldMap;
+    if (typeof this._fields === 'function') {
+      this._fields = this._fields();
+    }
+    return this._fields;
   }
 
   toString(): string {
@@ -1264,6 +1241,31 @@ export class GraphQLInputObjectType {
 defineToStringTag(GraphQLInputObjectType);
 defineToJSON(GraphQLInputObjectType);
 
+function defineInputFieldMap(
+  config: GraphQLInputObjectTypeConfig,
+): GraphQLInputFieldMap {
+  const fieldMap: any = resolveThunk(config.fields) || {};
+  invariant(
+    isPlainObj(fieldMap),
+    `${config.name} fields must be an object with field names as keys or a ` +
+      'function which returns such an object.',
+  );
+  const resultFieldMap = Object.create(null);
+  Object.keys(fieldMap).forEach(fieldName => {
+    const field = {
+      ...fieldMap[fieldName],
+      name: fieldName,
+    };
+    invariant(
+      !field.hasOwnProperty('resolve'),
+      `${config.name}.${fieldName} field type has a resolve property, but ` +
+        'Input Types cannot define resolvers.',
+    );
+    resultFieldMap[fieldName] = field;
+  });
+  return resultFieldMap;
+}
+
 export type GraphQLInputObjectTypeConfig = {|
   name: string,
   fields: Thunk<GraphQLInputFieldConfigMap>,