Skip to content

Commit 42ec5a5

Browse files
committed
Add 'Symbol.toStringTag' into every exported class
1 parent 9a04b4c commit 42ec5a5

File tree

11 files changed

+132
-6
lines changed

11 files changed

+132
-6
lines changed

.eslintrc.yml

Lines changed: 5 additions & 0 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`
@@ -610,8 +611,12 @@ overrides:
610611
'@typescript-eslint/space-before-function-paren': off
611612
'@typescript-eslint/space-infix-ops': off
612613
'@typescript-eslint/type-annotation-spacing': off
614+
- files: 'src/**'
615+
rules:
616+
internal-rules/require-to-string-tag: error
613617
- files: 'src/**/__*__/**'
614618
rules:
619+
internal-rules/require-to-string-tag: off
615620
node/no-unpublished-import: [error, { allowModules: ['chai', 'mocha'] }]
616621
import/no-restricted-paths: off
617622
import/no-extraneous-dependencies: [error, { devDependencies: true }]

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/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+
});

0 commit comments

Comments
 (0)