From f33d436ae17338d40ba6866639678f4692cc6aa5 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 19 Jul 2019 11:38:39 +1200 Subject: [PATCH 1/5] feat(prefer-readonly-types): Merge rules readonly-array and readonly-keyword --- README.md | 13 +- docs/rules/prefer-readonly-types.md | 173 +++ docs/rules/readonly-array.md | 69 - src/rules/index.ts | 13 +- ...only-array.ts => prefer-readonly-types.ts} | 57 +- src/rules/readonly-keyword.ts | 103 -- tests/rules/prefer-readonly-types.test.ts | 1109 +++++++++++++++++ tests/rules/readonly-array.test.ts | 659 ---------- tests/rules/readonly-keyword.test.ts | 480 ------- 9 files changed, 1342 insertions(+), 1334 deletions(-) create mode 100644 docs/rules/prefer-readonly-types.md delete mode 100644 docs/rules/readonly-array.md rename src/rules/{readonly-array.ts => prefer-readonly-types.ts} (78%) delete mode 100644 src/rules/readonly-keyword.ts create mode 100644 tests/rules/prefer-readonly-types.test.ts delete mode 100644 tests/rules/readonly-array.test.ts delete mode 100644 tests/rules/readonly-keyword.test.ts diff --git a/README.md b/README.md index 9179a279e..2e9860aaf 100644 --- a/README.md +++ b/README.md @@ -99,13 +99,12 @@ In addition to immutable rules this project also contains a few rules for enforc ### Immutability rules -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ------------------------------------------------------------ | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: | -| [`readonly-keyword`](./docs/rules/readonly-keyword.md) | Enforce readonly modifiers are used where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`readonly-array`](./docs/rules/readonly-array.md) | Enforce readonly array over mutable arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | -| [`no-method-signature`](./docs/rules/no-method-signature.md) | Enforce property signatures with readonly modifiers over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------------- | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: | +| [`prefer-readonly-types`](./docs/rules/prefer-readonly-types.md) | Use readonly types and readonly modifiers where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | +| [`no-method-signature`](./docs/rules/no-method-signature.md) | Enforce property signatures with readonly modifiers over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### Functional style rules diff --git a/docs/rules/prefer-readonly-types.md b/docs/rules/prefer-readonly-types.md new file mode 100644 index 000000000..752648641 --- /dev/null +++ b/docs/rules/prefer-readonly-types.md @@ -0,0 +1,173 @@ +# Prefer readonly types over mutable types (prefer-readonly-types) + +This rule enforces use of the readonly modifier and readonly types. + +## Rule Details + +This rule enforces use of `readonly T[]` (`ReadonlyArray`) over `T[]` (`Array`). + +The readonly modifier must appear on property signatures in interfaces, property declarations in classes, and index signatures. + +### Benefits of using the `readonly` modifier + +You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`. + +```typescript +interface Point { + x: number; + y: number; +} +const point: Point = { x: 23, y: 44 }; +point.x = 99; // This is legal +``` + +This is why the `readonly` modifier exists. It prevents you from assigning a value to the result of a member expression. + +```typescript +interface Point { + readonly x: number; + readonly y: number; +} +const point: Point = { x: 23, y: 44 }; +point.x = 99; // <- No object mutation allowed. +``` + +This is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However the `readonly` modifier has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the ES2016 object spread [syntax](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#object-spread-and-rest) that was added in typescript 2.1: + +```typescript +interface Point { + readonly x: number; + readonly y: number; +} +const point: Point = { x: 23, y: 44 }; +const transformedPoint = { ...point, x: 99 }; +``` + +Note that you can also use object spread when destructuring to [delete keys](http://stackoverflow.com/questions/35342355/remove-data-from-nested-objects-without-mutating/35676025#35676025) in an object: + +```typescript +let { [action.id]: deletedItem, ...rest } = state; +``` + +The `readonly` modifier also works on indexers: + +```typescript +const foo: { readonly [key: string]: number } = { a: 1, b: 2 }; +foo["a"] = 3; // Error: Index signature only permits reading +``` + +### Benefits of using `readonly T[]` + +Even if an array is declared with `const` it is still possible to mutate the contents of the array. + +```typescript +interface Point { + readonly x: number; + readonly y: number; +} +const points: Array = [{ x: 23, y: 44 }]; +points.push({ x: 1, y: 2 }); // This is legal +``` + +Using the `ReadonlyArray` type or `readonly T[]` will stop this mutation: + +```typescript +interface Point { + readonly x: number; + readonly y: number; +} + +const points: ReadonlyArray = [{ x: 23, y: 44 }]; +// const points: readonly Point[] = [{ x: 23, y: 44 }]; // This is the alternative syntax for the line above + +points.push({ x: 1, y: 2 }); // Unresolved method push() +``` + +## Options + +The rule accepts an options object with the following properties: + +```typescript +type Options = { + readonly checkImplicit: boolean + readonly ignoreClass?: boolean; + readonly ignoreInterface?: boolean; + readonly ignoreLocal?: boolean; + readonly ignorePattern?: string | Array; + readonly ignoreReturnType?: boolean; +}; + +const defaults = { + checkImplicit: false, + ignoreClass: false, + ignoreInterface: false, + ignoreLocal: false, + ignoreReturnType: false +}; +``` + +### `checkImplicit` + +By default, this function only checks explicit types. Enabling this option will make the rule also check implicit types. + +Note: Checking implicit types is more expensive (slow). + +### `ignoreReturnType` + +Doesn't check the return type of functions. + +### `ignoreClass` + +A boolean to specify if checking for `readonly` should apply to classes. `false` by default. + +Examples of **incorrect** code for the `{ "ignoreClass": false }` option: + +```ts +/*eslint ts-immutable/readonly: ["error", { "ignoreClass": false }]*/ + +class { + myprop: string; +} +``` + +Examples of **correct** code for the `{ "ignoreClass": true }` option: + +```ts +/*eslint ts-immutable/readonly: ["error", { "ignoreClass": true }]*/ + +class { + myprop: string; +} +``` + +### `ignoreInterface` + +A boolean to specify if checking for `readonly` should apply to interfaces. `false` by default. + +Examples of **incorrect** code for the `{ "ignoreInterface": false }` option: + +```ts +/*eslint ts-immutable/readonly: ["error", { "ignoreInterface": false }]*/ + +interface { + myprop: string; +} +``` + +Examples of **correct** code for the `{ "ignoreInterface": true }` option: + +```ts +/*eslint ts-immutable/readonly: ["error", { "ignoreInterface": true }]*/ + +interface { + myprop: string; +} +``` + +### `ignoreLocal` + +See the [ignoreLocal](./options/ignore-local.md) docs. + +### `ignorePattern` + +See the [ignorePattern](./options/ignore-pattern.md) docs. diff --git a/docs/rules/readonly-array.md b/docs/rules/readonly-array.md deleted file mode 100644 index 0f238497c..000000000 --- a/docs/rules/readonly-array.md +++ /dev/null @@ -1,69 +0,0 @@ -# Prefer readonly array over mutable arrays (readonly-array) - -This rule enforces use of `ReadonlyArray` or `readonly T[]` instead of `Array` or `T[]`. - -## Rule Details - -Below is some information about the `ReadonlyArray` type and the benefits of using it: - -Even if an array is declared with `const` it is still possible to mutate the contents of the array. - -```typescript -interface Point { - readonly x: number; - readonly y: number; -} -const points: Array = [{ x: 23, y: 44 }]; -points.push({ x: 1, y: 2 }); // This is legal -``` - -Using the `ReadonlyArray` type or `readonly T[]` will stop this mutation: - -```typescript -interface Point { - readonly x: number; - readonly y: number; -} - -const points: ReadonlyArray = [{ x: 23, y: 44 }]; -// const points: readonly Point[] = [{ x: 23, y: 44 }]; // This is the alternative syntax for the line above - -points.push({ x: 1, y: 2 }); // Unresolved method push() -``` - -## Options - -The rule accepts an options object with the following properties: - -```typescript -type Options = { - readonly ignoreReturnType?: boolean; - readonly ignoreLocal?: boolean; - readonly ignorePattern?: string | Array; - readonly checkImplicit: boolean -}; - -const defaults = { - ignoreReturnType: false, - ignoreLocal: false, - checkImplicit: false -}; -``` - -### `checkImplicit` - -By default, this function only checks explicit types. Enabling this option will make the rule also check implicit types. - -Note: Checking implicit types is more expensive (slow). - -### `ignoreReturnType` - -Doesn't check the return type of functions. - -### `ignoreLocal` - -See the [ignoreLocal](./options/ignore-local.md) docs. - -### `ignorePattern` - -See the [ignorePattern](./options/ignore-pattern.md) docs. diff --git a/src/rules/index.ts b/src/rules/index.ts index bbcec770d..5f5a645d5 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -37,13 +37,9 @@ import { name as noThisRuleName, rule as noThisRule } from "./no-this"; import { name as noThrowRuleName, rule as noThrowRule } from "./no-throw"; import { name as noTryRuleName, rule as noTryRule } from "./no-try"; import { - name as readonlyArrayRuleName, - rule as readonlyArrayRule -} from "./readonly-array"; -import { - name as readonlyKeywordRuleName, - rule as readonlyKeywordRule -} from "./readonly-keyword"; + name as preferReadonlyTypesRuleName, + rule as preferReadonlyTypesRule +} from "./prefer-readonly-types"; /** * All of the custom rules. @@ -63,6 +59,5 @@ export const rules = { [noThisRuleName]: noThisRule, [noThrowRuleName]: noThrowRule, [noTryRuleName]: noTryRule, - [readonlyArrayRuleName]: readonlyArrayRule, - [readonlyKeywordRuleName]: readonlyKeywordRule + [preferReadonlyTypesRuleName]: preferReadonlyTypesRule }; diff --git a/src/rules/readonly-array.ts b/src/rules/prefer-readonly-types.ts similarity index 78% rename from src/rules/readonly-array.ts rename to src/rules/prefer-readonly-types.ts index b2b59fab7..fbf2a22ec 100644 --- a/src/rules/readonly-array.ts +++ b/src/rules/prefer-readonly-types.ts @@ -21,15 +21,19 @@ import { isFunctionLike, isIdentifier, isTSArrayType, + isTSIndexSignature, + isTSParameterProperty, isTSTypeOperator } from "../util/typeguard"; // The name of this rule. -export const name = "readonly-array" as const; +export const name = "prefer-readonly-types" as const; // The options this rule can take. type Options = ignore.IgnoreLocalOption & ignore.IgnorePatternOption & + ignore.IgnoreClassOption & + ignore.IgnoreInterfaceOption & ignore.IgnoreReturnTypeOption & { readonly checkImplicit: boolean; }; @@ -39,6 +43,8 @@ const schema: JSONSchema4 = [ deepMerge([ ignore.ignoreLocalOptionSchema, ignore.ignorePatternOptionSchema, + ignore.ignoreClassOptionSchema, + ignore.ignoreInterfaceOptionSchema, ignore.ignoreReturnTypeOptionSchema, { type: "object", @@ -54,15 +60,18 @@ const schema: JSONSchema4 = [ // The default options for the rule. const defaultOptions: Options = { + checkImplicit: false, + ignoreClass: false, + ignoreInterface: false, ignoreLocal: false, - ignoreReturnType: false, - checkImplicit: false + ignoreReturnType: false }; // The possible error messages. const errorMessages = { - generic: "Only readonly arrays allowed.", - implicit: "Implicitly a mutable array. Only readonly arrays allowed." + array: "Only readonly arrays allowed.", + implicit: "Implicitly a mutable array. Only readonly arrays allowed.", + property: "A readonly modifier is required." } as const; // The meta data for this rule. @@ -96,7 +105,7 @@ function checkArrayOrTupleType( ? [ { node, - messageId: "generic", + messageId: "array", fix: node.parent && isTSArrayType(node.parent) ? fixer => [ @@ -127,7 +136,7 @@ function checkTypeReference( ? [ { node, - messageId: "generic", + messageId: "array", fix: fixer => fixer.insertTextBefore(node, "Readonly") } ] @@ -135,6 +144,35 @@ function checkTypeReference( }; } +/** + * Check if the given property/signature node violates this rule. + */ +function checkProperty( + node: + | TSESTree.ClassProperty + | TSESTree.TSIndexSignature + | TSESTree.TSParameterProperty + | TSESTree.TSPropertySignature, + context: RuleContext +): RuleResult { + return { + context, + descriptors: node.readonly + ? [] + : [ + { + node, + messageId: "property", + fix: isTSIndexSignature(node) + ? fixer => fixer.insertTextBefore(node, "readonly ") + : isTSParameterProperty(node) + ? fixer => fixer.insertTextBefore(node.parameter, "readonly ") + : fixer => fixer.insertTextBefore(node.key, "readonly ") + } + ] + }; +} + /** * Check if the given TypeReference violates this rule. */ @@ -203,12 +241,17 @@ export const rule = createRule( options ); const _checkTypeReference = checkNode(checkTypeReference, context, options); + const _checkProperty = checkNode(checkProperty, context, options); const _checkImplicitType = checkNode(checkImplicitType, context, options); return { TSArrayType: _checkArrayOrTupleType, TSTupleType: _checkArrayOrTupleType, TSTypeReference: _checkTypeReference, + ClassProperty: _checkProperty, + TSIndexSignature: _checkProperty, + TSParameterProperty: _checkProperty, + TSPropertySignature: _checkProperty, ...(options.checkImplicit ? { VariableDeclaration: _checkImplicitType, diff --git a/src/rules/readonly-keyword.ts b/src/rules/readonly-keyword.ts deleted file mode 100644 index a3bbd9ddc..000000000 --- a/src/rules/readonly-keyword.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { TSESTree } from "@typescript-eslint/typescript-estree"; -import { all as deepMerge } from "deepmerge"; -import { JSONSchema4 } from "json-schema"; - -import * as ignore from "../common/ignore-options"; -import { - checkNode, - createRule, - RuleContext, - RuleMetaData, - RuleResult -} from "../util/rule"; -import { isTSIndexSignature, isTSParameterProperty } from "../util/typeguard"; - -// The name of this rule. -export const name = "readonly-keyword" as const; - -// The options this rule can take. -type Options = ignore.IgnoreLocalOption & - ignore.IgnorePatternOption & - ignore.IgnoreClassOption & - ignore.IgnoreInterfaceOption; - -// The schema for the rule options. -const schema: JSONSchema4 = [ - deepMerge([ - ignore.ignoreLocalOptionSchema, - ignore.ignorePatternOptionSchema, - ignore.ignoreClassOptionSchema, - ignore.ignoreInterfaceOptionSchema - ]) -]; - -// The default options for the rule. -const defaultOptions: Options = { - ignoreClass: false, - ignoreInterface: false, - ignoreLocal: false -}; - -// The possible error messages. -const errorMessages = { - generic: "A readonly modifier is required." -} as const; - -// The meta data for this rule. -const meta: RuleMetaData = { - type: "suggestion", - docs: { - description: "Enforce readonly modifiers are used where possible.", - category: "Best Practices", - recommended: "error" - }, - messages: errorMessages, - fixable: "code", - schema -}; - -/** - * Check if the given node violates this rule. - */ -function check( - node: - | TSESTree.TSPropertySignature - | TSESTree.TSIndexSignature - | TSESTree.ClassProperty - | TSESTree.TSParameterProperty, - context: RuleContext -): RuleResult { - return { - context, - descriptors: node.readonly - ? [] - : [ - { - node, - messageId: "generic", - fix: isTSIndexSignature(node) - ? fixer => fixer.insertTextBefore(node, "readonly ") - : isTSParameterProperty(node) - ? fixer => fixer.insertTextBefore(node.parameter, "readonly ") - : fixer => fixer.insertTextBefore(node.key, "readonly ") - } - ] - }; -} - -// Create the rule. -export const rule = createRule( - name, - meta, - defaultOptions, - (context, options) => { - const _checkNode = checkNode(check, context, options); - - return { - ClassProperty: _checkNode, - TSIndexSignature: _checkNode, - TSPropertySignature: _checkNode, - TSParameterProperty: _checkNode - }; - } -); diff --git a/tests/rules/prefer-readonly-types.test.ts b/tests/rules/prefer-readonly-types.test.ts new file mode 100644 index 000000000..de165eea6 --- /dev/null +++ b/tests/rules/prefer-readonly-types.test.ts @@ -0,0 +1,1109 @@ +/** + * @fileoverview Tests for prefer-readonly-types + */ + +import dedent from "dedent"; +import { RuleTester } from "eslint"; + +import { name, rule } from "../../src/rules/prefer-readonly-types"; + +import { typescript } from "../configs"; +import { + InvalidTestCase, + processInvalidTestCase, + processValidTestCase, + ValidTestCase +} from "../util"; + +// Valid test cases. +const valid: ReadonlyArray = [ + // Should not fail on explicit ReadonlyArray parameter. + { + code: dedent` + function foo(...numbers: ReadonlyArray) { + }`, + optionsSet: [[]] + }, + { + code: dedent` + function foo(...numbers: readonly number[]) { + }`, + optionsSet: [[]] + }, + // Should not fail on explicit ReadonlyArray return type. + { + code: dedent` + function foo(): ReadonlyArray { + return [1, 2, 3]; + }`, + optionsSet: [[]] + }, + { + code: dedent` + const foo = (): ReadonlyArray => { + return [1, 2, 3]; + }`, + optionsSet: [[]] + }, + // ReadonlyArray Tuple. + { + code: dedent` + function foo(tuple: readonly [number, string, readonly [number, string]]) { + }`, + optionsSet: [[]] + }, + // Should not fail on ReadonlyArray type alias. + { + code: `type Foo = ReadonlyArray;`, + optionsSet: [[]] + }, + // Should not fail on ReadonlyArray type alias in local type. + { + code: dedent` + function foo() { + type Foo = ReadonlyArray; + }`, + optionsSet: [[]] + }, + // Should not fail on ReadonlyArray in variable declaration. + { + code: `const foo: ReadonlyArray = [];`, + optionsSet: [[]] + }, + // Ignore return type. + { + code: dedent` + function foo(...numbers: ReadonlyArray): Array {} + function bar(...numbers: readonly number[]): number[] {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type. + { + code: dedent` + const foo = function(...numbers: ReadonlyArray): Array {} + const bar = function(...numbers: readonly number[]): number[] {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type. + { + code: dedent` + const foo = (...numbers: ReadonlyArray): Array => {} + const bar = (...numbers: readonly number[]): number[] => {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type. + { + code: dedent` + class Foo { + foo(...numbers: ReadonlyArray): Array { + } + } + class Bar { + foo(...numbers: readonly number[]): number[] { + } + }`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with Type Arguments. + { + code: dedent` + function foo(...numbers: ReadonlyArray): Promise> {} + function foo(...numbers: ReadonlyArray): Promise {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with deep Type Arguments. + { + code: dedent` + type Foo = { readonly x: T; }; + function foo(...numbers: ReadonlyArray): Promise>> {} + function foo(...numbers: ReadonlyArray): Promise> {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with Type Arguments in a tuple. + { + code: dedent` + function foo(...numbers: ReadonlyArray): readonly [number, Array, number] {} + function foo(...numbers: ReadonlyArray): readonly [number, number[], number] {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with Type Arguments Union. + { + code: dedent` + function foo(...numbers: ReadonlyArray): { readonly a: Array } | { readonly b: string[] } {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with Type Arguments Intersection. + { + code: dedent` + function foo(...numbers: ReadonlyArray): { readonly a: Array } & { readonly b: string[] } {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Ignore return type with Type Arguments Conditional. + { + code: dedent` + function foo(x: T): T extends Array ? string : number[] {}`, + optionsSet: [[{ ignoreReturnType: true }]] + }, + // Should not fail on implicit ReadonlyArray type in variable declaration. + { + code: dedent` + const foo = [1, 2, 3] as const`, + optionsSet: [[{ checkImplicit: true }]] + }, + // Should not fail on implicit Array. + { + code: dedent` + const foo = [1, 2, 3] + function bar(param = [1, 2, 3]) {}`, + optionsSet: [[]] + }, + // Interface with readonly modifiers should not produce failures. + { + code: dedent` + interface Foo { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string }, + readonly [key: string]: string, + }`, + optionsSet: [[]] + }, + // PropertySignature and IndexSignature members without readonly modifier + // should produce failures. Also verify that nested members are checked. + { + code: dedent` + interface Foo { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string }, + readonly [key: string]: string, + readonly e: { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string }, + readonly [key: string]: string, + } + }`, + optionsSet: [[]] + }, + // Class with parameter properties. + { + code: dedent` + class Klass { + constructor ( + nonParameterProp: string, + readonly readonlyProp: string, + public readonly publicReadonlyProp: string, + protected readonly protectedReadonlyProp: string, + private readonly privateReadonlyProp: string, + ) { } + }`, + optionsSet: [[]] + }, + // CallSignature and MethodSignature cannot have readonly modifiers and should + // not produce failures. + { + code: dedent` + interface Foo { + (): void + foo(): void + }`, + optionsSet: [[]] + }, + // The literal with indexer with readonly modifier should not produce failures. + { + code: `let foo: { readonly [key: string]: number };`, + optionsSet: [[]] + }, + // Type literal in array template parameter with readonly should not produce failures. + { + code: `type foo = ReadonlyArray<{ readonly type: string, readonly code: string }>;`, + optionsSet: [[]] + }, + // Type literal with readonly on members should not produce failures. + { + code: dedent` + let foo: { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string } + readonly [key: string]: string + };`, + optionsSet: [[]] + }, + // Ignore Classes. + { + code: dedent` + class Klass { + foo: number; + private bar: number; + static baz: number; + private static qux: number; + }`, + optionsSet: [[{ ignoreClass: true }]] + }, + // Ignore Interfaces. + { + code: dedent` + interface Foo { + foo: number, + bar: ReadonlyArray, + baz: () => string, + qux: { [key: string]: string } + }`, + optionsSet: [[{ ignoreInterface: true }]] + }, + // Ignore Local. + { + code: dedent` + function foo() { + let foo: { + a: number, + b: ReadonlyArray, + c: () => string, + d: { [key: string]: string }, + [key: string]: string, + readonly d: { + a: number, + b: ReadonlyArray, + c: () => string, + d: { [key: string]: string }, + [key: string]: string, + } + } + };`, + optionsSet: [[{ ignoreLocal: true }]] + }, + // Ignore Prefix. + { + code: dedent` + let foo: { + mutableA: number, + mutableB: ReadonlyArray, + mutableC: () => string, + mutableD: { readonly [key: string]: string }, + mutableE: { + mutableA: number, + mutableB: ReadonlyArray, + mutableC: () => string, + mutableD: { readonly [key: string]: string }, + } + };`, + optionsSet: [[{ ignorePattern: "^mutable" }]] + }, + // Ignore Suffix. + { + code: dedent` + let foo: { + aMutable: number, + bMutable: ReadonlyArray, + cMutable: () => string, + dMutable: { readonly [key: string]: string }, + eMutable: { + aMutable: number, + bMutable: ReadonlyArray, + cMutable: () => string, + dMutable: { readonly [key: string]: string }, + } + };`, + optionsSet: [[{ ignorePattern: "Mutable$" }]] + } +]; + +// Invalid test cases. +const invalid: ReadonlyArray = [ + { + code: dedent` + function foo(...numbers: number[]) { + }`, + optionsSet: [[]], + output: dedent` + function foo(...numbers: readonly number[]) { + }`, + errors: [ + { + messageId: "array", + type: "TSArrayType", + line: 1, + column: 26 + } + ] + }, + { + code: dedent` + function foo(...numbers: Array) { + }`, + optionsSet: [[]], + output: dedent` + function foo(...numbers: ReadonlyArray) { + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 26 + } + ] + }, + // Should fail on Array type in interface. + { + code: dedent` + interface Foo { + readonly bar: Array + }`, + optionsSet: [[]], + output: dedent` + interface Foo { + readonly bar: ReadonlyArray + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 2, + column: 17 + } + ] + }, + // Should fail on Array type in index interface. + { + code: dedent` + interface Foo { + readonly [key: string]: { + readonly groups: Array + } + }`, + optionsSet: [[]], + output: dedent` + interface Foo { + readonly [key: string]: { + readonly groups: ReadonlyArray + } + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 22 + } + ] + }, + // Should fail on Array type as function return type and in local interface. + { + code: dedent` + function foo(): Array { + interface Foo { + readonly bar: Array + } + }`, + optionsSet: [[]], + output: dedent` + function foo(): ReadonlyArray { + interface Foo { + readonly bar: ReadonlyArray + } + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 17 + }, + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 19 + } + ] + }, + // Should fail on Array type as function return type and in local interface. + { + code: dedent` + const foo = (): Array => { + interface Foo { + readonly bar: Array + } + }`, + optionsSet: [[]], + output: dedent` + const foo = (): ReadonlyArray => { + interface Foo { + readonly bar: ReadonlyArray + } + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 17 + }, + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 19 + } + ] + }, + // Should fail on shorthand syntax Array type as return type. + { + code: dedent` + function foo(): number[] { + }`, + optionsSet: [[]], + output: dedent` + function foo(): readonly number[] { + }`, + errors: [ + { + messageId: "array", + type: "TSArrayType", + line: 1, + column: 17 + } + ] + }, + // Should fail on shorthand syntax Array type as return type. + { + code: `const foo = (): number[] => {}`, + optionsSet: [[]], + output: `const foo = (): readonly number[] => {}`, + errors: [ + { + messageId: "array", + type: "TSArrayType", + line: 1, + column: 17 + } + ] + }, + // Should fail inside function. + { + code: dedent` + const foo = function (): string { + let bar: Array; + };`, + optionsSet: [[]], + output: dedent` + const foo = function (): string { + let bar: ReadonlyArray; + };`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 2, + column: 12 + } + ] + }, + // Tuples. + { + code: dedent` + function foo(tuple: [number, string]) { + }`, + optionsSet: [[]], + output: dedent` + function foo(tuple: readonly [number, string]) { + }`, + errors: [ + { + messageId: "array", + type: "TSTupleType", + line: 1, + column: 21 + } + ] + }, + { + code: dedent` + function foo(tuple: [number, string, [number, string]]) { + }`, + optionsSet: [[]], + output: dedent` + function foo(tuple: readonly [number, string, readonly [number, string]]) { + }`, + errors: [ + { + messageId: "array", + type: "TSTupleType", + line: 1, + column: 21 + }, + { + messageId: "array", + type: "TSTupleType", + line: 1, + column: 38 + } + ] + }, + { + code: dedent` + function foo(tuple: readonly [number, string, [number, string]]) { + }`, + optionsSet: [[]], + output: dedent` + function foo(tuple: readonly [number, string, readonly [number, string]]) { + }`, + errors: [ + { + messageId: "array", + type: "TSTupleType", + line: 1, + column: 47 + } + ] + }, + { + code: dedent` + function foo(tuple: [number, string, readonly [number, string]]) { + }`, + optionsSet: [[]], + output: dedent` + function foo(tuple: readonly [number, string, readonly [number, string]]) { + }`, + errors: [ + { + messageId: "array", + type: "TSTupleType", + line: 1, + column: 21 + } + ] + }, + // Should fail on Array as type literal member as function parameter. + { + code: dedent` + function foo( + param1: { + readonly bar: Array, + readonly baz: ReadonlyArray + } + ): { + readonly bar: Array, + readonly baz: ReadonlyArray + } { + let foo: { + readonly bar: Array, + readonly baz: ReadonlyArray + } = { + bar: ["hello"], + baz: ["world"] + }; + return foo; + }`, + optionsSet: [[]], + output: dedent` + function foo( + param1: { + readonly bar: ReadonlyArray, + readonly baz: ReadonlyArray + } + ): { + readonly bar: ReadonlyArray, + readonly baz: ReadonlyArray + } { + let foo: { + readonly bar: ReadonlyArray, + readonly baz: ReadonlyArray + } = { + bar: ["hello"], + baz: ["world"] + }; + return foo; + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 19 + }, + { + messageId: "array", + type: "TSTypeReference", + line: 7, + column: 17 + }, + { + messageId: "array", + type: "TSTypeReference", + line: 11, + column: 19 + } + ] + }, + // Should fail on Array type alias. + { + code: `type Foo = Array;`, + optionsSet: [[]], + output: `type Foo = ReadonlyArray;`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 12 + } + ] + }, + // Should fail on Array as type member. + { + code: dedent` + function foo() { + type Foo = { + readonly bar: Array + } + }`, + optionsSet: [[]], + output: dedent` + function foo() { + type Foo = { + readonly bar: ReadonlyArray + } + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 19 + } + ] + }, + // Should fail on Array type alias in local type. + { + code: dedent` + function foo() { + type Foo = Array; + }`, + optionsSet: [[]], + output: dedent` + function foo() { + type Foo = ReadonlyArray; + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 2, + column: 14 + } + ] + }, + // Should fail on Array as type member in local type. + { + code: dedent` + function foo() { + type Foo = { + readonly bar: Array + } + }`, + optionsSet: [[]], + output: dedent` + function foo() { + type Foo = { + readonly bar: ReadonlyArray + } + }`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 3, + column: 19 + } + ] + }, + // Should fail on Array type in variable declaration. + { + code: `const foo: Array = [];`, + optionsSet: [[]], + output: `const foo: ReadonlyArray = [];`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 12 + } + ] + }, + // Should fail on shorthand Array syntax. + { + code: `const foo: number[] = [1, 2, 3];`, + optionsSet: [[]], + output: `const foo: readonly number[] = [1, 2, 3];`, + errors: [ + { + messageId: "array", + type: "TSArrayType", + line: 1, + column: 12 + } + ] + }, + // Should fail on Array type being used as template param. + { + code: `let x: Foo>;`, + optionsSet: [[]], + output: `let x: Foo>;`, + errors: [ + { + messageId: "array", + type: "TSTypeReference", + line: 1, + column: 12 + } + ] + }, + // Should fail on nested shorthand arrays. + { + code: `let x: readonly string[][];`, + optionsSet: [[]], + output: `let x: readonly (readonly string[])[];`, + errors: [ + { + messageId: "array", + type: "TSArrayType", + line: 1, + column: 17 + } + ] + }, + // Should fail on implicit Array type in variable declaration. + { + code: dedent` + const foo = [1, 2, 3] + function bar(param = [1, 2, 3]) {}`, + optionsSet: [[{ checkImplicit: true }]], + output: dedent` + const foo: readonly unknown[] = [1, 2, 3] + function bar(param: readonly unknown[] = [1, 2, 3]) {}`, + errors: [ + { + messageId: "implicit", + type: "VariableDeclarator", + line: 1, + column: 7 + }, + { + messageId: "implicit", + type: "AssignmentPattern", + line: 2, + column: 14 + } + ] + }, + // Class Property Signatures. + { + code: dedent` + class Klass { + foo: number; + private bar: number; + static baz: number; + private static qux: number; + }`, + optionsSet: [[]], + output: dedent` + class Klass { + readonly foo: number; + private readonly bar: number; + static readonly baz: number; + private static readonly qux: number; + }`, + errors: [ + { + messageId: "property", + type: "ClassProperty", + line: 2, + column: 3 + }, + { + messageId: "property", + type: "ClassProperty", + line: 3, + column: 3 + }, + { + messageId: "property", + type: "ClassProperty", + line: 4, + column: 3 + }, + { + messageId: "property", + type: "ClassProperty", + line: 5, + column: 3 + } + ] + }, + // Class Parameter Properties. + { + code: dedent` + class Klass { + constructor ( + public publicProp: string, + protected protectedProp: string, + private privateProp: string, + ) { } + }`, + optionsSet: [[]], + output: dedent` + class Klass { + constructor ( + public readonly publicProp: string, + protected readonly protectedProp: string, + private readonly privateProp: string, + ) { } + }`, + errors: [ + { + messageId: "property", + type: "TSParameterProperty", + line: 3, + column: 5 + }, + { + messageId: "property", + type: "TSParameterProperty", + line: 4, + column: 5 + }, + { + messageId: "property", + type: "TSParameterProperty", + line: 5, + column: 5 + } + ] + }, + // Interface Index Signatures. + { + code: dedent` + interface Foo { + [key: string]: string + } + interface Bar { + [key: string]: { prop: string } + }`, + optionsSet: [[]], + output: dedent` + interface Foo { + readonly [key: string]: string + } + interface Bar { + readonly [key: string]: { readonly prop: string } + }`, + errors: [ + { + messageId: "property", + type: "TSIndexSignature", + line: 2, + column: 3 + }, + { + messageId: "property", + type: "TSIndexSignature", + line: 5, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 5, + column: 20 + } + ] + }, + // Function Index Signatures. + { + code: dedent` + function foo(): { [source: string]: string } { + return undefined; + } + function bar(param: { [source: string]: string }): void { + return undefined; + }`, + optionsSet: [[]], + output: dedent` + function foo(): { readonly [source: string]: string } { + return undefined; + } + function bar(param: { readonly [source: string]: string }): void { + return undefined; + }`, + errors: [ + { + messageId: "property", + type: "TSIndexSignature", + line: 1, + column: 19 + }, + { + messageId: "property", + type: "TSIndexSignature", + line: 4, + column: 23 + } + ] + }, + // Type literal with indexer without readonly modifier should produce failures. + { + code: `let foo: { [key: string]: number };`, + optionsSet: [[]], + output: `let foo: { readonly [key: string]: number };`, + errors: [ + { + messageId: "property", + type: "TSIndexSignature", + line: 1, + column: 12 + } + ] + }, + // Type literal in property template parameter without readonly should produce failures. + { + code: dedent` + type foo = ReadonlyArray<{ + type: string, + code: string, + }>;`, + optionsSet: [[]], + output: dedent` + type foo = ReadonlyArray<{ + readonly type: string, + readonly code: string, + }>;`, + errors: [ + { + messageId: "property", + type: "TSPropertySignature", + line: 2, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 3, + column: 3 + } + ] + }, + // Type literal without readonly on members should produce failures. + // Also verify that nested members are checked. + { + code: dedent` + let foo: { + a: number, + b: ReadonlyArray, + c: () => string, + d: { readonly [key: string]: string }, + [key: string]: string, + readonly e: { + a: number, + b: ReadonlyArray, + c: () => string, + d: { readonly [key: string]: string }, + [key: string]: string, + } + };`, + optionsSet: [[]], + output: dedent` + let foo: { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string }, + readonly [key: string]: string, + readonly e: { + readonly a: number, + readonly b: ReadonlyArray, + readonly c: () => string, + readonly d: { readonly [key: string]: string }, + readonly [key: string]: string, + } + };`, + errors: [ + { + messageId: "property", + type: "TSPropertySignature", + line: 2, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 3, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 4, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 5, + column: 3 + }, + { + messageId: "property", + type: "TSIndexSignature", + line: 6, + column: 3 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 8, + column: 5 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 9, + column: 5 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 10, + column: 5 + }, + { + messageId: "property", + type: "TSPropertySignature", + line: 11, + column: 5 + }, + { + messageId: "property", + type: "TSIndexSignature", + line: 12, + column: 5 + } + ] + } +]; + +describe("TypeScript", () => { + const ruleTester = new RuleTester(typescript); + ruleTester.run(name, rule, { + valid: processValidTestCase(valid), + invalid: processInvalidTestCase(invalid) + }); +}); diff --git a/tests/rules/readonly-array.test.ts b/tests/rules/readonly-array.test.ts deleted file mode 100644 index e86ba123a..000000000 --- a/tests/rules/readonly-array.test.ts +++ /dev/null @@ -1,659 +0,0 @@ -/** - * @fileoverview Tests for readonly-array - */ - -import dedent from "dedent"; -import { RuleTester } from "eslint"; - -import { name, rule } from "../../src/rules/readonly-array"; - -import { typescript } from "../configs"; -import { - InvalidTestCase, - processInvalidTestCase, - processValidTestCase, - ValidTestCase -} from "../util"; - -// Valid test cases. -const valid: ReadonlyArray = [ - // Should not fail on explicit ReadonlyArray parameter. - { - code: dedent` - function foo(...numbers: ReadonlyArray) { - }`, - optionsSet: [[]] - }, - { - code: dedent` - function foo(...numbers: readonly number[]) { - }`, - optionsSet: [[]] - }, - // Should not fail on explicit ReadonlyArray return type. - { - code: dedent` - function foo(): ReadonlyArray { - return [1, 2, 3]; - }`, - optionsSet: [[]] - }, - { - code: dedent` - const foo = (): ReadonlyArray => { - return [1, 2, 3]; - }`, - optionsSet: [[]] - }, - // ReadonlyArray Tuple. - { - code: dedent` - function foo(tuple: readonly [number, string, readonly [number, string]]) { - }`, - optionsSet: [[]] - }, - // Should not fail on ReadonlyArray type alias. - { - code: `type Foo = ReadonlyArray;`, - optionsSet: [[]] - }, - // Should not fail on ReadonlyArray type alias in local type. - { - code: dedent` - function foo() { - type Foo = ReadonlyArray; - }`, - optionsSet: [[]] - }, - // Should not fail on ReadonlyArray in variable declaration. - { - code: `const foo: ReadonlyArray = [];`, - optionsSet: [[]] - }, - // Ignore return type. - { - code: dedent` - function foo(...numbers: ReadonlyArray): Array {} - function bar(...numbers: readonly number[]): number[] {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type. - { - code: dedent` - const foo = function(...numbers: ReadonlyArray): Array {} - const bar = function(...numbers: readonly number[]): number[] {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type. - { - code: dedent` - const foo = (...numbers: ReadonlyArray): Array => {} - const bar = (...numbers: readonly number[]): number[] => {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type. - { - code: dedent` - class Foo { - foo(...numbers: ReadonlyArray): Array { - } - } - class Bar { - foo(...numbers: readonly number[]): number[] { - } - }`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with Type Arguments. - { - code: dedent` - function foo(...numbers: ReadonlyArray): Promise> {} - function foo(...numbers: ReadonlyArray): Promise {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with deep Type Arguments. - { - code: dedent` - type Foo = { x: T; }; - function foo(...numbers: ReadonlyArray): Promise>> {} - function foo(...numbers: ReadonlyArray): Promise> {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with Type Arguments in a tuple. - { - code: dedent` - function foo(...numbers: ReadonlyArray): readonly [number, Array, number] {} - function foo(...numbers: ReadonlyArray): readonly [number, number[], number] {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with Type Arguments Union. - { - code: dedent` - function foo(...numbers: ReadonlyArray): { a: Array } | { b: string[] } {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with Type Arguments Intersection. - { - code: dedent` - function foo(...numbers: ReadonlyArray): { a: Array } & { b: string[] } {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Ignore return type with Type Arguments Conditional. - { - code: dedent` - function foo(x: T): T extends Array ? string : number[] {}`, - optionsSet: [[{ ignoreReturnType: true }]] - }, - // Should not fail on implicit ReadonlyArray type in variable declaration. - { - code: dedent` - const foo = [1, 2, 3] as const`, - optionsSet: [[{ checkImplicit: true }]] - }, - // Should not fail on implicit Array. - { - code: dedent` - const foo = [1, 2, 3] - function bar(param = [1, 2, 3]) {}`, - optionsSet: [[]] - } -]; - -// Invalid test cases. -const invalid: ReadonlyArray = [ - { - code: dedent` - function foo(...numbers: number[]) { - }`, - optionsSet: [[]], - output: dedent` - function foo(...numbers: readonly number[]) { - }`, - errors: [ - { - messageId: "generic", - type: "TSArrayType", - line: 1, - column: 26 - } - ] - }, - { - code: dedent` - function foo(...numbers: Array) { - }`, - optionsSet: [[]], - output: dedent` - function foo(...numbers: ReadonlyArray) { - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 26 - } - ] - }, - // Should fail on Array type in interface. - { - code: dedent` - interface Foo { - bar: Array - }`, - optionsSet: [[]], - output: dedent` - interface Foo { - bar: ReadonlyArray - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 2, - column: 8 - } - ] - }, - // Should fail on Array type in index interface. - { - code: dedent`interface Foo { - [key: string]: { - groups: Array - } - }`, - optionsSet: [[]], - output: dedent`interface Foo { - [key: string]: { - groups: ReadonlyArray - } - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 13 - } - ] - }, - // Should fail on Array type as function return type and in local interface. - { - code: dedent` - function foo(): Array { - interface Foo { - bar: Array - } - }`, - optionsSet: [[]], - output: dedent` - function foo(): ReadonlyArray { - interface Foo { - bar: ReadonlyArray - } - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 17 - }, - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 10 - } - ] - }, - // Should fail on Array type as function return type and in local interface. - { - code: dedent` - const foo = (): Array => { - interface Foo { - bar: Array - } - }`, - optionsSet: [[]], - output: dedent` - const foo = (): ReadonlyArray => { - interface Foo { - bar: ReadonlyArray - } - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 17 - }, - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 10 - } - ] - }, - // Should fail on shorthand syntax Array type as return type. - { - code: dedent` - function foo(): number[] { - }`, - optionsSet: [[]], - output: dedent` - function foo(): readonly number[] { - }`, - errors: [ - { - messageId: "generic", - type: "TSArrayType", - line: 1, - column: 17 - } - ] - }, - // Should fail on shorthand syntax Array type as return type. - { - code: `const foo = (): number[] => {}`, - optionsSet: [[]], - output: `const foo = (): readonly number[] => {}`, - errors: [ - { - messageId: "generic", - type: "TSArrayType", - line: 1, - column: 17 - } - ] - }, - // Should fail inside function. - { - code: dedent` - const foo = function (): string { - let bar: Array; - };`, - optionsSet: [[]], - output: dedent` - const foo = function (): string { - let bar: ReadonlyArray; - };`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 2, - column: 12 - } - ] - }, - // Tuples. - { - code: dedent` - function foo(tuple: [number, string]) { - }`, - optionsSet: [[]], - output: dedent` - function foo(tuple: readonly [number, string]) { - }`, - errors: [ - { - messageId: "generic", - type: "TSTupleType", - line: 1, - column: 21 - } - ] - }, - { - code: dedent` - function foo(tuple: [number, string, [number, string]]) { - }`, - optionsSet: [[]], - output: dedent` - function foo(tuple: readonly [number, string, readonly [number, string]]) { - }`, - errors: [ - { - messageId: "generic", - type: "TSTupleType", - line: 1, - column: 21 - }, - { - messageId: "generic", - type: "TSTupleType", - line: 1, - column: 38 - } - ] - }, - { - code: dedent` - function foo(tuple: readonly [number, string, [number, string]]) { - }`, - optionsSet: [[]], - output: dedent` - function foo(tuple: readonly [number, string, readonly [number, string]]) { - }`, - errors: [ - { - messageId: "generic", - type: "TSTupleType", - line: 1, - column: 47 - } - ] - }, - { - code: dedent` - function foo(tuple: [number, string, readonly [number, string]]) { - }`, - optionsSet: [[]], - output: dedent` - function foo(tuple: readonly [number, string, readonly [number, string]]) { - }`, - errors: [ - { - messageId: "generic", - type: "TSTupleType", - line: 1, - column: 21 - } - ] - }, - // Should fail on Array as type literal member as function parameter. - { - code: dedent` - function foo( - param1: { - bar: Array, - baz: ReadonlyArray - } - ): { - bar: Array, - baz: ReadonlyArray - } { - let foo: { - bar: Array, - baz: ReadonlyArray - } = { - bar: ["hello"], - baz: ["world"] - }; - return foo; - }`, - optionsSet: [[]], - output: dedent` - function foo( - param1: { - bar: ReadonlyArray, - baz: ReadonlyArray - } - ): { - bar: ReadonlyArray, - baz: ReadonlyArray - } { - let foo: { - bar: ReadonlyArray, - baz: ReadonlyArray - } = { - bar: ["hello"], - baz: ["world"] - }; - return foo; - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 10 - }, - { - messageId: "generic", - type: "TSTypeReference", - line: 7, - column: 8 - }, - { - messageId: "generic", - type: "TSTypeReference", - line: 11, - column: 10 - } - ] - }, - // Should fail on Array type alias. - { - code: `type Foo = Array;`, - optionsSet: [[]], - output: `type Foo = ReadonlyArray;`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 12 - } - ] - }, - // Should fail on Array as type member. - { - code: dedent` - function foo() { - type Foo = { - bar: Array - } - }`, - optionsSet: [[]], - output: dedent` - function foo() { - type Foo = { - bar: ReadonlyArray - } - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 10 - } - ] - }, - // Should fail on Array type alias in local type. - { - code: dedent` - function foo() { - type Foo = Array; - }`, - optionsSet: [[]], - output: dedent` - function foo() { - type Foo = ReadonlyArray; - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 2, - column: 14 - } - ] - }, - // Should fail on Array as type member in local type. - { - code: dedent` - function foo() { - type Foo = { - bar: Array - } - }`, - optionsSet: [[]], - output: dedent` - function foo() { - type Foo = { - bar: ReadonlyArray - } - }`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 3, - column: 10 - } - ] - }, - // Should fail on Array type in variable declaration. - { - code: `const foo: Array = [];`, - optionsSet: [[]], - output: `const foo: ReadonlyArray = [];`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 12 - } - ] - }, - // Should fail on shorthand Array syntax. - { - code: `const foo: number[] = [1, 2, 3];`, - optionsSet: [[]], - output: `const foo: readonly number[] = [1, 2, 3];`, - errors: [ - { - messageId: "generic", - type: "TSArrayType", - line: 1, - column: 12 - } - ] - }, - // Should fail on Array type being used as template param. - { - code: `let x: Foo>;`, - optionsSet: [[]], - output: `let x: Foo>;`, - errors: [ - { - messageId: "generic", - type: "TSTypeReference", - line: 1, - column: 12 - } - ] - }, - // Should fail on nested shorthand arrays. - { - code: `let x: readonly string[][];`, - optionsSet: [[]], - output: `let x: readonly (readonly string[])[];`, - errors: [ - { - messageId: "generic", - type: "TSArrayType", - line: 1, - column: 17 - } - ] - }, - // Should fail on implicit Array type in variable declaration. - { - code: dedent` - const foo = [1, 2, 3] - function bar(param = [1, 2, 3]) {}`, - optionsSet: [[{ checkImplicit: true }]], - output: dedent` - const foo: readonly unknown[] = [1, 2, 3] - function bar(param: readonly unknown[] = [1, 2, 3]) {}`, - errors: [ - { - messageId: "implicit", - type: "VariableDeclarator", - line: 1, - column: 7 - }, - { - messageId: "implicit", - type: "AssignmentPattern", - line: 2, - column: 14 - } - ] - } -]; - -describe("TypeScript", () => { - const ruleTester = new RuleTester(typescript); - ruleTester.run(name, rule, { - valid: processValidTestCase(valid), - invalid: processInvalidTestCase(invalid) - }); -}); diff --git a/tests/rules/readonly-keyword.test.ts b/tests/rules/readonly-keyword.test.ts deleted file mode 100644 index d4ef17e9e..000000000 --- a/tests/rules/readonly-keyword.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -/** - * @fileoverview Tests for readonly-keyword - */ - -import dedent from "dedent"; -import { RuleTester } from "eslint"; - -import { name, rule } from "../../src/rules/readonly-keyword"; - -import { typescript } from "../configs"; -import { - InvalidTestCase, - processInvalidTestCase, - processValidTestCase, - ValidTestCase -} from "../util"; - -// Valid test cases. -const valid: ReadonlyArray = [ - // Interface with readonly modifiers should not produce failures. - { - code: dedent` - interface Foo { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string }, - readonly [key: string]: string, - }`, - optionsSet: [[]] - }, - // PropertySignature and IndexSignature members without readonly modifier - // should produce failures. Also verify that nested members are checked. - { - code: dedent` - interface Foo { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string }, - readonly [key: string]: string, - readonly e: { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string }, - readonly [key: string]: string, - } - }`, - optionsSet: [[]] - }, - // Class with parameter properties. - { - code: dedent` - class Klass { - constructor ( - nonParameterProp: string, - readonly readonlyProp: string, - public readonly publicReadonlyProp: string, - protected readonly protectedReadonlyProp: string, - private readonly privateReadonlyProp: string, - ) { } - }`, - optionsSet: [[]] - }, - // CallSignature and MethodSignature cannot have readonly modifiers and should - // not produce failures. - { - code: dedent` - interface Foo { - (): void - foo(): void - }`, - optionsSet: [[]] - }, - // The literal with indexer with readonly modifier should not produce failures. - { - code: `let foo: { readonly [key: string]: number };`, - optionsSet: [[]] - }, - // Type literal in array template parameter with readonly should not produce failures. - { - code: `type foo = ReadonlyArray<{ readonly type: string, readonly code: string }>;`, - optionsSet: [[]] - }, - // Type literal with readonly on members should not produce failures. - { - code: dedent` - let foo: { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string } - readonly [key: string]: string - };`, - optionsSet: [[]] - }, - // Ignore Classes. - { - code: dedent` - class Klass { - foo: number; - private bar: number; - static baz: number; - private static qux: number; - }`, - optionsSet: [[{ ignoreClass: true }]] - }, - // Ignore Interfaces. - { - code: dedent` - interface Foo { - foo: number, - bar: Array, - baz: () => string, - qux: { [key: string]: string } - }`, - optionsSet: [[{ ignoreInterface: true }]] - }, - // Ignore Local. - { - code: dedent` - function foo() { - let foo: { - a: number, - b: Array, - c: () => string, - d: { [key: string]: string }, - [key: string]: string, - readonly d: { - a: number, - b: Array, - c: () => string, - d: { [key: string]: string }, - [key: string]: string, - } - } - };`, - optionsSet: [[{ ignoreLocal: true }]] - }, - // Ignore Prefix. - { - code: dedent` - let foo: { - mutableA: number, - mutableB: Array, - mutableC: () => string, - mutableD: { readonly [key: string]: string }, - mutableE: { - mutableA: number, - mutableB: Array, - mutableC: () => string, - mutableD: { readonly [key: string]: string }, - } - };`, - optionsSet: [[{ ignorePattern: "^mutable" }]] - }, - // Ignore Suffix. - { - code: dedent` - let foo: { - aMutable: number, - bMutable: Array, - cMutable: () => string, - dMutable: { readonly [key: string]: string }, - eMutable: { - aMutable: number, - bMutable: Array, - cMutable: () => string, - dMutable: { readonly [key: string]: string }, - } - };`, - optionsSet: [[{ ignorePattern: "Mutable$" }]] - } -]; - -// Invalid test cases. -const invalid: ReadonlyArray = [ - // Class Property Signatures. - { - code: dedent` - class Klass { - foo: number; - private bar: number; - static baz: number; - private static qux: number; - }`, - optionsSet: [[]], - output: dedent` - class Klass { - readonly foo: number; - private readonly bar: number; - static readonly baz: number; - private static readonly qux: number; - }`, - errors: [ - { - messageId: "generic", - type: "ClassProperty", - line: 2, - column: 3 - }, - { - messageId: "generic", - type: "ClassProperty", - line: 3, - column: 3 - }, - { - messageId: "generic", - type: "ClassProperty", - line: 4, - column: 3 - }, - { - messageId: "generic", - type: "ClassProperty", - line: 5, - column: 3 - } - ] - }, - // Class Parameter Properties. - { - code: dedent` - class Klass { - constructor ( - public publicProp: string, - protected protectedProp: string, - private privateProp: string, - ) { } - }`, - optionsSet: [[]], - output: dedent` - class Klass { - constructor ( - public readonly publicProp: string, - protected readonly protectedProp: string, - private readonly privateProp: string, - ) { } - }`, - errors: [ - { - messageId: "generic", - type: "TSParameterProperty", - line: 3, - column: 5 - }, - { - messageId: "generic", - type: "TSParameterProperty", - line: 4, - column: 5 - }, - { - messageId: "generic", - type: "TSParameterProperty", - line: 5, - column: 5 - } - ] - }, - // Interface Index Signatures. - { - code: dedent` - interface Foo { - [key: string]: string - } - interface Bar { - [key: string]: { prop: string } - }`, - optionsSet: [[]], - output: dedent` - interface Foo { - readonly [key: string]: string - } - interface Bar { - readonly [key: string]: { readonly prop: string } - }`, - errors: [ - { - messageId: "generic", - type: "TSIndexSignature", - line: 2, - column: 3 - }, - { - messageId: "generic", - type: "TSIndexSignature", - line: 5, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 5, - column: 20 - } - ] - }, - // Function Index Signatures. - { - code: dedent` - function foo(): { [source: string]: string } { - return undefined; - } - function bar(param: { [source: string]: string }): void { - return undefined; - }`, - optionsSet: [[]], - output: dedent` - function foo(): { readonly [source: string]: string } { - return undefined; - } - function bar(param: { readonly [source: string]: string }): void { - return undefined; - }`, - errors: [ - { - messageId: "generic", - type: "TSIndexSignature", - line: 1, - column: 19 - }, - { - messageId: "generic", - type: "TSIndexSignature", - line: 4, - column: 23 - } - ] - }, - // Type literal with indexer without readonly modifier should produce failures. - { - code: `let foo: { [key: string]: number };`, - optionsSet: [[]], - output: `let foo: { readonly [key: string]: number };`, - errors: [ - { - messageId: "generic", - type: "TSIndexSignature", - line: 1, - column: 12 - } - ] - }, - // Type literal in array template parameter without readonly should produce failures. - { - code: dedent` - type foo = ReadonlyArray<{ - type: string, - code: string, - }>;`, - optionsSet: [[]], - output: dedent` - type foo = ReadonlyArray<{ - readonly type: string, - readonly code: string, - }>;`, - errors: [ - { - messageId: "generic", - type: "TSPropertySignature", - line: 2, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 3, - column: 3 - } - ] - }, - // Type literal without readonly on members should produce failures. - // Also verify that nested members are checked. - { - code: dedent` - let foo: { - a: number, - b: Array, - c: () => string, - d: { readonly [key: string]: string }, - [key: string]: string, - readonly e: { - a: number, - b: Array, - c: () => string, - d: { readonly [key: string]: string }, - [key: string]: string, - } - };`, - optionsSet: [[]], - output: dedent` - let foo: { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string }, - readonly [key: string]: string, - readonly e: { - readonly a: number, - readonly b: Array, - readonly c: () => string, - readonly d: { readonly [key: string]: string }, - readonly [key: string]: string, - } - };`, - errors: [ - { - messageId: "generic", - type: "TSPropertySignature", - line: 2, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 3, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 4, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 5, - column: 3 - }, - { - messageId: "generic", - type: "TSIndexSignature", - line: 6, - column: 3 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 8, - column: 5 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 9, - column: 5 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 10, - column: 5 - }, - { - messageId: "generic", - type: "TSPropertySignature", - line: 11, - column: 5 - }, - { - messageId: "generic", - type: "TSIndexSignature", - line: 12, - column: 5 - } - ] - } -]; - -describe("TypeScript", () => { - const ruleTester = new RuleTester(typescript); - ruleTester.run(name, rule, { - valid: processValidTestCase(valid), - invalid: processInvalidTestCase(invalid) - }); -}); From c1b62a7d864c9d4b62f0da9855065099758194cb Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 19 Jul 2019 11:46:16 +1200 Subject: [PATCH 2/5] fix(prefer-readonly-types): Give tuples their own error message. --- src/rules/prefer-readonly-types.ts | 4 +++- src/util/typeguard.ts | 6 ++++++ tests/rules/prefer-readonly-types.test.ts | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/rules/prefer-readonly-types.ts b/src/rules/prefer-readonly-types.ts index fbf2a22ec..513985d18 100644 --- a/src/rules/prefer-readonly-types.ts +++ b/src/rules/prefer-readonly-types.ts @@ -23,6 +23,7 @@ import { isTSArrayType, isTSIndexSignature, isTSParameterProperty, + isTSTupleType, isTSTypeOperator } from "../util/typeguard"; @@ -70,6 +71,7 @@ const defaultOptions: Options = { // The possible error messages. const errorMessages = { array: "Only readonly arrays allowed.", + tuple: "Only readonly tuples allowed.", implicit: "Implicitly a mutable array. Only readonly arrays allowed.", property: "A readonly modifier is required." } as const; @@ -105,7 +107,7 @@ function checkArrayOrTupleType( ? [ { node, - messageId: "array", + messageId: isTSTupleType(node) ? "tuple" : "array", fix: node.parent && isTSArrayType(node.parent) ? fixer => [ diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index a058a6ddb..17ce6e912 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -166,6 +166,12 @@ export function isTSPropertySignature( return node.type === AST_NODE_TYPES.TSPropertySignature; } +export function isTSTupleType( + node: TSESTree.Node +): node is TSESTree.TSTupleType { + return node.type === AST_NODE_TYPES.TSTupleType; +} + export function isTSTypeAliasDeclaration( node: TSESTree.Node ): node is TSESTree.TSTypeAliasDeclaration { diff --git a/tests/rules/prefer-readonly-types.test.ts b/tests/rules/prefer-readonly-types.test.ts index de165eea6..30514e001 100644 --- a/tests/rules/prefer-readonly-types.test.ts +++ b/tests/rules/prefer-readonly-types.test.ts @@ -517,7 +517,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "tuple", type: "TSTupleType", line: 1, column: 21 @@ -534,13 +534,13 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "tuple", type: "TSTupleType", line: 1, column: 21 }, { - messageId: "array", + messageId: "tuple", type: "TSTupleType", line: 1, column: 38 @@ -557,7 +557,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "tuple", type: "TSTupleType", line: 1, column: 47 @@ -574,7 +574,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "tuple", type: "TSTupleType", line: 1, column: 21 From 86c0ee5d2da37a9d69046b4d6638e4dfcd73874e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 19 Jul 2019 12:17:13 +1200 Subject: [PATCH 3/5] fix(prefer-readonly-types): replace readonly-array and readonly-keyword with prefer-readonly-types --- .eslintrc | 6 ++++-- src/configs/all.ts | 3 +-- src/configs/immutable.ts | 3 +-- tests/rules/_work.test.ts | 2 +- tests/util.ts | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.eslintrc b/.eslintrc index 0246dcc90..5fd24bcdd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -33,8 +33,10 @@ // Our rules. "ts-immutable/immutable-data": "error", "ts-immutable/no-let": "error", - "ts-immutable/readonly-array": ["error", { "ignoreReturnType": true }], - "ts-immutable/readonly-keyword": "error", + "ts-immutable/prefer-readonly-types": [ + "error", + { "ignoreReturnType": true } + ], "ts-immutable/no-method-signature": "error", "ts-immutable/no-this": "error", "ts-immutable/no-class": "error", diff --git a/src/configs/all.ts b/src/configs/all.ts index fd1c59d64..cbeb442bf 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -17,8 +17,7 @@ const config = { rules: { "ts-immutable/no-method-signature": "error", "ts-immutable/no-mixed-interface": "error", - "ts-immutable/readonly-array": "error", - "ts-immutable/readonly-keyword": "error" + "ts-immutable/prefer-readonly-types": "error" } } ] diff --git a/src/configs/immutable.ts b/src/configs/immutable.ts index efff06d63..ab82b994b 100644 --- a/src/configs/immutable.ts +++ b/src/configs/immutable.ts @@ -14,8 +14,7 @@ const config = deepMerge([ files: ["*.ts", "*.tsx"], rules: { "ts-immutable/no-method-signature": "warn", - "ts-immutable/readonly-array": "error", - "ts-immutable/readonly-keyword": "error" + "ts-immutable/prefer-readonly-types": "error" } } ] diff --git a/tests/rules/_work.test.ts b/tests/rules/_work.test.ts index 093545912..6f2c93bcf 100644 --- a/tests/rules/_work.test.ts +++ b/tests/rules/_work.test.ts @@ -9,7 +9,7 @@ import { RuleTester } from "eslint"; * Step 1. * Import the rule to test. */ -import { rule } from "../../src/rules/readonly-array"; +import { rule } from "../../src/rules/prefer-readonly-types"; import { typescript } from "../configs"; diff --git a/tests/util.ts b/tests/util.ts index 917e614a9..fd582ff57 100644 --- a/tests/util.ts +++ b/tests/util.ts @@ -61,7 +61,7 @@ export function processInvalidTestCase( ]; }, [] - /* eslint-disable ts-immutable/readonly-array */ + /* eslint-disable-next-line ts-immutable/prefer-readonly-types */ ) as Array; } @@ -89,5 +89,5 @@ export function createDummyRule( }; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - return createRule<"generic", Array>("dummy", meta, [], create); + return createRule<"generic", ReadonlyArray>("dummy", meta, [], create); } From 9c62d672f93fc353d5f383b007ddd50c60a017e2 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 19 Jul 2019 12:17:40 +1200 Subject: [PATCH 4/5] docs(prefer-readonly-types): remove old docs --- docs/rules/readonly-keyword.md | 128 --------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 docs/rules/readonly-keyword.md diff --git a/docs/rules/readonly-keyword.md b/docs/rules/readonly-keyword.md deleted file mode 100644 index 35bed3653..000000000 --- a/docs/rules/readonly-keyword.md +++ /dev/null @@ -1,128 +0,0 @@ -# Enforce readonly modifiers are used where possible (readonly-keyword) - -This rule enforces use of the `readonly` modifier. The `readonly` modifier can appear on property signatures in interfaces, property declarations in classes, and index signatures. - -## Rule Details - -Below is some information about the `readonly` modifier and the benefits of using it: - -You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`. - -```typescript -interface Point { - x: number; - y: number; -} -const point: Point = { x: 23, y: 44 }; -point.x = 99; // This is legal -``` - -This is why the `readonly` modifier exists. It prevents you from assigning a value to the result of a member expression. - -```typescript -interface Point { - readonly x: number; - readonly y: number; -} -const point: Point = { x: 23, y: 44 }; -point.x = 99; // <- No object mutation allowed. -``` - -This is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However the `readonly` modifier has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the ES2016 object spread [syntax](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#object-spread-and-rest) that was added in typescript 2.1: - -```typescript -interface Point { - readonly x: number; - readonly y: number; -} -const point: Point = { x: 23, y: 44 }; -const transformedPoint = { ...point, x: 99 }; -``` - -Note that you can also use object spread when destructuring to [delete keys](http://stackoverflow.com/questions/35342355/remove-data-from-nested-objects-without-mutating/35676025#35676025) in an object: - -```typescript -let { [action.id]: deletedItem, ...rest } = state; -``` - -The `readonly` modifier also works on indexers: - -```typescript -const foo: { readonly [key: string]: number } = { a: 1, b: 2 }; -foo["a"] = 3; // Error: Index signature only permits reading -``` - -## Options - -The rule accepts an options object with the following properties: - -```typescript -type Options = { - readonly ignoreClass?: boolean; - readonly ignoreInterface?: boolean; - readonly ignoreLocal?: boolean; - readonly ignorePattern?: string | Array; -}; - -const defaults = { - ignoreClass: false, - ignoreInterface: false, - ignoreLocal: false -}; -``` - -### `ignoreClass` - -A boolean to specify if checking for `readonly` should apply to classes. `false` by default. - -Examples of **incorrect** code for the `{ "ignoreClass": false }` option: - -```ts -/*eslint ts-immutable/readonly: ["error", { "ignoreClass": false }]*/ - -class { - myprop: string; -} -``` - -Examples of **correct** code for the `{ "ignoreClass": true }` option: - -```ts -/*eslint ts-immutable/readonly: ["error", { "ignoreClass": true }]*/ - -class { - myprop: string; -} -``` - -### `ignoreInterface` - -A boolean to specify if checking for `readonly` should apply to interfaces. `false` by default. - -Examples of **incorrect** code for the `{ "ignoreInterface": false }` option: - -```ts -/*eslint ts-immutable/readonly: ["error", { "ignoreInterface": false }]*/ - -interface { - myprop: string; -} -``` - -Examples of **correct** code for the `{ "ignoreInterface": true }` option: - -```ts -/*eslint ts-immutable/readonly: ["error", { "ignoreInterface": true }]*/ - -interface { - myprop: string; -} -``` - -### `ignoreLocal` - -See the [ignoreLocal](./options/ignore-local.md) docs. - -### `ignorePattern` - -See the [ignorePattern](./options/ignore-pattern.md) docs. From 8ac0f2507e768ab9b7ca211cf4ed2b66e35657c6 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 19 Jul 2019 12:08:25 +1200 Subject: [PATCH 5/5] feat(prefer-readonly-types): Enforce ReadonlySet over Set and ReadonlyMap over Map --- src/rules/prefer-readonly-types.ts | 46 +++++++++------ tests/rules/prefer-readonly-types.test.ts | 68 +++++++++++++++++------ 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/src/rules/prefer-readonly-types.ts b/src/rules/prefer-readonly-types.ts index 513985d18..a65de1934 100644 --- a/src/rules/prefer-readonly-types.ts +++ b/src/rules/prefer-readonly-types.ts @@ -71,9 +71,10 @@ const defaultOptions: Options = { // The possible error messages. const errorMessages = { array: "Only readonly arrays allowed.", - tuple: "Only readonly tuples allowed.", implicit: "Implicitly a mutable array. Only readonly arrays allowed.", - property: "A readonly modifier is required." + property: "A readonly modifier is required.", + tuple: "Only readonly tuples allowed.", + type: "Only readonly types allowed." } as const; // The meta data for this rule. @@ -89,6 +90,11 @@ const meta: RuleMetaData = { schema }; +const mutableToImmutableTypes: ReadonlyMap = new Map< + string, + string +>([["Array", "ReadonlyArray"], ["Map", "ReadonlyMap"], ["Set", "ReadonlySet"]]); + /** * Check if the given ArrayType or TupleType violates this rule. */ @@ -129,21 +135,27 @@ function checkTypeReference( context: RuleContext, options: Options ): RuleResult { - return { - context, - descriptors: - isIdentifier(node.typeName) && - node.typeName.name === "Array" && - (!options.ignoreReturnType || !isInReturnType(node)) - ? [ - { - node, - messageId: "array", - fix: fixer => fixer.insertTextBefore(node, "Readonly") - } - ] - : [] - }; + if (isIdentifier(node.typeName)) { + const immutableType = mutableToImmutableTypes.get(node.typeName.name); + return { + context, + descriptors: + immutableType && (!options.ignoreReturnType || !isInReturnType(node)) + ? [ + { + node, + messageId: "type", + fix: fixer => fixer.replaceText(node.typeName, immutableType) + } + ] + : [] + }; + } else { + return { + context, + descriptors: [] + }; + } } /** diff --git a/tests/rules/prefer-readonly-types.test.ts b/tests/rules/prefer-readonly-types.test.ts index 30514e001..c1ea99253 100644 --- a/tests/rules/prefer-readonly-types.test.ts +++ b/tests/rules/prefer-readonly-types.test.ts @@ -343,13 +343,47 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 26 } ] }, + { + code: dedent` + function foo(numbers: Set) { + }`, + optionsSet: [[]], + output: dedent` + function foo(numbers: ReadonlySet) { + }`, + errors: [ + { + messageId: "type", + type: "TSTypeReference", + line: 1, + column: 23 + } + ] + }, + { + code: dedent` + function foo(numbers: Map) { + }`, + optionsSet: [[]], + output: dedent` + function foo(numbers: ReadonlyMap) { + }`, + errors: [ + { + messageId: "type", + type: "TSTypeReference", + line: 1, + column: 23 + } + ] + }, // Should fail on Array type in interface. { code: dedent` @@ -363,7 +397,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 2, column: 17 @@ -387,7 +421,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 22 @@ -411,13 +445,13 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 17 }, { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 19 @@ -441,13 +475,13 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 17 }, { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 19 @@ -499,7 +533,7 @@ const invalid: ReadonlyArray = [ };`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 2, column: 12 @@ -624,19 +658,19 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 19 }, { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 7, column: 17 }, { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 11, column: 19 @@ -650,7 +684,7 @@ const invalid: ReadonlyArray = [ output: `type Foo = ReadonlyArray;`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 12 @@ -674,7 +708,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 19 @@ -694,7 +728,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 2, column: 14 @@ -718,7 +752,7 @@ const invalid: ReadonlyArray = [ }`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 3, column: 19 @@ -732,7 +766,7 @@ const invalid: ReadonlyArray = [ output: `const foo: ReadonlyArray = [];`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 12 @@ -760,7 +794,7 @@ const invalid: ReadonlyArray = [ output: `let x: Foo>;`, errors: [ { - messageId: "array", + messageId: "type", type: "TSTypeReference", line: 1, column: 12