Skip to content

Commit

Permalink
fix(eslint-plugin): [array-type] support readonly operator (#429)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher authored and JamesHenry committed Apr 24, 2019
1 parent 06538e3 commit 8e2d2f5
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
37 changes: 31 additions & 6 deletions packages/eslint-plugin/src/rules/array-type.ts
Expand Up @@ -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;
}
Expand Down Expand Up @@ -153,8 +155,14 @@ export default util.createRule<Options, MessageIds>({
? '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),
Expand All @@ -163,8 +171,20 @@ export default util.createRule<Options, MessageIds>({
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);
Expand All @@ -184,13 +204,18 @@ export default util.createRule<Options, MessageIds>({
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;

Expand All @@ -203,7 +228,7 @@ export default util.createRule<Options, MessageIds>({
type: 'any',
},
fix(fixer) {
return fixer.replaceText(node, 'any[]');
return fixer.replaceText(node, `${readonlyPrefix}any[]`);
},
});
return;
Expand All @@ -229,7 +254,7 @@ export default util.createRule<Options, MessageIds>({
return [
fixer.replaceTextRange(
[node.range[0], type.range[0]],
parens ? '(' : '',
`${readonlyPrefix}${parens ? '(' : ''}`,
),
fixer.replaceTextRange(
[type.range[1], node.range[1]],
Expand Down
12 changes: 6 additions & 6 deletions packages/eslint-plugin/src/rules/unified-signatures.ts
Expand Up @@ -136,7 +136,7 @@ export default util.createRule({
}

function checkOverloads(
signatures: ReadonlyArray<OverloadNode[]>,
signatures: readonly OverloadNode[][],
typeParameters?: TSESTree.TSTypeParameterDeclaration,
): Failure[] {
const result: Failure[] = [];
Expand Down Expand Up @@ -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<TSESTree.Parameter>,
types2: ReadonlyArray<TSESTree.Parameter>,
types1: readonly TSESTree.Parameter[],
types2: readonly TSESTree.Parameter[],
): Unify | undefined {
const index = getIndexOfFirstDifference(
types1,
Expand Down Expand Up @@ -436,8 +436,8 @@ export default util.createRule({

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

/** Calls `action` for every pair of values in `values`. */
function forEachPair<T>(
values: ReadonlyArray<T>,
values: readonly T[],
action: (a: T, b: T) => void,
): void {
for (let i = 0; i < values.length; i++) {
Expand Down
115 changes: 99 additions & 16 deletions packages/eslint-plugin/tests/rules/array-type.test.ts
Expand Up @@ -9,7 +9,7 @@ const ruleTester = new RuleTester({
ruleTester.run('array-type', rule, {
valid: [
{
code: 'let a = []',
code: 'let a: readonly any[] = []',
options: ['array'],
},
{
Expand Down Expand Up @@ -826,31 +826,87 @@ let yyyy: Arr<Array<Array<Arr<string>>>> = [[[["2"]]]];`,
},
],
},

// readonly tests
{
code: 'const x: readonly number[] = [];',
output: 'const x: ReadonlyArray<number> = [];',
options: ['generic'],
errors: [
{
messageId: 'errorStringGeneric',
data: { type: 'number' },
line: 1,
column: 10,
},
],
},
{
code: 'const x: readonly (number | string | boolean)[] = [];',
output: 'const x: ReadonlyArray<number | string | boolean> = [];',
options: ['generic'],
errors: [
{
messageId: 'errorStringGeneric',
data: { type: 'T' },
line: 1,
column: 10,
},
],
},
{
code: 'const x: ReadonlyArray<number> = [];',
output: 'const x: readonly number[] = [];',
options: ['array'],
errors: [
{
messageId: 'errorStringArray',
data: { type: 'number' },
line: 1,
column: 10,
},
],
},
{
code: 'const x: ReadonlyArray<number | string | boolean> = [];',
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(
Expand Down Expand Up @@ -894,5 +950,32 @@ class Foo<T = Bar[][]> extends Bar<T, T[]> implements Baz<T[]> {
`let yy: number[][] = [[4, 5], [6]];`,
`let yy: Array<Array<number>> = [[4, 5], [6]];`,
);

// readonly
testOutput(
'generic',
`let x: readonly number[][]`,
`let x: ReadonlyArray<Array<number>>`,
);
testOutput(
'generic',
`let x: readonly (readonly number[])[]`,
`let x: ReadonlyArray<ReadonlyArray<number>>`,
);
testOutput(
'array',
`let x: ReadonlyArray<Array<number>>`,
`let x: readonly number[][]`,
);
testOutput(
'array',
`let x: ReadonlyArray<ReadonlyArray<number>>`,
`let x: readonly (readonly number[])[]`,
);
testOutput(
'array',
`let x: ReadonlyArray<readonly number[]>`,
`let x: readonly (readonly number[])[]`,
);
});
});
4 changes: 2 additions & 2 deletions packages/eslint-plugin/typings/eslint-utils.d.ts
Expand Up @@ -74,7 +74,7 @@ declare module 'eslint-utils' {
globalScope: Scope.Scope,
options?: {
mode: 'strict' | 'legacy';
globalObjectNames: ReadonlyArray<string>;
globalObjectNames: readonly string[];
},
);

Expand Down Expand Up @@ -103,7 +103,7 @@ declare module 'eslint-utils' {
}
export interface FoundReference<T = any> {
node: TSESTree.Node;
path: ReadonlyArray<string>;
path: readonly string[];
type: ReferenceType;
entry: T;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/node-utils.ts
Expand Up @@ -678,7 +678,7 @@ export function nodeHasTokens(n: ts.Node, ast: ts.SourceFile) {
* @param callback
*/
export function firstDefined<T, U>(
array: ReadonlyArray<T> | undefined,
array: readonly T[] | undefined,
callback: (element: T, index: number) => U | undefined,
): U | undefined {
if (array === undefined) {
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript-estree/src/semantic-errors.ts
Expand Up @@ -52,8 +52,8 @@ export function getFirstSemanticOrSyntacticError(
}

function whitelistSupportedDiagnostics(
diagnostics: ReadonlyArray<ts.DiagnosticWithLocation | ts.Diagnostic>,
): ReadonlyArray<ts.DiagnosticWithLocation | ts.Diagnostic> {
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."
Expand Down
2 changes: 1 addition & 1 deletion packages/typescript-estree/src/ts-estree/ts-estree.ts
Expand Up @@ -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;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/typescript-estree/src/tsconfig-parser.ts
Expand Up @@ -152,9 +152,9 @@ export function calculateProjectParserOptions(
const oldReadDirectory = host.readDirectory;
host.readDirectory = (
path: string,
extensions?: ReadonlyArray<string>,
exclude?: ReadonlyArray<string>,
include?: ReadonlyArray<string>,
extensions?: readonly string[],
exclude?: readonly string[],
include?: readonly string[],
depth?: number,
) =>
oldReadDirectory(
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -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"
Expand Down

0 comments on commit 8e2d2f5

Please sign in to comment.