Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions src/jsutils/__tests__/instanceOf-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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,
);
});
Expand Down
25 changes: 17 additions & 8 deletions src/jsutils/instanceOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/language/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
Loading
Loading