diff --git a/src/jsutils/__tests__/instanceOf-test.ts b/src/jsutils/__tests__/instanceOf-test.ts index 5a54a641e5..f0920c80bc 100644 --- a/src/jsutils/__tests__/instanceOf-test.ts +++ b/src/jsutils/__tests__/instanceOf-test.ts @@ -5,15 +5,17 @@ import { instanceOf } from '../instanceOf.js'; describe('instanceOf', () => { it('do not throw on values without prototype', () => { + const fooSymbol: unique symbol = Symbol('Foo'); class Foo { + readonly __kind: symbol = fooSymbol; get [Symbol.toStringTag]() { return 'Foo'; } } - expect(instanceOf(true, Foo)).to.equal(false); - expect(instanceOf(null, Foo)).to.equal(false); - expect(instanceOf(Object.create(null), Foo)).to.equal(false); + expect(instanceOf(true, fooSymbol, Foo)).to.equal(false); + expect(instanceOf(null, fooSymbol, Foo)).to.equal(false); + expect(instanceOf(Object.create(null), fooSymbol, Foo)).to.equal(false); }); it('detect name clashes with older versions of this lib', () => { @@ -23,56 +25,66 @@ describe('instanceOf', () => { } function newVersion() { - class Foo { + const fooSymbol: unique symbol = Symbol('Foo'); + class FooClass { + readonly __kind: symbol = fooSymbol; get [Symbol.toStringTag]() { return 'Foo'; } } - return Foo; + return { fooSymbol, FooClass }; } - const NewClass = newVersion(); + const { fooSymbol: newSymbol, FooClass: NewClass } = newVersion(); const OldClass = oldVersion(); - expect(instanceOf(new NewClass(), NewClass)).to.equal(true); - expect(() => instanceOf(new OldClass(), NewClass)).to.throw(); + expect(instanceOf(new NewClass(), newSymbol, NewClass)).to.equal(true); + expect(() => instanceOf(new OldClass(), newSymbol, NewClass)).to.throw(); }); it('allows instances to have share the same constructor name', () => { function getMinifiedClass(tag: string) { + const someSymbol: unique symbol = Symbol(tag); class SomeNameAfterMinification { + readonly __kind: symbol = someSymbol; get [Symbol.toStringTag]() { return tag; } } - return SomeNameAfterMinification; + return { someSymbol, SomeNameAfterMinification }; } - const Foo = getMinifiedClass('Foo'); - const Bar = getMinifiedClass('Bar'); - expect(instanceOf(new Foo(), Bar)).to.equal(false); - expect(instanceOf(new Bar(), Foo)).to.equal(false); + const { someSymbol: fooSymbol, SomeNameAfterMinification: Foo } = + getMinifiedClass('Foo'); + const { someSymbol: barSymbol, SomeNameAfterMinification: Bar } = + getMinifiedClass('Bar'); + expect(instanceOf(new Foo(), barSymbol, Bar)).to.equal(false); + expect(instanceOf(new Bar(), fooSymbol, Foo)).to.equal(false); - const DuplicateOfFoo = getMinifiedClass('Foo'); - expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw(); - expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw(); + const { + someSymbol: duplicateOfFooSymbol, + SomeNameAfterMinification: DuplicateOfFoo, + } = getMinifiedClass('Foo'); + expect(() => instanceOf(new DuplicateOfFoo(), fooSymbol, Foo)).to.throw(); + expect(() => instanceOf(new Foo(), duplicateOfFooSymbol, Foo)).to.throw(); }); it('fails with descriptive error message', () => { function getFoo() { + const fooSymbol: unique symbol = Symbol('Foo'); class Foo { get [Symbol.toStringTag]() { return 'Foo'; } } - return Foo; + return { fooSymbol, Foo }; } - const Foo1 = getFoo(); - const Foo2 = getFoo(); + const { fooSymbol: foo1Symbol, Foo: Foo1 } = getFoo(); + const { fooSymbol: foo2Symbol, Foo: Foo2 } = getFoo(); - expect(() => instanceOf(new Foo1(), Foo2)).to.throw( + expect(() => instanceOf(new Foo1(), foo2Symbol, Foo2)).to.throw( /^Cannot use Foo "{}" from another module or realm./m, ); - expect(() => instanceOf(new Foo2(), Foo1)).to.throw( + expect(() => instanceOf(new Foo2(), foo1Symbol, Foo1)).to.throw( /^Cannot use Foo "{}" from another module or realm./m, ); }); diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index 66811433ae..2a483ce121 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -7,20 +7,29 @@ const isProduction = process.env.NODE_ENV === 'production'; /** - * A replacement for instanceof which includes an error warning when multi-realm - * constructors are detected. + * A replacement for instanceof relying on a symbol-driven type brand which in + * development mode includes an error warning when multi-realm constructors are + * detected. * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production * See: https://webpack.js.org/guides/production/ */ -export const instanceOf: (value: unknown, constructor: Constructor) => boolean = - /* c8 ignore next 6 */ +export const instanceOf: ( + value: unknown, + symbol: symbol, + constructor: Constructor, +) => boolean = + /* c8 ignore next 9 */ // FIXME: https://github.com/graphql/graphql-js/issues/2317 isProduction - ? function instanceOf(value: unknown, constructor: Constructor): boolean { - return value instanceof constructor; + ? function instanceOf(value: unknown, symbol: symbol): boolean { + return (value as any)?.__kind === symbol; } - : function instanceOf(value: unknown, constructor: Constructor): boolean { - if (value instanceof constructor) { + : function instanceOf( + value: unknown, + symbol: symbol, + constructor: Constructor, + ): boolean { + if ((value as any)?.__kind === symbol) { return true; } if (typeof value === 'object' && value !== null) { diff --git a/src/language/source.ts b/src/language/source.ts index eb21547154..06d78b2c3f 100644 --- a/src/language/source.ts +++ b/src/language/source.ts @@ -6,6 +6,8 @@ interface Location { column: number; } +const sourceSymbol: unique symbol = Symbol('Source'); + /** * A representation of source input to GraphQL. The `name` and `locationOffset` parameters are * optional, but they are useful for clients who store GraphQL documents in source files. @@ -14,6 +16,8 @@ interface Location { * The `line` and `column` properties in `locationOffset` are 1-indexed. */ export class Source { + readonly __kind: symbol; + body: string; name: string; locationOffset: Location; @@ -23,6 +27,7 @@ export class Source { name: string = 'GraphQL request', locationOffset: Location = { line: 1, column: 1 }, ) { + this.__kind = sourceSymbol; this.body = body; this.name = name; this.locationOffset = locationOffset; @@ -47,5 +52,5 @@ export class Source { * @internal */ export function isSource(source: unknown): source is Source { - return instanceOf(source, Source); + return instanceOf(source, sourceSymbol, Source); } diff --git a/src/type/definition.ts b/src/type/definition.ts index 1bbce5df5e..b35c44947e 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -76,11 +76,13 @@ export function assertType(type: unknown): GraphQLType { return type; } +const scalarSymbol: unique symbol = Symbol('Scalar'); + /** * There are predicates for each GraphQL schema element. */ export function isScalarType(type: unknown): type is GraphQLScalarType { - return instanceOf(type, GraphQLScalarType); + return instanceOf(type, scalarSymbol, GraphQLScalarType); } export function assertScalarType(type: unknown): GraphQLScalarType { @@ -90,8 +92,10 @@ export function assertScalarType(type: unknown): GraphQLScalarType { return type; } +const objectSymbol: unique symbol = Symbol('Object'); + export function isObjectType(type: unknown): type is GraphQLObjectType { - return instanceOf(type, GraphQLObjectType); + return instanceOf(type, objectSymbol, GraphQLObjectType); } export function assertObjectType(type: unknown): GraphQLObjectType { @@ -101,8 +105,10 @@ export function assertObjectType(type: unknown): GraphQLObjectType { return type; } +const fieldSymbol: unique symbol = Symbol('Field'); + export function isField(field: unknown): field is GraphQLField { - return instanceOf(field, GraphQLField); + return instanceOf(field, fieldSymbol, GraphQLField); } export function assertField(field: unknown): GraphQLField { @@ -112,8 +118,10 @@ export function assertField(field: unknown): GraphQLField { return field; } +const argumentSymbol: unique symbol = Symbol('Argument'); + export function isArgument(arg: unknown): arg is GraphQLArgument { - return instanceOf(arg, GraphQLArgument); + return instanceOf(arg, argumentSymbol, GraphQLArgument); } export function assertArgument(arg: unknown): GraphQLArgument { @@ -123,8 +131,10 @@ export function assertArgument(arg: unknown): GraphQLArgument { return arg; } +const interfaceSymbol: unique symbol = Symbol('Interface'); + export function isInterfaceType(type: unknown): type is GraphQLInterfaceType { - return instanceOf(type, GraphQLInterfaceType); + return instanceOf(type, interfaceSymbol, GraphQLInterfaceType); } export function assertInterfaceType(type: unknown): GraphQLInterfaceType { @@ -136,8 +146,10 @@ export function assertInterfaceType(type: unknown): GraphQLInterfaceType { return type; } +const unionSymbol: unique symbol = Symbol('Union'); + export function isUnionType(type: unknown): type is GraphQLUnionType { - return instanceOf(type, GraphQLUnionType); + return instanceOf(type, unionSymbol, GraphQLUnionType); } export function assertUnionType(type: unknown): GraphQLUnionType { @@ -147,8 +159,10 @@ export function assertUnionType(type: unknown): GraphQLUnionType { return type; } +const enumSymbol: unique symbol = Symbol('Enum'); + export function isEnumType(type: unknown): type is GraphQLEnumType { - return instanceOf(type, GraphQLEnumType); + return instanceOf(type, enumSymbol, GraphQLEnumType); } export function assertEnumType(type: unknown): GraphQLEnumType { @@ -158,8 +172,10 @@ export function assertEnumType(type: unknown): GraphQLEnumType { return type; } +const enumValueSymbol: unique symbol = Symbol('EnumValue'); + export function isEnumValue(value: unknown): value is GraphQLEnumValue { - return instanceOf(value, GraphQLEnumValue); + return instanceOf(value, enumValueSymbol, GraphQLEnumValue); } export function assertEnumValue(value: unknown): GraphQLEnumValue { @@ -169,10 +185,12 @@ export function assertEnumValue(value: unknown): GraphQLEnumValue { return value; } +const inputObjectSymbol: unique symbol = Symbol('InputObject'); + export function isInputObjectType( type: unknown, ): type is GraphQLInputObjectType { - return instanceOf(type, GraphQLInputObjectType); + return instanceOf(type, inputObjectSymbol, GraphQLInputObjectType); } export function assertInputObjectType(type: unknown): GraphQLInputObjectType { @@ -184,8 +202,10 @@ export function assertInputObjectType(type: unknown): GraphQLInputObjectType { return type; } +const inputFieldSymbol: unique symbol = Symbol('InputField'); + export function isInputField(field: unknown): field is GraphQLInputField { - return instanceOf(field, GraphQLInputField); + return instanceOf(field, inputFieldSymbol, GraphQLInputField); } export function assertInputField(field: unknown): GraphQLInputField { @@ -195,6 +215,8 @@ export function assertInputField(field: unknown): GraphQLInputField { return field; } +const listSymbol: unique symbol = Symbol('List'); + export function isListType( type: GraphQLInputType, ): type is GraphQLList; @@ -203,7 +225,7 @@ export function isListType( ): type is GraphQLList; export function isListType(type: unknown): type is GraphQLList; export function isListType(type: unknown): type is GraphQLList { - return instanceOf(type, GraphQLList); + return instanceOf(type, listSymbol, GraphQLList); } export function assertListType(type: unknown): GraphQLList { @@ -213,6 +235,8 @@ export function assertListType(type: unknown): GraphQLList { return type; } +const nonNullSymbol: unique symbol = Symbol('NonNull'); + export function isNonNullType( type: GraphQLInputType, ): type is GraphQLNonNull; @@ -225,7 +249,7 @@ export function isNonNullType( export function isNonNullType( type: unknown, ): type is GraphQLNonNull { - return instanceOf(type, GraphQLNonNull); + return instanceOf(type, nonNullSymbol, GraphQLNonNull); } export function assertNonNullType( @@ -368,9 +392,11 @@ export function assertAbstractType(type: unknown): GraphQLAbstractType { export class GraphQLList implements GraphQLSchemaElement { + readonly __kind: symbol; readonly ofType: T; constructor(ofType: T) { + this.__kind = listSymbol; this.ofType = ofType; } @@ -411,9 +437,11 @@ export class GraphQLList export class GraphQLNonNull implements GraphQLSchemaElement { + readonly __kind: symbol; readonly ofType: T; constructor(ofType: T) { + this.__kind = nonNullSymbol; this.ofType = ofType; } @@ -651,6 +679,7 @@ export interface GraphQLScalarTypeExtensions { export class GraphQLScalarType implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; specifiedByURL: Maybe; @@ -669,6 +698,7 @@ export class GraphQLScalarType extensionASTNodes: ReadonlyArray; constructor(config: Readonly>) { + this.__kind = scalarSymbol; this.name = assertName(config.name); this.description = config.description; this.specifiedByURL = config.specifiedByURL; @@ -869,6 +899,7 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { export class GraphQLObjectType implements GraphQLSchemaElement { + readonly __kind = objectSymbol; name: string; description: Maybe; isTypeOf: Maybe>; @@ -880,6 +911,7 @@ export class GraphQLObjectType private _interfaces: ThunkReadonlyArray; constructor(config: Readonly>) { + this.__kind = objectSymbol; this.name = assertName(config.name); this.description = config.description; this.isTypeOf = config.isTypeOf; @@ -1093,6 +1125,7 @@ export type GraphQLFieldNormalizedConfigMap = ObjMap< export class GraphQLField implements GraphQLSchemaElement { + readonly __kind: symbol; parentType: | GraphQLObjectType | GraphQLInterfaceType @@ -1115,6 +1148,7 @@ export class GraphQLField name: string, config: GraphQLFieldConfig, ) { + this.__kind = fieldSymbol; this.parentType = parentType; this.name = assertName(name); this.description = config.description; @@ -1166,6 +1200,7 @@ export class GraphQLField } export class GraphQLArgument implements GraphQLSchemaElement { + readonly __kind: symbol; parent: GraphQLField | GraphQLDirective; name: string; description: Maybe; @@ -1181,6 +1216,7 @@ export class GraphQLArgument implements GraphQLSchemaElement { name: string, config: GraphQLArgumentConfig, ) { + this.__kind = argumentSymbol; this.parent = parent; this.name = assertName(name); this.description = config.description; @@ -1270,6 +1306,7 @@ export interface GraphQLInterfaceTypeExtensions { export class GraphQLInterfaceType implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; resolveType: Maybe>; @@ -1281,6 +1318,7 @@ export class GraphQLInterfaceType private _interfaces: ThunkReadonlyArray; constructor(config: Readonly>) { + this.__kind = interfaceSymbol; this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; @@ -1397,6 +1435,7 @@ export interface GraphQLUnionTypeExtensions { * ``` */ export class GraphQLUnionType implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; resolveType: Maybe>; @@ -1407,6 +1446,7 @@ export class GraphQLUnionType implements GraphQLSchemaElement { private _types: ThunkReadonlyArray; constructor(config: Readonly>) { + this.__kind = unionSymbol; this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; @@ -1514,6 +1554,7 @@ export interface GraphQLEnumTypeExtensions { * will be used as its internal value. */ export class GraphQLEnumType /* */ implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; extensions: Readonly; @@ -1528,6 +1569,7 @@ export class GraphQLEnumType /* */ implements GraphQLSchemaElement { private _nameLookup: ObjMap | null; constructor(config: Readonly */>) { + this.__kind = enumSymbol; this.name = assertName(config.name); this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -1738,6 +1780,7 @@ export interface GraphQLEnumValueNormalizedConfig } export class GraphQLEnumValue implements GraphQLSchemaElement { + readonly __kind: symbol; parentEnum: GraphQLEnumType; name: string; description: Maybe; @@ -1751,6 +1794,7 @@ export class GraphQLEnumValue implements GraphQLSchemaElement { name: string, config: GraphQLEnumValueConfig, ) { + this.__kind = enumValueSymbol; this.parentEnum = parentEnum; this.name = assertEnumValueName(name); this.description = config.description; @@ -1818,6 +1862,7 @@ export interface GraphQLInputObjectTypeExtensions { * ``` */ export class GraphQLInputObjectType implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; extensions: Readonly; @@ -1828,6 +1873,7 @@ export class GraphQLInputObjectType implements GraphQLSchemaElement { private _fields: ThunkObjMap; constructor(config: Readonly) { + this.__kind = inputObjectSymbol; this.name = assertName(config.name); this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -1935,6 +1981,7 @@ export type GraphQLInputFieldNormalizedConfigMap = ObjMap; export class GraphQLInputField implements GraphQLSchemaElement { + readonly __kind: symbol; parentType: GraphQLInputObjectType; name: string; description: Maybe; @@ -1955,6 +2002,7 @@ export class GraphQLInputField implements GraphQLSchemaElement { `${parentType}.${name} field has a resolve property, but Input Types cannot define resolvers.`, ); + this.__kind = inputFieldSymbol; this.parentType = parentType; this.name = assertName(name); this.description = config.description; diff --git a/src/type/directives.ts b/src/type/directives.ts index 96f5b6b65a..a3170aed26 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -19,11 +19,13 @@ import type { import { GraphQLArgument, GraphQLNonNull } from './definition.js'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from './scalars.js'; +const directiveSymbol: unique symbol = Symbol('Directive'); + /** * Test if the given value is a GraphQL directive. */ export function isDirective(directive: unknown): directive is GraphQLDirective { - return instanceOf(directive, GraphQLDirective); + return instanceOf(directive, directiveSymbol, GraphQLDirective); } export function assertDirective(directive: unknown): GraphQLDirective { @@ -53,6 +55,7 @@ export interface GraphQLDirectiveExtensions { * behavior. Type system creators will usually not create these directly. */ export class GraphQLDirective implements GraphQLSchemaElement { + readonly __kind: symbol; name: string; description: Maybe; locations: ReadonlyArray; @@ -62,6 +65,7 @@ export class GraphQLDirective implements GraphQLSchemaElement { astNode: Maybe; constructor(config: Readonly) { + this.__kind = directiveSymbol; this.name = assertName(config.name); this.description = config.description; this.locations = config.locations; diff --git a/src/type/schema.ts b/src/type/schema.ts index 0f19c5998c..e8600eb980 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -41,7 +41,7 @@ import { * Test if the given value is a GraphQL schema. */ export function isSchema(schema: unknown): schema is GraphQLSchema { - return instanceOf(schema, GraphQLSchema); + return instanceOf(schema, schemaSymbol, GraphQLSchema); } export function assertSchema(schema: unknown): GraphQLSchema { @@ -64,6 +64,8 @@ export interface GraphQLSchemaExtensions { [attributeName: string | symbol]: unknown; } +const schemaSymbol: unique symbol = Symbol('Schema'); + /** * Schema Definition * @@ -133,6 +135,7 @@ export interface GraphQLSchemaExtensions { * ``` */ export class GraphQLSchema { + readonly __kind = schemaSymbol; description: Maybe; extensions: Readonly; astNode: Maybe;