Skip to content

Commit 8e2d2f5

Browse files
bradzacherJamesHenry
authored andcommitted
fix(eslint-plugin): [array-type] support readonly operator (#429)
1 parent 06538e3 commit 8e2d2f5

File tree

10 files changed

+150
-42
lines changed

10 files changed

+150
-42
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"lerna": "^3.10.5",
7272
"lint-staged": "8.1.0",
7373
"lodash.isplainobject": "4.0.6",
74-
"prettier": "^1.14.3",
74+
"prettier": "^1.17.0",
7575
"rimraf": "^2.6.3",
7676
"ts-jest": "^24.0.0",
7777
"ts-node": "^8.0.1",

packages/eslint-plugin/src/rules/array-type.ts

+31-6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ function typeNeedsParentheses(node: TSESTree.Node): boolean {
6565
case AST_NODE_TYPES.TSTypeOperator:
6666
case AST_NODE_TYPES.TSInferType:
6767
return true;
68+
case AST_NODE_TYPES.Identifier:
69+
return node.name === 'ReadonlyArray';
6870
default:
6971
return false;
7072
}
@@ -153,8 +155,14 @@ export default util.createRule<Options, MessageIds>({
153155
? 'errorStringGeneric'
154156
: 'errorStringGenericSimple';
155157

158+
const isReadonly =
159+
node.parent &&
160+
node.parent.type === AST_NODE_TYPES.TSTypeOperator &&
161+
node.parent.operator === 'readonly';
162+
const typeOpNode = isReadonly ? node.parent! : null;
163+
156164
context.report({
157-
node,
165+
node: isReadonly ? node.parent! : node,
158166
messageId,
159167
data: {
160168
type: getMessageType(node.elementType),
@@ -163,8 +171,20 @@ export default util.createRule<Options, MessageIds>({
163171
const startText = requireWhitespaceBefore(node);
164172
const toFix = [
165173
fixer.replaceTextRange([node.range[1] - 2, node.range[1]], '>'),
166-
fixer.insertTextBefore(node, `${startText ? ' ' : ''}Array<`),
174+
fixer.insertTextBefore(
175+
node,
176+
`${startText ? ' ' : ''}${isReadonly ? 'Readonly' : ''}Array<`,
177+
),
167178
];
179+
if (typeOpNode) {
180+
// remove the readonly operator if it exists
181+
toFix.unshift(
182+
fixer.removeRange([
183+
typeOpNode.range[0],
184+
typeOpNode.range[0] + 'readonly '.length,
185+
]),
186+
);
187+
}
168188

169189
if (node.elementType.type === AST_NODE_TYPES.TSParenthesizedType) {
170190
const first = sourceCode.getFirstToken(node.elementType);
@@ -184,13 +204,18 @@ export default util.createRule<Options, MessageIds>({
184204
TSTypeReference(node: TSESTree.TSTypeReference) {
185205
if (
186206
option === 'generic' ||
187-
node.typeName.type !== AST_NODE_TYPES.Identifier ||
188-
node.typeName.name !== 'Array'
207+
node.typeName.type !== AST_NODE_TYPES.Identifier
189208
) {
190209
return;
191210
}
211+
if (!['Array', 'ReadonlyArray'].includes(node.typeName.name)) {
212+
return;
213+
}
214+
192215
const messageId =
193216
option === 'array' ? 'errorStringArray' : 'errorStringArraySimple';
217+
const isReadonly = node.typeName.name === 'ReadonlyArray';
218+
const readonlyPrefix = isReadonly ? 'readonly ' : '';
194219

195220
const typeParams = node.typeParameters && node.typeParameters.params;
196221

@@ -203,7 +228,7 @@ export default util.createRule<Options, MessageIds>({
203228
type: 'any',
204229
},
205230
fix(fixer) {
206-
return fixer.replaceText(node, 'any[]');
231+
return fixer.replaceText(node, `${readonlyPrefix}any[]`);
207232
},
208233
});
209234
return;
@@ -229,7 +254,7 @@ export default util.createRule<Options, MessageIds>({
229254
return [
230255
fixer.replaceTextRange(
231256
[node.range[0], type.range[0]],
232-
parens ? '(' : '',
257+
`${readonlyPrefix}${parens ? '(' : ''}`,
233258
),
234259
fixer.replaceTextRange(
235260
[type.range[1], node.range[1]],

packages/eslint-plugin/src/rules/unified-signatures.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export default util.createRule({
136136
}
137137

138138
function checkOverloads(
139-
signatures: ReadonlyArray<OverloadNode[]>,
139+
signatures: readonly OverloadNode[][],
140140
typeParameters?: TSESTree.TSTypeParameterDeclaration,
141141
): Failure[] {
142142
const result: Failure[] = [];
@@ -213,8 +213,8 @@ export default util.createRule({
213213

214214
/** Detect `a(x: number, y: number, z: number)` and `a(x: number, y: string, z: number)`. */
215215
function signaturesDifferBySingleParameter(
216-
types1: ReadonlyArray<TSESTree.Parameter>,
217-
types2: ReadonlyArray<TSESTree.Parameter>,
216+
types1: readonly TSESTree.Parameter[],
217+
types2: readonly TSESTree.Parameter[],
218218
): Unify | undefined {
219219
const index = getIndexOfFirstDifference(
220220
types1,
@@ -436,8 +436,8 @@ export default util.createRule({
436436

437437
/* Returns the first index where `a` and `b` differ. */
438438
function getIndexOfFirstDifference<T>(
439-
a: ReadonlyArray<T>,
440-
b: ReadonlyArray<T>,
439+
a: readonly T[],
440+
b: readonly T[],
441441
equal: util.Equal<T>,
442442
): number | undefined {
443443
for (let i = 0; i < a.length && i < b.length; i++) {
@@ -450,7 +450,7 @@ export default util.createRule({
450450

451451
/** Calls `action` for every pair of values in `values`. */
452452
function forEachPair<T>(
453-
values: ReadonlyArray<T>,
453+
values: readonly T[],
454454
action: (a: T, b: T) => void,
455455
): void {
456456
for (let i = 0; i < values.length; i++) {

packages/eslint-plugin/tests/rules/array-type.test.ts

+99-16
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ruleTester = new RuleTester({
99
ruleTester.run('array-type', rule, {
1010
valid: [
1111
{
12-
code: 'let a = []',
12+
code: 'let a: readonly any[] = []',
1313
options: ['array'],
1414
},
1515
{
@@ -826,31 +826,87 @@ let yyyy: Arr<Array<Array<Arr<string>>>> = [[[["2"]]]];`,
826826
},
827827
],
828828
},
829+
830+
// readonly tests
831+
{
832+
code: 'const x: readonly number[] = [];',
833+
output: 'const x: ReadonlyArray<number> = [];',
834+
options: ['generic'],
835+
errors: [
836+
{
837+
messageId: 'errorStringGeneric',
838+
data: { type: 'number' },
839+
line: 1,
840+
column: 10,
841+
},
842+
],
843+
},
844+
{
845+
code: 'const x: readonly (number | string | boolean)[] = [];',
846+
output: 'const x: ReadonlyArray<number | string | boolean> = [];',
847+
options: ['generic'],
848+
errors: [
849+
{
850+
messageId: 'errorStringGeneric',
851+
data: { type: 'T' },
852+
line: 1,
853+
column: 10,
854+
},
855+
],
856+
},
857+
{
858+
code: 'const x: ReadonlyArray<number> = [];',
859+
output: 'const x: readonly number[] = [];',
860+
options: ['array'],
861+
errors: [
862+
{
863+
messageId: 'errorStringArray',
864+
data: { type: 'number' },
865+
line: 1,
866+
column: 10,
867+
},
868+
],
869+
},
870+
{
871+
code: 'const x: ReadonlyArray<number | string | boolean> = [];',
872+
output: 'const x: readonly (number | string | boolean)[] = [];',
873+
options: ['array'],
874+
errors: [
875+
{
876+
messageId: 'errorStringArray',
877+
data: { type: 'T' },
878+
line: 1,
879+
column: 10,
880+
},
881+
],
882+
},
829883
],
830884
});
831885

832886
// eslint rule tester is not working with multi-pass
833887
// https://github.com/eslint/eslint/issues/11187
834888
describe('array-type (nested)', () => {
835-
it('should fix correctly', () => {
889+
describe('should deeply fix correctly', () => {
836890
function testOutput(option: string, code: string, output: string): void {
837-
const linter = new Linter();
891+
it(code, () => {
892+
const linter = new Linter();
838893

839-
linter.defineRule('array-type', Object.assign({}, rule) as any);
840-
const result = linter.verifyAndFix(
841-
code,
842-
{
843-
rules: {
844-
'array-type': [2, option],
894+
linter.defineRule('array-type', Object.assign({}, rule) as any);
895+
const result = linter.verifyAndFix(
896+
code,
897+
{
898+
rules: {
899+
'array-type': [2, option],
900+
},
901+
parser: '@typescript-eslint/parser',
845902
},
846-
parser: '@typescript-eslint/parser',
847-
},
848-
{
849-
fix: true,
850-
},
851-
);
903+
{
904+
fix: true,
905+
},
906+
);
852907

853-
expect(output).toBe(result.output);
908+
expect(result.output).toBe(output);
909+
});
854910
}
855911

856912
testOutput(
@@ -894,5 +950,32 @@ class Foo<T = Bar[][]> extends Bar<T, T[]> implements Baz<T[]> {
894950
`let yy: number[][] = [[4, 5], [6]];`,
895951
`let yy: Array<Array<number>> = [[4, 5], [6]];`,
896952
);
953+
954+
// readonly
955+
testOutput(
956+
'generic',
957+
`let x: readonly number[][]`,
958+
`let x: ReadonlyArray<Array<number>>`,
959+
);
960+
testOutput(
961+
'generic',
962+
`let x: readonly (readonly number[])[]`,
963+
`let x: ReadonlyArray<ReadonlyArray<number>>`,
964+
);
965+
testOutput(
966+
'array',
967+
`let x: ReadonlyArray<Array<number>>`,
968+
`let x: readonly number[][]`,
969+
);
970+
testOutput(
971+
'array',
972+
`let x: ReadonlyArray<ReadonlyArray<number>>`,
973+
`let x: readonly (readonly number[])[]`,
974+
);
975+
testOutput(
976+
'array',
977+
`let x: ReadonlyArray<readonly number[]>`,
978+
`let x: readonly (readonly number[])[]`,
979+
);
897980
});
898981
});

packages/eslint-plugin/typings/eslint-utils.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ declare module 'eslint-utils' {
7474
globalScope: Scope.Scope,
7575
options?: {
7676
mode: 'strict' | 'legacy';
77-
globalObjectNames: ReadonlyArray<string>;
77+
globalObjectNames: readonly string[];
7878
},
7979
);
8080

@@ -103,7 +103,7 @@ declare module 'eslint-utils' {
103103
}
104104
export interface FoundReference<T = any> {
105105
node: TSESTree.Node;
106-
path: ReadonlyArray<string>;
106+
path: readonly string[];
107107
type: ReferenceType;
108108
entry: T;
109109
}

packages/typescript-estree/src/node-utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ export function nodeHasTokens(n: ts.Node, ast: ts.SourceFile) {
678678
* @param callback
679679
*/
680680
export function firstDefined<T, U>(
681-
array: ReadonlyArray<T> | undefined,
681+
array: readonly T[] | undefined,
682682
callback: (element: T, index: number) => U | undefined,
683683
): U | undefined {
684684
if (array === undefined) {

packages/typescript-estree/src/semantic-errors.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export function getFirstSemanticOrSyntacticError(
5252
}
5353

5454
function whitelistSupportedDiagnostics(
55-
diagnostics: ReadonlyArray<ts.DiagnosticWithLocation | ts.Diagnostic>,
56-
): ReadonlyArray<ts.DiagnosticWithLocation | ts.Diagnostic> {
55+
diagnostics: readonly (ts.DiagnosticWithLocation | ts.Diagnostic)[],
56+
): readonly (ts.DiagnosticWithLocation | ts.Diagnostic)[] {
5757
return diagnostics.filter(diagnostic => {
5858
switch (diagnostic.code) {
5959
case 1013: // ts 3.2 "A rest parameter or binding pattern may not have a trailing comma."

packages/typescript-estree/src/ts-estree/ts-estree.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,7 @@ export interface TSTypeLiteral extends BaseNode {
13201320

13211321
export interface TSTypeOperator extends BaseNode {
13221322
type: AST_NODE_TYPES.TSTypeOperator;
1323-
operator: 'keyof' | 'unique';
1323+
operator: 'keyof' | 'unique' | 'readonly';
13241324
typeAnnotation?: TSTypeAnnotation;
13251325
}
13261326

packages/typescript-estree/src/tsconfig-parser.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ export function calculateProjectParserOptions(
152152
const oldReadDirectory = host.readDirectory;
153153
host.readDirectory = (
154154
path: string,
155-
extensions?: ReadonlyArray<string>,
156-
exclude?: ReadonlyArray<string>,
157-
include?: ReadonlyArray<string>,
155+
extensions?: readonly string[],
156+
exclude?: readonly string[],
157+
include?: readonly string[],
158158
depth?: number,
159159
) =>
160160
oldReadDirectory(

yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -5799,10 +5799,10 @@ prelude-ls@~1.1.2:
57995799
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
58005800
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
58015801

5802-
prettier@^1.14.3:
5803-
version "1.16.4"
5804-
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
5805-
integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==
5802+
prettier@^1.17.0:
5803+
version "1.17.0"
5804+
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.0.tgz#53b303676eed22cc14a9f0cec09b477b3026c008"
5805+
integrity sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==
58065806

58075807
pretty-format@^23.6.0:
58085808
version "23.6.0"

0 commit comments

Comments
 (0)