diff --git a/.eslintrc b/.eslintrc index eb00ca638..02f1e9ed4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,8 +34,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/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/readonly-keyword.md b/docs/rules/prefer-readonly-types.md similarity index 67% rename from docs/rules/readonly-keyword.md rename to docs/rules/prefer-readonly-types.md index 35bed3653..752648641 100644 --- a/docs/rules/readonly-keyword.md +++ b/docs/rules/prefer-readonly-types.md @@ -1,10 +1,14 @@ -# Enforce readonly modifiers are used where possible (readonly-keyword) +# Prefer readonly types over mutable types (prefer-readonly-types) -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. +This rule enforces use of the readonly modifier and readonly types. ## Rule Details -Below is some information about the `readonly` modifier and the benefits of using it: +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`. @@ -52,25 +56,66 @@ 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 + 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. 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/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/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 70% rename from src/rules/readonly-array.ts rename to src/rules/prefer-readonly-types.ts index b2b59fab7..a65de1934 100644 --- a/src/rules/readonly-array.ts +++ b/src/rules/prefer-readonly-types.ts @@ -21,15 +21,20 @@ import { isFunctionLike, isIdentifier, isTSArrayType, + isTSIndexSignature, + isTSParameterProperty, + isTSTupleType, 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 +44,8 @@ const schema: JSONSchema4 = [ deepMerge([ ignore.ignoreLocalOptionSchema, ignore.ignorePatternOptionSchema, + ignore.ignoreClassOptionSchema, + ignore.ignoreInterfaceOptionSchema, ignore.ignoreReturnTypeOptionSchema, { type: "object", @@ -54,15 +61,20 @@ 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.", + tuple: "Only readonly tuples allowed.", + type: "Only readonly types allowed." } as const; // The meta data for this rule. @@ -78,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. */ @@ -96,7 +113,7 @@ function checkArrayOrTupleType( ? [ { node, - messageId: "generic", + messageId: isTSTupleType(node) ? "tuple" : "array", fix: node.parent && isTSArrayType(node.parent) ? fixer => [ @@ -117,21 +134,56 @@ function checkTypeReference( node: TSESTree.TSTypeReference, context: RuleContext, options: Options +): RuleResult { + 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: [] + }; + } +} + +/** + * 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: - isIdentifier(node.typeName) && - node.typeName.name === "Array" && - (!options.ignoreReturnType || !isInReturnType(node)) - ? [ - { - node, - messageId: "generic", - fix: fixer => fixer.insertTextBefore(node, "Readonly") - } - ] - : [] + 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 ") + } + ] }; } @@ -203,12 +255,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/src/util/typeguard.ts b/src/util/typeguard.ts index 5b6f15d04..c9e4e173a 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -179,6 +179,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/_work.test.ts b/tests/rules/_work.test.ts index 21f390b99..76a9a13ae 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/rules/prefer-readonly-types.test.ts b/tests/rules/prefer-readonly-types.test.ts new file mode 100644 index 000000000..a604b0b12 --- /dev/null +++ b/tests/rules/prefer-readonly-types.test.ts @@ -0,0 +1,1143 @@ +/** + * @file 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: "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` + interface Foo { + readonly bar: Array + }`, + optionsSet: [[]], + output: dedent` + interface Foo { + readonly bar: ReadonlyArray + }`, + errors: [ + { + messageId: "type", + 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: "type", + 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: "type", + type: "TSTypeReference", + line: 1, + column: 17 + }, + { + messageId: "type", + 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: "type", + type: "TSTypeReference", + line: 1, + column: 17 + }, + { + messageId: "type", + 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: "type", + 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: "tuple", + 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: "tuple", + type: "TSTupleType", + line: 1, + column: 21 + }, + { + messageId: "tuple", + 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: "tuple", + 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: "tuple", + 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: "type", + type: "TSTypeReference", + line: 3, + column: 19 + }, + { + messageId: "type", + type: "TSTypeReference", + line: 7, + column: 17 + }, + { + messageId: "type", + type: "TSTypeReference", + line: 11, + column: 19 + } + ] + }, + // Should fail on Array type alias. + { + code: `type Foo = Array;`, + optionsSet: [[]], + output: `type Foo = ReadonlyArray;`, + errors: [ + { + messageId: "type", + 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: "type", + 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: "type", + 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: "type", + 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: "type", + 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: "type", + 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 51eb2e4c0..000000000 --- a/tests/rules/readonly-array.test.ts +++ /dev/null @@ -1,659 +0,0 @@ -/** - * @file 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 94c8912b9..000000000 --- a/tests/rules/readonly-keyword.test.ts +++ /dev/null @@ -1,480 +0,0 @@ -/** - * @file 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) - }); -}); diff --git a/tests/util.ts b/tests/util.ts index 94424e668..972fb851b 100644 --- a/tests/util.ts +++ b/tests/util.ts @@ -49,7 +49,7 @@ export function processInvalidTestCase( ]; }, [] - /* eslint-disable ts-immutable/readonly-array */ + /* eslint-disable-next-line ts-immutable/prefer-readonly-types */ ) as Array; } @@ -92,5 +92,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); }