From 8e2d2f587c82ff3f6719a5045b165df9dad2c219 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 24 Apr 2019 07:11:05 -0700 Subject: [PATCH] fix(eslint-plugin): [array-type] support readonly operator (#429) --- package.json | 2 +- .../eslint-plugin/src/rules/array-type.ts | 37 +++++- .../src/rules/unified-signatures.ts | 12 +- .../tests/rules/array-type.test.ts | 115 +++++++++++++++--- .../eslint-plugin/typings/eslint-utils.d.ts | 4 +- packages/typescript-estree/src/node-utils.ts | 2 +- .../typescript-estree/src/semantic-errors.ts | 4 +- .../src/ts-estree/ts-estree.ts | 2 +- .../typescript-estree/src/tsconfig-parser.ts | 6 +- yarn.lock | 8 +- 10 files changed, 150 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index f0f5a45beef..756d368ffaf 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "lerna": "^3.10.5", "lint-staged": "8.1.0", "lodash.isplainobject": "4.0.6", - "prettier": "^1.14.3", + "prettier": "^1.17.0", "rimraf": "^2.6.3", "ts-jest": "^24.0.0", "ts-node": "^8.0.1", diff --git a/packages/eslint-plugin/src/rules/array-type.ts b/packages/eslint-plugin/src/rules/array-type.ts index efc38b082fc..4aef0268fa1 100644 --- a/packages/eslint-plugin/src/rules/array-type.ts +++ b/packages/eslint-plugin/src/rules/array-type.ts @@ -65,6 +65,8 @@ function typeNeedsParentheses(node: TSESTree.Node): boolean { case AST_NODE_TYPES.TSTypeOperator: case AST_NODE_TYPES.TSInferType: return true; + case AST_NODE_TYPES.Identifier: + return node.name === 'ReadonlyArray'; default: return false; } @@ -153,8 +155,14 @@ export default util.createRule({ ? 'errorStringGeneric' : 'errorStringGenericSimple'; + const isReadonly = + node.parent && + node.parent.type === AST_NODE_TYPES.TSTypeOperator && + node.parent.operator === 'readonly'; + const typeOpNode = isReadonly ? node.parent! : null; + context.report({ - node, + node: isReadonly ? node.parent! : node, messageId, data: { type: getMessageType(node.elementType), @@ -163,8 +171,20 @@ export default util.createRule({ const startText = requireWhitespaceBefore(node); const toFix = [ fixer.replaceTextRange([node.range[1] - 2, node.range[1]], '>'), - fixer.insertTextBefore(node, `${startText ? ' ' : ''}Array<`), + fixer.insertTextBefore( + node, + `${startText ? ' ' : ''}${isReadonly ? 'Readonly' : ''}Array<`, + ), ]; + if (typeOpNode) { + // remove the readonly operator if it exists + toFix.unshift( + fixer.removeRange([ + typeOpNode.range[0], + typeOpNode.range[0] + 'readonly '.length, + ]), + ); + } if (node.elementType.type === AST_NODE_TYPES.TSParenthesizedType) { const first = sourceCode.getFirstToken(node.elementType); @@ -184,13 +204,18 @@ export default util.createRule({ TSTypeReference(node: TSESTree.TSTypeReference) { if ( option === 'generic' || - node.typeName.type !== AST_NODE_TYPES.Identifier || - node.typeName.name !== 'Array' + node.typeName.type !== AST_NODE_TYPES.Identifier ) { return; } + if (!['Array', 'ReadonlyArray'].includes(node.typeName.name)) { + return; + } + const messageId = option === 'array' ? 'errorStringArray' : 'errorStringArraySimple'; + const isReadonly = node.typeName.name === 'ReadonlyArray'; + const readonlyPrefix = isReadonly ? 'readonly ' : ''; const typeParams = node.typeParameters && node.typeParameters.params; @@ -203,7 +228,7 @@ export default util.createRule({ type: 'any', }, fix(fixer) { - return fixer.replaceText(node, 'any[]'); + return fixer.replaceText(node, `${readonlyPrefix}any[]`); }, }); return; @@ -229,7 +254,7 @@ export default util.createRule({ return [ fixer.replaceTextRange( [node.range[0], type.range[0]], - parens ? '(' : '', + `${readonlyPrefix}${parens ? '(' : ''}`, ), fixer.replaceTextRange( [type.range[1], node.range[1]], diff --git a/packages/eslint-plugin/src/rules/unified-signatures.ts b/packages/eslint-plugin/src/rules/unified-signatures.ts index 1b54bd4217f..c1c1ad470da 100644 --- a/packages/eslint-plugin/src/rules/unified-signatures.ts +++ b/packages/eslint-plugin/src/rules/unified-signatures.ts @@ -136,7 +136,7 @@ export default util.createRule({ } function checkOverloads( - signatures: ReadonlyArray, + signatures: readonly OverloadNode[][], typeParameters?: TSESTree.TSTypeParameterDeclaration, ): Failure[] { const result: Failure[] = []; @@ -213,8 +213,8 @@ export default util.createRule({ /** Detect `a(x: number, y: number, z: number)` and `a(x: number, y: string, z: number)`. */ function signaturesDifferBySingleParameter( - types1: ReadonlyArray, - types2: ReadonlyArray, + types1: readonly TSESTree.Parameter[], + types2: readonly TSESTree.Parameter[], ): Unify | undefined { const index = getIndexOfFirstDifference( types1, @@ -436,8 +436,8 @@ export default util.createRule({ /* Returns the first index where `a` and `b` differ. */ function getIndexOfFirstDifference( - a: ReadonlyArray, - b: ReadonlyArray, + a: readonly T[], + b: readonly T[], equal: util.Equal, ): number | undefined { for (let i = 0; i < a.length && i < b.length; i++) { @@ -450,7 +450,7 @@ export default util.createRule({ /** Calls `action` for every pair of values in `values`. */ function forEachPair( - values: ReadonlyArray, + values: readonly T[], action: (a: T, b: T) => void, ): void { for (let i = 0; i < values.length; i++) { diff --git a/packages/eslint-plugin/tests/rules/array-type.test.ts b/packages/eslint-plugin/tests/rules/array-type.test.ts index 1178f4c4f9f..31d0b018d32 100644 --- a/packages/eslint-plugin/tests/rules/array-type.test.ts +++ b/packages/eslint-plugin/tests/rules/array-type.test.ts @@ -9,7 +9,7 @@ const ruleTester = new RuleTester({ ruleTester.run('array-type', rule, { valid: [ { - code: 'let a = []', + code: 'let a: readonly any[] = []', options: ['array'], }, { @@ -826,31 +826,87 @@ let yyyy: Arr>>> = [[[["2"]]]];`, }, ], }, + + // readonly tests + { + code: 'const x: readonly number[] = [];', + output: 'const x: ReadonlyArray = [];', + options: ['generic'], + errors: [ + { + messageId: 'errorStringGeneric', + data: { type: 'number' }, + line: 1, + column: 10, + }, + ], + }, + { + code: 'const x: readonly (number | string | boolean)[] = [];', + output: 'const x: ReadonlyArray = [];', + options: ['generic'], + errors: [ + { + messageId: 'errorStringGeneric', + data: { type: 'T' }, + line: 1, + column: 10, + }, + ], + }, + { + code: 'const x: ReadonlyArray = [];', + output: 'const x: readonly number[] = [];', + options: ['array'], + errors: [ + { + messageId: 'errorStringArray', + data: { type: 'number' }, + line: 1, + column: 10, + }, + ], + }, + { + code: 'const x: ReadonlyArray = [];', + output: 'const x: readonly (number | string | boolean)[] = [];', + options: ['array'], + errors: [ + { + messageId: 'errorStringArray', + data: { type: 'T' }, + line: 1, + column: 10, + }, + ], + }, ], }); // eslint rule tester is not working with multi-pass // https://github.com/eslint/eslint/issues/11187 describe('array-type (nested)', () => { - it('should fix correctly', () => { + describe('should deeply fix correctly', () => { function testOutput(option: string, code: string, output: string): void { - const linter = new Linter(); + it(code, () => { + const linter = new Linter(); - linter.defineRule('array-type', Object.assign({}, rule) as any); - const result = linter.verifyAndFix( - code, - { - rules: { - 'array-type': [2, option], + linter.defineRule('array-type', Object.assign({}, rule) as any); + const result = linter.verifyAndFix( + code, + { + rules: { + 'array-type': [2, option], + }, + parser: '@typescript-eslint/parser', }, - parser: '@typescript-eslint/parser', - }, - { - fix: true, - }, - ); + { + fix: true, + }, + ); - expect(output).toBe(result.output); + expect(result.output).toBe(output); + }); } testOutput( @@ -894,5 +950,32 @@ class Foo extends Bar implements Baz { `let yy: number[][] = [[4, 5], [6]];`, `let yy: Array> = [[4, 5], [6]];`, ); + + // readonly + testOutput( + 'generic', + `let x: readonly number[][]`, + `let x: ReadonlyArray>`, + ); + testOutput( + 'generic', + `let x: readonly (readonly number[])[]`, + `let x: ReadonlyArray>`, + ); + testOutput( + 'array', + `let x: ReadonlyArray>`, + `let x: readonly number[][]`, + ); + testOutput( + 'array', + `let x: ReadonlyArray>`, + `let x: readonly (readonly number[])[]`, + ); + testOutput( + 'array', + `let x: ReadonlyArray`, + `let x: readonly (readonly number[])[]`, + ); }); }); diff --git a/packages/eslint-plugin/typings/eslint-utils.d.ts b/packages/eslint-plugin/typings/eslint-utils.d.ts index d926229b00e..d93c90532da 100644 --- a/packages/eslint-plugin/typings/eslint-utils.d.ts +++ b/packages/eslint-plugin/typings/eslint-utils.d.ts @@ -74,7 +74,7 @@ declare module 'eslint-utils' { globalScope: Scope.Scope, options?: { mode: 'strict' | 'legacy'; - globalObjectNames: ReadonlyArray; + globalObjectNames: readonly string[]; }, ); @@ -103,7 +103,7 @@ declare module 'eslint-utils' { } export interface FoundReference { node: TSESTree.Node; - path: ReadonlyArray; + path: readonly string[]; type: ReferenceType; entry: T; } diff --git a/packages/typescript-estree/src/node-utils.ts b/packages/typescript-estree/src/node-utils.ts index b476413731b..80793df4e2d 100644 --- a/packages/typescript-estree/src/node-utils.ts +++ b/packages/typescript-estree/src/node-utils.ts @@ -678,7 +678,7 @@ export function nodeHasTokens(n: ts.Node, ast: ts.SourceFile) { * @param callback */ export function firstDefined( - array: ReadonlyArray | undefined, + array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined, ): U | undefined { if (array === undefined) { diff --git a/packages/typescript-estree/src/semantic-errors.ts b/packages/typescript-estree/src/semantic-errors.ts index 4486532c09d..14568aba123 100644 --- a/packages/typescript-estree/src/semantic-errors.ts +++ b/packages/typescript-estree/src/semantic-errors.ts @@ -52,8 +52,8 @@ export function getFirstSemanticOrSyntacticError( } function whitelistSupportedDiagnostics( - diagnostics: ReadonlyArray, -): ReadonlyArray { + diagnostics: readonly (ts.DiagnosticWithLocation | ts.Diagnostic)[], +): readonly (ts.DiagnosticWithLocation | ts.Diagnostic)[] { return diagnostics.filter(diagnostic => { switch (diagnostic.code) { case 1013: // ts 3.2 "A rest parameter or binding pattern may not have a trailing comma." diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 52f1214f1d3..9de0fc257d1 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -1320,7 +1320,7 @@ export interface TSTypeLiteral extends BaseNode { export interface TSTypeOperator extends BaseNode { type: AST_NODE_TYPES.TSTypeOperator; - operator: 'keyof' | 'unique'; + operator: 'keyof' | 'unique' | 'readonly'; typeAnnotation?: TSTypeAnnotation; } diff --git a/packages/typescript-estree/src/tsconfig-parser.ts b/packages/typescript-estree/src/tsconfig-parser.ts index d953b39d8ea..6ab8047b0be 100644 --- a/packages/typescript-estree/src/tsconfig-parser.ts +++ b/packages/typescript-estree/src/tsconfig-parser.ts @@ -152,9 +152,9 @@ export function calculateProjectParserOptions( const oldReadDirectory = host.readDirectory; host.readDirectory = ( path: string, - extensions?: ReadonlyArray, - exclude?: ReadonlyArray, - include?: ReadonlyArray, + extensions?: readonly string[], + exclude?: readonly string[], + include?: readonly string[], depth?: number, ) => oldReadDirectory( diff --git a/yarn.lock b/yarn.lock index d26da3f9306..47d1c5bf7d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5799,10 +5799,10 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@^1.14.3: - version "1.16.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" - integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== +prettier@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.0.tgz#53b303676eed22cc14a9f0cec09b477b3026c008" + integrity sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw== pretty-format@^23.6.0: version "23.6.0"