Skip to content

Commit 0810817

Browse files
committed
Merge remote-tracking branch 'upstream/main' into tsdoc-eslint
2 parents 085c5f0 + 58122ef commit 0810817

File tree

15 files changed

+189
-21
lines changed

15 files changed

+189
-21
lines changed

.eslintrc.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ rules:
2222

2323
internal-rules/only-ascii: error
2424
internal-rules/no-dir-import: error
25+
internal-rules/require-to-string-tag: off
2526

2627
##############################################################################
2728
# `eslint-plugin-istanbul` rule list based on `v0.1.2`
@@ -452,6 +453,14 @@ overrides:
452453
extends:
453454
- plugin:import/typescript
454455
rules:
456+
##########################################################################
457+
# Validating TS Doc comments
458+
##########################################################################
459+
460+
# Supported Rules
461+
# https://tsdoc.org/pages/packages/eslint-plugin-tsdoc/
462+
'tsdoc/syntax': error
463+
455464
##########################################################################
456465
# `@typescript-eslint/eslint-plugin` rule list based on `v4.25.x`
457466
##########################################################################
@@ -612,12 +621,12 @@ overrides:
612621
'@typescript-eslint/space-infix-ops': off
613622
'@typescript-eslint/type-annotation-spacing': off
614623

615-
# Validating TS Doc comments. see:
616-
# https://tsdoc.org/pages/packages/eslint-plugin-tsdoc/
617-
'tsdoc/syntax': error
618-
624+
- files: 'src/**'
625+
rules:
626+
internal-rules/require-to-string-tag: error
619627
- files: 'src/**/__*__/**'
620628
rules:
629+
internal-rules/require-to-string-tag: off
621630
node/no-unpublished-import: [error, { allowModules: ['chai', 'mocha'] }]
622631
import/no-restricted-paths: off
623632
import/no-extraneous-dependencies: [error, { devDependencies: true }]

.mocharc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ throw-deprecation: true
22
check-leaks: true
33
require:
44
- 'resources/ts-register.js'
5+
extension:
6+
- 'ts'

resources/eslint-internal-rules/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
const onlyASCII = require('./only-ascii.js');
44
const noDirImport = require('./no-dir-import.js');
5+
const requireToStringTag = require('./require-to-string-tag.js');
56

67
module.exports = {
78
rules: {
89
'only-ascii': onlyASCII,
910
'no-dir-import': noDirImport,
11+
'require-to-string-tag': requireToStringTag,
1012
},
1113
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
module.exports = function requireToStringTag(context) {
4+
const sourceCode = context.getSourceCode();
5+
6+
return {
7+
'ExportNamedDeclaration > ClassDeclaration': (classNode) => {
8+
const properties = classNode.body.body;
9+
if (properties.some(isToStringTagProperty)) {
10+
return;
11+
}
12+
13+
const jsDoc = context.getJSDocComment(classNode)?.value;
14+
// FIXME: use proper TSDoc parser instead of includes once we fix TSDoc comments
15+
if (jsDoc?.includes('@internal') === true) {
16+
return;
17+
}
18+
19+
context.report({
20+
node: classNode,
21+
message:
22+
'All classes in public API required to have [Symbol.toStringTag] method',
23+
});
24+
},
25+
};
26+
27+
function isToStringTagProperty(propertyNode) {
28+
if (
29+
propertyNode.type !== 'MethodDefinition' ||
30+
propertyNode.kind !== 'get'
31+
) {
32+
return false;
33+
}
34+
const keyText = sourceCode.getText(propertyNode.key);
35+
return keyText === 'Symbol.toStringTag';
36+
}
37+
};

src/jsutils/__tests__/instanceOf-test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,43 @@ import { describe, it } from 'mocha';
44
import { instanceOf } from '../instanceOf';
55

66
describe('instanceOf', () => {
7+
it('allows instances to have share the same constructor name', () => {
8+
function getMinifiedClass(tag: string) {
9+
class SomeNameAfterMinification {
10+
get [Symbol.toStringTag]() {
11+
return tag;
12+
}
13+
}
14+
return SomeNameAfterMinification;
15+
}
16+
17+
const Foo = getMinifiedClass('Foo');
18+
const Bar = getMinifiedClass('Bar');
19+
expect(instanceOf(new Foo(), Bar)).to.equal(false);
20+
expect(instanceOf(new Bar(), Foo)).to.equal(false);
21+
22+
const DuplicateOfFoo = getMinifiedClass('Foo');
23+
expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw();
24+
expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw();
25+
});
26+
727
it('fails with descriptive error message', () => {
828
function getFoo() {
9-
class Foo {}
29+
class Foo {
30+
get [Symbol.toStringTag]() {
31+
return 'Foo';
32+
}
33+
}
1034
return Foo;
1135
}
1236
const Foo1 = getFoo();
1337
const Foo2 = getFoo();
1438

1539
expect(() => instanceOf(new Foo1(), Foo2)).to.throw(
16-
/^Cannot use Foo "\[object Object\]" from another module or realm./m,
40+
/^Cannot use Foo "{}" from another module or realm./m,
1741
);
1842
expect(() => instanceOf(new Foo2(), Foo1)).to.throw(
19-
/^Cannot use Foo "\[object Object\]" from another module or realm./m,
43+
/^Cannot use Foo "{}" from another module or realm./m,
2044
);
2145
});
2246
});

src/jsutils/instanceOf.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { inspect } from './inspect';
2+
13
/**
24
* A replacement for instanceof which includes an error warning when multi-realm
35
* constructors are detected.
@@ -10,16 +12,23 @@ export const instanceOf: (value: unknown, constructor: Constructor) => boolean =
1012
function instanceOf(value: unknown, constructor: Constructor): boolean {
1113
return value instanceof constructor;
1214
}
13-
: function instanceOf(value: any, constructor: Constructor): boolean {
15+
: function instanceOf(value: unknown, constructor: Constructor): boolean {
1416
if (value instanceof constructor) {
1517
return true;
1618
}
17-
if (value) {
18-
const valueClass = value.constructor;
19-
const className = constructor.name;
20-
if (className && valueClass && valueClass.name === className) {
19+
if (typeof value === 'object' && value !== null) {
20+
// Prefer Symbol.toStringTag since it is immune to minification.
21+
const className = constructor.prototype[Symbol.toStringTag];
22+
const valueClassName =
23+
// We still need to support constructor's name to detect conflicts with older versions of this library.
24+
Symbol.toStringTag in value
25+
? // @ts-expect-error TS bug see, https://github.com/microsoft/TypeScript/issues/38009
26+
value[Symbol.toStringTag]
27+
: value.constructor?.name;
28+
if (className === valueClassName) {
29+
const stringifiedValue = inspect(value);
2130
throw new Error(
22-
`Cannot use ${className} "${value}" from another module or realm.
31+
`Cannot use ${className} "${stringifiedValue}" from another module or realm.
2332
2433
Ensure that there is only one instance of "graphql" in the node_modules
2534
directory. If different versions of "graphql" are the dependencies of other
@@ -38,5 +47,7 @@ spurious results.`,
3847
};
3948

4049
interface Constructor extends Function {
41-
name: string;
50+
prototype: {
51+
[Symbol.toStringTag]: string;
52+
};
4253
}

src/language/__tests__/lexer-test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,13 @@ describe('Lexer', () => {
114114
});
115115
});
116116

117-
it('can be JSON.stringified, util.inspected or jsutils.inspect', () => {
118-
const token = lexOne('foo');
117+
it('can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
118+
const lexer = new Lexer(new Source('foo'));
119+
const token = lexer.advance();
120+
121+
expect(Object.prototype.toString.call(lexer)).to.equal('[object Lexer]');
122+
123+
expect(Object.prototype.toString.call(token)).to.equal('[object Token]');
119124
expect(JSON.stringify(token)).to.equal(
120125
'{"kind":"Name","value":"foo","line":1,"column":1}',
121126
);

src/language/__tests__/parser-test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,12 @@ describe('Parser', () => {
372372
expect(() => parse(document)).to.throw('Syntax Error');
373373
});
374374

375-
it('contains location information that only stringifies start/end', () => {
376-
const result = parse('{ id }');
375+
it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
376+
const { loc } = parse('{ id }');
377377

378-
expect(JSON.stringify(result.loc)).to.equal('{"start":0,"end":6}');
379-
expect(inspect(result.loc)).to.equal('{ start: 0, end: 6 }');
378+
expect(Object.prototype.toString.call(loc)).to.equal('[object Location]');
379+
expect(JSON.stringify(loc)).to.equal('{"start":0,"end":6}');
380+
expect(inspect(loc)).to.equal('{ start: 0, end: 6 }');
380381
});
381382

382383
it('contains references to source', () => {

src/language/ast.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export class Location {
4242
toJSON(): { start: number; end: number } {
4343
return { start: this.start, end: this.end };
4444
}
45+
46+
get [Symbol.toStringTag]() {
47+
return 'Location';
48+
}
4549
}
4650

4751
/**
@@ -121,6 +125,10 @@ export class Token {
121125
column: this.column,
122126
};
123127
}
128+
129+
get [Symbol.toStringTag]() {
130+
return 'Token';
131+
}
124132
}
125133

126134
/**

src/language/lexer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export class Lexer {
7979
}
8080
return token;
8181
}
82+
83+
get [Symbol.toStringTag]() {
84+
return 'Lexer';
85+
}
8286
}
8387

8488
/**

src/utilities/TypeInfo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ export class TypeInfo {
292292
break;
293293
}
294294
}
295+
296+
get [Symbol.toStringTag]() {
297+
return 'TypeInfo';
298+
}
295299
}
296300

297301
type GetFieldDefFn = (

src/utilities/__tests__/TypeInfo-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ import { TypeInfo, visitWithTypeInfo } from '../TypeInfo';
1515
import { testSchema } from '../../validation/__tests__/harness';
1616

1717
describe('TypeInfo', () => {
18+
it('can be Object.toStringified', () => {
19+
const typeInfo = new TypeInfo(testSchema);
20+
21+
expect(Object.prototype.toString.call(typeInfo)).to.equal(
22+
'[object TypeInfo]',
23+
);
24+
});
25+
1826
it('allow all methods to be called before entering any node', () => {
1927
const typeInfo = new TypeInfo(testSchema);
2028

src/validation/ValidationContext.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ export class ASTValidationContext {
131131
}
132132
return fragments;
133133
}
134+
135+
get [Symbol.toStringTag]() {
136+
return 'ASTValidationContext';
137+
}
134138
}
135139

136140
export type ASTValidationRule = (context: ASTValidationContext) => ASTVisitor;
@@ -150,6 +154,10 @@ export class SDLValidationContext extends ASTValidationContext {
150154
getSchema(): Maybe<GraphQLSchema> {
151155
return this._schema;
152156
}
157+
158+
get [Symbol.toStringTag]() {
159+
return 'SDLValidationContext';
160+
}
153161
}
154162

155163
export type SDLValidationRule = (context: SDLValidationContext) => ASTVisitor;
@@ -253,6 +261,10 @@ export class ValidationContext extends ASTValidationContext {
253261
getEnumValue(): Maybe<GraphQLEnumValue> {
254262
return this._typeInfo.getEnumValue();
255263
}
264+
265+
get [Symbol.toStringTag]() {
266+
return 'ValidationContext';
267+
}
256268
}
257269

258270
export type ValidationRule = (context: ValidationContext) => ASTVisitor;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { identityFunc } from '../../jsutils/identityFunc';
5+
6+
import { parse } from '../../language/parser';
7+
8+
import { GraphQLSchema } from '../../type/schema';
9+
10+
import { TypeInfo } from '../../utilities/TypeInfo';
11+
12+
import {
13+
ASTValidationContext,
14+
SDLValidationContext,
15+
ValidationContext,
16+
} from '../ValidationContext';
17+
18+
describe('ValidationContext', () => {
19+
it('can be Object.toStringified', () => {
20+
const schema = new GraphQLSchema({});
21+
const typeInfo = new TypeInfo(schema);
22+
const ast = parse('{ foo }');
23+
const onError = identityFunc;
24+
25+
const astContext = new ASTValidationContext(ast, onError);
26+
expect(Object.prototype.toString.call(astContext)).to.equal(
27+
'[object ASTValidationContext]',
28+
);
29+
30+
const sdlContext = new SDLValidationContext(ast, schema, onError);
31+
expect(Object.prototype.toString.call(sdlContext)).to.equal(
32+
'[object SDLValidationContext]',
33+
);
34+
35+
const context = new ValidationContext(schema, ast, typeInfo, onError);
36+
expect(Object.prototype.toString.call(context)).to.equal(
37+
'[object ValidationContext]',
38+
);
39+
});
40+
});

src/validation/rules/SingleFieldSubscriptionsRule.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import type {
88
} from '../../language/ast';
99
import { Kind } from '../../language/kinds';
1010

11-
import type { ValidationContext } from '../ValidationContext';
1211
import type { ExecutionContext } from '../../execution/execute';
1312
import {
1413
collectFields,
1514
defaultFieldResolver,
1615
defaultTypeResolver,
1716
} from '../../execution/execute';
1817

18+
import type { ValidationContext } from '../ValidationContext';
19+
1920
/**
2021
* Subscriptions must only include a non-introspection field.
2122
*

0 commit comments

Comments
 (0)