From 03e0e187c4ada1bfa3c7845c94e13af4490e5282 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 12 Jun 2021 00:09:43 +0300 Subject: [PATCH] Add 'Symbol.toStringTag' into every exported class --- .eslintrc.yml | 5 +++ resources/eslint-internal-rules/index.js | 2 + .../require-to-string-tag.js | 34 ++++++++++++++++ src/language/__tests__/lexer-test.ts | 9 ++++- src/language/__tests__/parser-test.ts | 9 +++-- src/language/ast.ts | 8 ++++ src/language/lexer.ts | 4 ++ src/utilities/TypeInfo.ts | 4 ++ src/utilities/__tests__/TypeInfo-test.ts | 8 ++++ src/validation/ValidationContext.ts | 12 ++++++ .../__tests__/ValidationContext-test.ts | 40 +++++++++++++++++++ 11 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 resources/eslint-internal-rules/require-to-string-tag.js create mode 100644 src/validation/__tests__/ValidationContext-test.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 8b764927564..1c8c8c49689 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -22,6 +22,7 @@ rules: internal-rules/only-ascii: error internal-rules/no-dir-import: error + internal-rules/require-to-string-tag: off ############################################################################## # `eslint-plugin-istanbul` rule list based on `v0.1.2` @@ -610,8 +611,12 @@ overrides: '@typescript-eslint/space-before-function-paren': off '@typescript-eslint/space-infix-ops': off '@typescript-eslint/type-annotation-spacing': off + - files: 'src/**' + rules: + internal-rules/require-to-string-tag: error - files: 'src/**/__*__/**' rules: + internal-rules/require-to-string-tag: off node/no-unpublished-import: [error, { allowModules: ['chai', 'mocha'] }] import/no-restricted-paths: off import/no-extraneous-dependencies: [error, { devDependencies: true }] diff --git a/resources/eslint-internal-rules/index.js b/resources/eslint-internal-rules/index.js index d63500e5cf4..4acc530f3a9 100644 --- a/resources/eslint-internal-rules/index.js +++ b/resources/eslint-internal-rules/index.js @@ -2,10 +2,12 @@ const onlyASCII = require('./only-ascii.js'); const noDirImport = require('./no-dir-import.js'); +const requireToStringTag = require('./require-to-string-tag.js'); module.exports = { rules: { 'only-ascii': onlyASCII, 'no-dir-import': noDirImport, + 'require-to-string-tag': requireToStringTag, }, }; diff --git a/resources/eslint-internal-rules/require-to-string-tag.js b/resources/eslint-internal-rules/require-to-string-tag.js new file mode 100644 index 00000000000..b1f5f329748 --- /dev/null +++ b/resources/eslint-internal-rules/require-to-string-tag.js @@ -0,0 +1,34 @@ +'use strict'; + +module.exports = function requireToStringTag(context) { + const sourceCode = context.getSourceCode(); + + return { + 'ExportNamedDeclaration > ClassDeclaration': (classNode) => { + const properties = classNode.body.body; + if (properties.some(isToStringTagProperty)) { + return; + } + + const jsDoc = context.getJSDocComment(classNode)?.value; + // FIXME: use proper TSDoc parser instead of includes once we fix TSDoc comments + if (jsDoc?.includes('@internal') === true) { + return; + } + + context.report({ + node: classNode, + message: + 'All classes in public API required to have [Symbol.toStringTag] method', + }); + }, + }; +}; + +function isToStringTagProperty(propertyNode) { + if (propertyNode.type !== 'MethodDefinition' || propertyNode.kind !== 'get') { + return false; + } + const keyText = sourceCode.getText(propertyNode.key); + return keyText === 'Symbol.toStringTag'; +} diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts index 053c3297092..d98f68b051e 100644 --- a/src/language/__tests__/lexer-test.ts +++ b/src/language/__tests__/lexer-test.ts @@ -114,8 +114,13 @@ describe('Lexer', () => { }); }); - it('can be JSON.stringified, util.inspected or jsutils.inspect', () => { - const token = lexOne('foo'); + it('can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => { + const lexer = new Lexer(new Source('foo')); + const token = lexer.advance(); + + expect(Object.prototype.toString.call(lexer)).to.equal('[object Lexer]'); + + expect(Object.prototype.toString.call(token)).to.equal('[object Token]'); expect(JSON.stringify(token)).to.equal( '{"kind":"Name","value":"foo","line":1,"column":1}', ); diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index d042bec2914..ffdfe11b461 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -372,11 +372,12 @@ describe('Parser', () => { expect(() => parse(document)).to.throw('Syntax Error'); }); - it('contains location information that only stringifies start/end', () => { - const result = parse('{ id }'); + it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => { + const { loc } = parse('{ id }'); - expect(JSON.stringify(result.loc)).to.equal('{"start":0,"end":6}'); - expect(inspect(result.loc)).to.equal('{ start: 0, end: 6 }'); + expect(Object.prototype.toString.call(loc)).to.equal('[object Location]'); + expect(JSON.stringify(loc)).to.equal('{"start":0,"end":6}'); + expect(inspect(loc)).to.equal('{ start: 0, end: 6 }'); }); it('contains references to source', () => { diff --git a/src/language/ast.ts b/src/language/ast.ts index 62ddf24c6b7..ccb6240111a 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -42,6 +42,10 @@ export class Location { toJSON(): { start: number; end: number } { return { start: this.start, end: this.end }; } + + get [Symbol.toStringTag]() { + return 'Location'; + } } /** @@ -121,6 +125,10 @@ export class Token { column: this.column, }; } + + get [Symbol.toStringTag]() { + return 'Token'; + } } /** diff --git a/src/language/lexer.ts b/src/language/lexer.ts index b5637e388d1..53f6544acce 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -79,6 +79,10 @@ export class Lexer { } return token; } + + get [Symbol.toStringTag]() { + return 'Lexer'; + } } /** diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index f9ba319ce60..e92dfe53cd2 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -292,6 +292,10 @@ export class TypeInfo { break; } } + + get [Symbol.toStringTag]() { + return 'TypeInfo'; + } } type GetFieldDefFn = ( diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index 57b1d67491d..1d28eb7d056 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -15,6 +15,14 @@ import { TypeInfo, visitWithTypeInfo } from '../TypeInfo'; import { testSchema } from '../../validation/__tests__/harness'; describe('TypeInfo', () => { + it('can be Object.toStringified', () => { + const typeInfo = new TypeInfo(testSchema); + + expect(Object.prototype.toString.call(typeInfo)).to.equal( + '[object TypeInfo]', + ); + }); + it('allow all methods to be called before entering any node', () => { const typeInfo = new TypeInfo(testSchema); diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 93bf43d391b..cf6eaa1239a 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -131,6 +131,10 @@ export class ASTValidationContext { } return fragments; } + + get [Symbol.toStringTag]() { + return 'ASTValidationContext'; + } } export type ASTValidationRule = (context: ASTValidationContext) => ASTVisitor; @@ -150,6 +154,10 @@ export class SDLValidationContext extends ASTValidationContext { getSchema(): Maybe { return this._schema; } + + get [Symbol.toStringTag]() { + return 'SDLValidationContext'; + } } export type SDLValidationRule = (context: SDLValidationContext) => ASTVisitor; @@ -253,6 +261,10 @@ export class ValidationContext extends ASTValidationContext { getEnumValue(): Maybe { return this._typeInfo.getEnumValue(); } + + get [Symbol.toStringTag]() { + return 'ValidationContext'; + } } export type ValidationRule = (context: ValidationContext) => ASTVisitor; diff --git a/src/validation/__tests__/ValidationContext-test.ts b/src/validation/__tests__/ValidationContext-test.ts new file mode 100644 index 00000000000..159aa305490 --- /dev/null +++ b/src/validation/__tests__/ValidationContext-test.ts @@ -0,0 +1,40 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { identityFunc } from '../../jsutils/identityFunc'; + +import { parse } from '../../language/parser'; + +import { GraphQLSchema } from '../../type/schema'; + +import { TypeInfo } from '../../utilities/TypeInfo'; + +import { + ASTValidationContext, + SDLValidationContext, + ValidationContext, +} from '../ValidationContext'; + +describe('ValidationContext', () => { + it('can be Object.toStringified', () => { + const schema = new GraphQLSchema({}); + const typeInfo = new TypeInfo(schema); + const ast = parse('{ foo }'); + const onError = identityFunc; + + const astContext = new ASTValidationContext(ast, onError); + expect(Object.prototype.toString.call(astContext)).to.equal( + '[object ASTValidationContext]', + ); + + const sdlContext = new SDLValidationContext(ast, schema, onError); + expect(Object.prototype.toString.call(sdlContext)).to.equal( + '[object SDLValidationContext]', + ); + + const context = new ValidationContext(schema, ast, typeInfo, onError); + expect(Object.prototype.toString.call(context)).to.equal( + '[object ValidationContext]', + ); + }); +});