diff --git a/.cspell.json b/.cspell.json
index 5df00838e..c49a53d05 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -28,7 +28,8 @@
"/`[^`]*`/",
"/\\.\\/docs\\/rules\\/[^.]*.md/",
"/TS[^\\s]+/",
- "\\(#.+?\\)"
+ "\\(#.+?\\)",
+ "\/\/ @ts-.*"
],
"words": [
"globstar",
diff --git a/README.md b/README.md
index be0f913a9..71440d3e7 100644
--- a/README.md
+++ b/README.md
@@ -192,12 +192,12 @@ The [below section](#supported-rules) gives details on which rules are enabled b
:see_no_evil: = `no-mutations` Ruleset.
-| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: |
-| -------------------------------------------------------------- | -------------------------------------------------------------------------- | :---------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: |
-| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
-| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
-| [`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: |
-| [`prefer-readonly-type`](./docs/rules/prefer-readonly-type.md) | Use readonly types and readonly modifiers where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: |
+| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: |
+| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | :---------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: |
+| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
+| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
+| [`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: |
+| [`prefer-readonly-type-declaration`](./docs/rules/prefer-readonly-type-declaration.md) | Encore use of readonly types where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: |
### No Object-Orientation Rules
diff --git a/docs/rules/prefer-readonly-type-declaration.md b/docs/rules/prefer-readonly-type-declaration.md
new file mode 100644
index 000000000..60eb1c09f
--- /dev/null
+++ b/docs/rules/prefer-readonly-type-declaration.md
@@ -0,0 +1,236 @@
+# Prefer readonly types over mutable types (prefer-readonly-type-declaration)
+
+This rule enforces use of readonly type declarations.
+
+## Rule Details
+
+This rule checks that declared types are deeply readonly (unless declared to not be).
+
+It can also be used to enforce a naming convention for readonly vs mutable type aliases and interfaces.
+
+Examples of **incorrect** code for this rule:
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: "error" */
+
+type Point = {
+ x: number;
+ y: number;
+};
+
+const point: Point = { x: 23, y: 44 };
+point.x = 99;
+```
+
+Examples of **correct** code for this rule:
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: "error" */
+
+type Point = {
+ readonly x: number;
+ readonly y: number;
+};
+const point1: Point = { x: 23, y: 44 };
+const transformedPoint1 = { ...point, x: 99 };
+
+type MutablePoint = {
+ x: number;
+ y: number;
+};
+const point2: MutablePoint = { x: 23, y: 44 };
+point2.x = 99;
+```
+
+## Options
+
+This rule accepts an options object of the following type:
+
+```ts
+{
+ allowLocalMutation: boolean;
+ allowMutableReturnType: boolean;
+ ignoreClass: boolean | "fieldsOnly";
+ ignoreInterface: boolean;
+ ignoreCollections: boolean;
+ ignorePattern?: string | Array;
+ aliases: {
+ mustBeReadonly: {
+ pattern: ReadonlyArray | string;
+ requireOthersToBeMutable: boolean;
+ };
+ mustBeMutable: {
+ pattern: ReadonlyArray | string;
+ requireOthersToBeReadonly: boolean;
+ };
+ blacklist: ReadonlyArray | string;
+ };
+}
+```
+
+The default options:
+
+```ts
+{
+ allowLocalMutation: false,
+ allowMutableReturnType: true,
+ ignoreClass: false,
+ ignoreInterface: false,
+ ignoreCollections: false,
+ aliases: {
+ blacklist: "^Mutable$",
+ mustBeReadonly: {
+ pattern: "^(I?)Readonly",
+ requireOthersToBeMutable: false,
+ },
+ mustBeMutable: {
+ pattern: "^(I?)Mutable",
+ requireOthersToBeReadonly: true,
+ },
+ },
+}
+```
+
+### `allowMutableReturnType`
+
+If set, return types of functions will not be checked.
+
+### `ignoreClass`
+
+If set, classes will not be checked.
+
+Examples of **incorrect** code for the `{ "ignoreClass": false }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreClass": false }] */
+
+class {
+ myprop: string;
+}
+```
+
+Examples of **correct** code for the `{ "ignoreClass": true }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreClass": true }] */
+
+class {
+ myprop: string;
+}
+```
+
+### `ignoreInterface`
+
+If set, interfaces will not be checked.
+
+Examples of **incorrect** code for the `{ "ignoreInterface": false }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreInterface": false }] */
+
+interface {
+ myprop: string;
+}
+```
+
+Examples of **correct** code for the `{ "ignoreInterface": true }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreInterface": true }] */
+
+interface {
+ myprop: string;
+}
+```
+
+### `ignoreCollections`
+
+If set, collections (Array, Tuple, Set, and Map) will not be required to be readonly when used outside of type aliases and interfaces.
+
+Examples of **incorrect** code for the `{ "ignoreCollections": false }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreCollections": false }] */
+
+const foo: number[] = [];
+const bar: [string, string] = ["foo", "bar"];
+const baz: Set = new Set();
+const qux: Map = new Map();
+```
+
+Examples of **correct** code for the `{ "ignoreCollections": true }` option:
+
+```ts
+/* eslint functional/readonly: ["error", { "ignoreCollections": true }] */
+
+const foo: number[] = [];
+const bar: [string, string] = ["foo", "bar"];
+const baz: Set = new Set();
+const qux: Map = new Map();
+```
+
+### `aliases`
+
+These options apply only to type aliases and interface declarations.
+
+#### `aliases.mustBeReadonly`
+
+##### `aliases.mustBeReadonly.pattern`
+
+The regex pattern(s) used to test against the type's name. If it's a match the type must be deeply readonly.
+
+Set to an empty array to disable this check.
+
+##### `aliases.mustBeReadonly.requireOthersToBeMutable`
+
+If set, all other types that don't match the pattern(s) must **not** be deeply readonly.
+
+#### `aliases.mustBeMutable`
+
+##### `aliases.mustBeMutable.pattern`
+
+The regex pattern(s) used to test against the type's name. If it's a match the type must **not** be deeply readonly.
+
+Set to an empty array to disable this check.
+
+##### `aliases.mustBeMutable.requireOthersToBeReadonly`
+
+If set, all other types that don't match the pattern(s) must be deeply readonly.
+
+#### `aliases.blacklist`
+
+Any type names that match this regex pattern(s) will be ignored by this rule.
+
+#### `aliases` Examples
+
+By toggling the default settings of `aliases.mustBeReadonly.requireOthersToBeMutable` and `aliases.mustBeMutable.requireOthersToBeReadonly`, you can make it so that types are mutable by default and immutable versions need to be prefixed. This more closely matches how TypeScript itself implements types like `Set` and `ReadonlySet`.
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: ["error", { "aliases": { "mustBeReadonly": { "requireOthersToBeMutable": true }, "mustBeMutable": { "requireOthersToBeReadonly": false } } }] */
+
+type Point = {
+ x: number;
+ y: number;
+};
+type ReadonlyPoint = Readonly;
+```
+
+Alternatively, if both `aliases.mustBeReadonly.requireOthersToBeMutable` and `aliases.mustBeMutable.requireOthersToBeReadonly` are set, you can make it so that types explicitly need to be marked as either readonly or mutable.
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: ["error", { "aliases": { "mustBeReadonly": { "requireOthersToBeMutable": true }, "mustBeMutable": { "requireOthersToBeReadonly": true } } }] */
+
+type MutablePoint = {
+ x: number;
+ y: number;
+};
+type ReadonlyPoint = Readonly;
+```
+
+### `allowLocalMutation`
+
+See the [allowLocalMutation](./options/allow-local-mutation.md) docs.
+
+### `ignorePattern`
+
+See the [ignorePattern](./options/ignore-pattern.md) docs.
diff --git a/docs/rules/prefer-readonly-type.md b/docs/rules/prefer-readonly-type.md
index 1043c89c2..4749abba7 100644
--- a/docs/rules/prefer-readonly-type.md
+++ b/docs/rules/prefer-readonly-type.md
@@ -1,6 +1,8 @@
# Prefer readonly types over mutable types (prefer-readonly-type)
-This rule enforces use of the readonly modifier and readonly types.
+## :warning: This rule is deprecated
+
+This rule has been replaced by [prefer-readonly-type-declaration](./prefer-readonly-type-declaration.md) and [@typescript-eslint/prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md)
## Rule Details
diff --git a/src/configs/all.ts b/src/configs/all.ts
index 0db73d9d0..690bc34d1 100644
--- a/src/configs/all.ts
+++ b/src/configs/all.ts
@@ -21,7 +21,7 @@ const config: Linter.Config = {
rules: {
"functional/no-method-signature": "error",
"functional/no-mixed-type": "error",
- "functional/prefer-readonly-type": "error",
+ "functional/prefer-readonly-type-declaration": "error",
"functional/prefer-tacit": ["error", { assumeTypes: false }],
"functional/no-return-void": "error",
},
diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts
index bc5cc4600..acd249f7d 100644
--- a/src/configs/no-mutations.ts
+++ b/src/configs/no-mutations.ts
@@ -10,7 +10,7 @@ const config: Linter.Config = {
files: ["*.ts", "*.tsx"],
rules: {
"functional/no-method-signature": "warn",
- "functional/prefer-readonly-type": "error",
+ "functional/prefer-readonly-type-declaration": "error",
},
},
],
diff --git a/src/rules/index.ts b/src/rules/index.ts
index 06eb7baef..699147c16 100644
--- a/src/rules/index.ts
+++ b/src/rules/index.ts
@@ -13,6 +13,7 @@ import * as noThisExpression from "./no-this-expression";
import * as noThrowStatement from "./no-throw-statement";
import * as noTryStatement from "./no-try-statement";
import * as preferReadonlyTypes from "./prefer-readonly-type";
+import * as preferReadonlyTypesDeclaration from "./prefer-readonly-type-declaration";
import * as preferTacit from "./prefer-tacit";
/**
@@ -34,5 +35,6 @@ export const rules = {
[noThrowStatement.name]: noThrowStatement.rule,
[noTryStatement.name]: noTryStatement.rule,
[preferReadonlyTypes.name]: preferReadonlyTypes.rule,
+ [preferReadonlyTypesDeclaration.name]: preferReadonlyTypesDeclaration.rule,
[preferTacit.name]: preferTacit.rule,
};
diff --git a/src/rules/prefer-readonly-type-declaration.ts b/src/rules/prefer-readonly-type-declaration.ts
new file mode 100644
index 000000000..b90ced046
--- /dev/null
+++ b/src/rules/prefer-readonly-type-declaration.ts
@@ -0,0 +1,608 @@
+import type { TSESTree } from "@typescript-eslint/experimental-utils";
+import { all as deepMerge } from "deepmerge";
+import type { JSONSchema4 } from "json-schema";
+
+import type {
+ AllowLocalMutationOption,
+ IgnoreClassOption,
+ IgnoreInterfaceOption,
+ IgnorePatternOption,
+} from "~/common/ignore-options";
+import {
+ allowLocalMutationOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInterfaceOptionSchema,
+ ignorePatternOptionSchema,
+} from "~/common/ignore-options";
+import type { RuleContext, RuleMetaData, RuleResult } from "~/util/rule";
+import { isReadonly, createRule } from "~/util/rule";
+import {
+ getParentIndexSignature,
+ getTypeDeclaration,
+ isInReturnType,
+} from "~/util/tree";
+import {
+ isIdentifier,
+ isTSArrayType,
+ isTSIndexSignature,
+ isTSInterfaceDeclaration,
+ isTSParameterProperty,
+ isTSPropertySignature,
+ isTSTupleType,
+ isTSTypeAliasDeclaration,
+ isTSTypeOperator,
+} from "~/util/typeguard";
+
+// The name of this rule.
+export const name = "prefer-readonly-type-declaration" as const;
+
+// The options this rule can take.
+type Options = AllowLocalMutationOption &
+ IgnoreClassOption &
+ IgnoreInterfaceOption &
+ IgnorePatternOption & {
+ readonly allowMutableReturnType: boolean;
+ readonly ignoreCollections: boolean;
+ readonly aliases: {
+ readonly mustBeReadonly: {
+ readonly pattern: ReadonlyArray | string;
+ readonly requireOthersToBeMutable: boolean;
+ };
+ readonly mustBeMutable: {
+ readonly pattern: ReadonlyArray | string;
+ readonly requireOthersToBeReadonly: boolean;
+ };
+ readonly blacklist: ReadonlyArray | string;
+ };
+ };
+
+// The schema for the rule options.
+const schema: JSONSchema4 = [
+ deepMerge([
+ allowLocalMutationOptionSchema,
+ ignorePatternOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInterfaceOptionSchema,
+ {
+ type: "object",
+ properties: {
+ allowMutableReturnType: {
+ type: "boolean",
+ },
+ ignoreCollections: {
+ type: "boolean",
+ },
+ aliases: {
+ type: "object",
+ properties: {
+ mustBeReadonly: {
+ type: "object",
+ properties: {
+ pattern: {
+ type: ["string", "array"],
+ items: {
+ type: "string",
+ },
+ },
+ requireOthersToBeMutable: {
+ type: "boolean",
+ },
+ },
+ additionalProperties: false,
+ },
+ mustBeMutable: {
+ type: "object",
+ properties: {
+ pattern: {
+ type: ["string", "array"],
+ items: {
+ type: "string",
+ },
+ },
+ requireOthersToBeReadonly: {
+ type: "boolean",
+ },
+ },
+ additionalProperties: false,
+ },
+ blacklist: {
+ type: "array",
+ items: {
+ type: ["string", "array"],
+ items: {
+ type: "string",
+ },
+ },
+ },
+ ignoreInterface: {
+ type: "boolean",
+ },
+ },
+ additionalProperties: false,
+ },
+ },
+ additionalProperties: false,
+ },
+ ]),
+];
+
+// The default options for the rule.
+const defaultOptions: Options = {
+ ignoreClass: false,
+ ignoreInterface: false,
+ ignoreCollections: false,
+ allowLocalMutation: false,
+ allowMutableReturnType: true,
+ aliases: {
+ blacklist: "^Mutable$",
+ mustBeReadonly: {
+ pattern: "^(I?)Readonly",
+ requireOthersToBeMutable: false,
+ },
+ mustBeMutable: {
+ pattern: "^(I?)Mutable",
+ requireOthersToBeReadonly: true,
+ },
+ },
+};
+
+// The possible error messages.
+const errorMessages = {
+ aliasConfigErrorMutableReadonly:
+ "Configuration error - this type must be marked as both readonly and mutable.",
+ aliasNeedsExplicitMarking:
+ "Type must be explicity marked as either readonly or mutable.",
+ aliasShouldBeMutable:
+ "Mutable types should not be fully readonly. If this type is supposed to me readonly, mark it as so.",
+ aliasShouldBeReadonly:
+ "Readonly types should not be mutable at all. If this type is supposed to me mutable, mark it as so.",
+ arrayShouldBeReadonly: "Array should be readonly.",
+ propertyShouldBeReadonly: "This property should be readonly.",
+ tupleShouldBeReadonly: "Tuple should be readonly.",
+ typeShouldBeReadonly: "Type should be readonly.",
+} as const;
+
+// The meta data for this rule.
+const meta: RuleMetaData = {
+ type: "suggestion",
+ docs: {
+ description: "Prefer readonly types over mutable one and enforce patterns.",
+ category: "Best Practices",
+ recommended: "error",
+ },
+ messages: errorMessages,
+ fixable: "code",
+ schema,
+};
+
+const mutableToImmutableTypes: ReadonlyMap = new Map<
+ string,
+ string
+>([
+ ["Array", "ReadonlyArray"],
+ ["Map", "ReadonlyMap"],
+ ["Set", "ReadonlySet"],
+]);
+const mutableTypeRegex = new RegExp(
+ `^${[...mutableToImmutableTypes.keys()].join("|")}$`,
+ "u"
+);
+
+const enum RequiredReadonlyness {
+ READONLY,
+ MUTABLE,
+ EITHER,
+}
+
+const enum TypeReadonlynessDetails {
+ NONE,
+ ERROR_MUTABLE_READONLY,
+ NEEDS_EXPLICIT_MARKING,
+ IGNORE,
+ MUTABLE_OK,
+ MUTABLE_NOT_OK,
+ READONLY_OK,
+ READONLY_NOT_OK,
+}
+
+const cachedDetails = new WeakMap<
+ TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
+ TypeReadonlynessDetails
+>();
+
+/**
+ * Get the details for the given type alias.
+ */
+function getTypeAliasDeclarationDetails(
+ node: TSESTree.Node,
+ context: RuleContext,
+ options: Options
+): TypeReadonlynessDetails {
+ const typeDeclaration = getTypeDeclaration(node);
+ if (typeDeclaration === null) {
+ return TypeReadonlynessDetails.NONE;
+ }
+
+ const indexSignature = getParentIndexSignature(node);
+ if (indexSignature !== null && getTypeDeclaration(indexSignature) !== null) {
+ return TypeReadonlynessDetails.IGNORE;
+ }
+
+ if (options.ignoreInterface && isTSInterfaceDeclaration(typeDeclaration)) {
+ return TypeReadonlynessDetails.IGNORE;
+ }
+
+ const cached = cachedDetails.get(typeDeclaration);
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const result = getTypeAliasDeclarationDetailsInternal(
+ typeDeclaration,
+ context,
+ options
+ );
+ cachedDetails.set(typeDeclaration, result);
+ return result;
+}
+
+/**
+ * Get the details for the given type alias.
+ */
+function getTypeAliasDeclarationDetailsInternal(
+ node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
+ context: RuleContext,
+ options: Options
+): TypeReadonlynessDetails {
+ const blacklistPatterns = (
+ Array.isArray(options.aliases.blacklist)
+ ? options.aliases.blacklist
+ : [options.aliases.blacklist]
+ ).map((pattern) => new RegExp(pattern, "u"));
+
+ const blacklisted = blacklistPatterns.some((pattern) =>
+ pattern.test(node.id.name)
+ );
+
+ if (blacklisted) {
+ return TypeReadonlynessDetails.IGNORE;
+ }
+
+ const mustBeReadonlyPatterns = (
+ Array.isArray(options.aliases.mustBeReadonly.pattern)
+ ? options.aliases.mustBeReadonly.pattern
+ : [options.aliases.mustBeReadonly.pattern]
+ ).map((pattern) => new RegExp(pattern, "u"));
+
+ const mustBeMutablePatterns = (
+ Array.isArray(options.aliases.mustBeMutable.pattern)
+ ? options.aliases.mustBeMutable.pattern
+ : [options.aliases.mustBeMutable.pattern]
+ ).map((pattern) => new RegExp(pattern, "u"));
+
+ const patternStatesReadonly = mustBeReadonlyPatterns.some((pattern) =>
+ pattern.test(node.id.name)
+ );
+ const patternStatesMutable = mustBeMutablePatterns.some((pattern) =>
+ pattern.test(node.id.name)
+ );
+
+ if (patternStatesReadonly && patternStatesMutable) {
+ return TypeReadonlynessDetails.ERROR_MUTABLE_READONLY;
+ }
+
+ if (
+ !patternStatesReadonly &&
+ !patternStatesMutable &&
+ options.aliases.mustBeReadonly.requireOthersToBeMutable &&
+ options.aliases.mustBeMutable.requireOthersToBeReadonly
+ ) {
+ return TypeReadonlynessDetails.NEEDS_EXPLICIT_MARKING;
+ }
+
+ const requiredReadonlyness =
+ patternStatesReadonly ||
+ (!patternStatesMutable &&
+ options.aliases.mustBeMutable.requireOthersToBeReadonly)
+ ? RequiredReadonlyness.READONLY
+ : patternStatesMutable ||
+ (!patternStatesReadonly &&
+ options.aliases.mustBeReadonly.requireOthersToBeMutable)
+ ? RequiredReadonlyness.MUTABLE
+ : RequiredReadonlyness.EITHER;
+
+ if (requiredReadonlyness === RequiredReadonlyness.EITHER) {
+ return TypeReadonlynessDetails.IGNORE;
+ }
+
+ const readonly = isReadonly(
+ isTSTypeAliasDeclaration(node) ? node.typeAnnotation : node.body,
+ context
+ );
+
+ if (requiredReadonlyness === RequiredReadonlyness.MUTABLE) {
+ return readonly
+ ? TypeReadonlynessDetails.MUTABLE_NOT_OK
+ : TypeReadonlynessDetails.MUTABLE_OK;
+ }
+
+ return readonly
+ ? TypeReadonlynessDetails.READONLY_OK
+ : TypeReadonlynessDetails.READONLY_NOT_OK;
+}
+
+/**
+ * Check if the given Interface or Type Alias violates this rule.
+ */
+function checkTypeDeclaration(
+ node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
+ context: RuleContext,
+ options: Options
+): RuleResult {
+ const details = getTypeAliasDeclarationDetails(node, context, options);
+
+ switch (details) {
+ case TypeReadonlynessDetails.NEEDS_EXPLICIT_MARKING: {
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.id,
+ messageId: "aliasNeedsExplicitMarking",
+ },
+ ],
+ };
+ }
+ case TypeReadonlynessDetails.ERROR_MUTABLE_READONLY: {
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.id,
+ messageId: "aliasConfigErrorMutableReadonly",
+ },
+ ],
+ };
+ }
+ case TypeReadonlynessDetails.MUTABLE_NOT_OK: {
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.id,
+ messageId: "aliasShouldBeMutable",
+ },
+ ],
+ };
+ }
+ case TypeReadonlynessDetails.READONLY_NOT_OK: {
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.id,
+ messageId: "aliasShouldBeReadonly",
+ },
+ ],
+ };
+ }
+ default: {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+ }
+}
+
+/**
+ * Check if the given ArrayType or TupleType violates this rule.
+ */
+function checkArrayOrTupleType(
+ node: TSESTree.TSArrayType | TSESTree.TSTupleType,
+ context: RuleContext,
+ options: Options
+): RuleResult {
+ if (options.ignoreCollections) {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+
+ const aliasDetails = getTypeAliasDeclarationDetails(node, context, options);
+
+ switch (aliasDetails) {
+ case TypeReadonlynessDetails.NONE:
+ case TypeReadonlynessDetails.READONLY_NOT_OK: {
+ return {
+ context,
+ descriptors:
+ (node.parent === undefined ||
+ !isTSTypeOperator(node.parent) ||
+ node.parent.operator !== "readonly") &&
+ (!options.allowMutableReturnType || !isInReturnType(node))
+ ? [
+ {
+ node,
+ messageId: isTSTupleType(node)
+ ? "tupleShouldBeReadonly"
+ : "arrayShouldBeReadonly",
+ fix:
+ node.parent !== undefined && isTSArrayType(node.parent)
+ ? (fixer) => [
+ fixer.insertTextBefore(node, "(readonly "),
+ fixer.insertTextAfter(node, ")"),
+ ]
+ : (fixer) => fixer.insertTextBefore(node, "readonly "),
+ },
+ ]
+ : [],
+ };
+ }
+ default: {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+ }
+}
+
+/**
+ * Check if the given TSMappedType violates this rule.
+ */
+function checkMappedType(
+ node: TSESTree.TSMappedType,
+ context: RuleContext,
+ options: Options
+): RuleResult {
+ const aliasDetails = getTypeAliasDeclarationDetails(node, context, options);
+
+ switch (aliasDetails) {
+ case TypeReadonlynessDetails.NONE:
+ case TypeReadonlynessDetails.READONLY_NOT_OK: {
+ return {
+ context,
+ descriptors:
+ node.readonly === true || node.readonly === "+"
+ ? []
+ : [
+ {
+ node,
+ messageId: "propertyShouldBeReadonly",
+ fix: (fixer) =>
+ fixer.insertTextBeforeRange(
+ [node.range[0] + 1, node.range[1]],
+ " readonly"
+ ),
+ },
+ ],
+ };
+ }
+ default: {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+ }
+}
+
+/**
+ * Check if the given TypeReference violates this rule.
+ */
+function checkTypeReference(
+ node: TSESTree.TSTypeReference,
+ context: RuleContext,
+ options: Options
+): RuleResult {
+ if (
+ !isIdentifier(node.typeName) ||
+ (options.ignoreCollections && mutableTypeRegex.test(node.typeName.name))
+ ) {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+
+ const aliasDetails = getTypeAliasDeclarationDetails(node, context, options);
+
+ switch (aliasDetails) {
+ case TypeReadonlynessDetails.NONE:
+ case TypeReadonlynessDetails.READONLY_NOT_OK: {
+ const immutableType = mutableToImmutableTypes.get(node.typeName.name);
+
+ return {
+ context,
+ descriptors:
+ immutableType === undefined ||
+ immutableType.length === 0 ||
+ (options.allowMutableReturnType && isInReturnType(node))
+ ? []
+ : [
+ {
+ node,
+ messageId: "typeShouldBeReadonly",
+ fix: (fixer) =>
+ fixer.replaceText(node.typeName, immutableType),
+ },
+ ],
+ };
+ }
+ default: {
+ 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,
+ options: Options
+): RuleResult {
+ const aliasDetails = getTypeAliasDeclarationDetails(node, context, options);
+
+ switch (aliasDetails) {
+ case TypeReadonlynessDetails.NONE:
+ case TypeReadonlynessDetails.READONLY_NOT_OK: {
+ return {
+ context,
+ descriptors:
+ node.readonly !== true &&
+ (!options.allowMutableReturnType || !isInReturnType(node))
+ ? [
+ {
+ node,
+ messageId: "propertyShouldBeReadonly",
+ fix:
+ isTSIndexSignature(node) || isTSPropertySignature(node)
+ ? (fixer) => fixer.insertTextBefore(node, "readonly ")
+ : isTSParameterProperty(node)
+ ? (fixer) =>
+ fixer.insertTextBefore(node.parameter, "readonly ")
+ : (fixer) =>
+ fixer.insertTextBefore(node.key, "readonly "),
+ },
+ ]
+ : [],
+ };
+ }
+ default: {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+ }
+}
+
+// Create the rule.
+export const rule = createRule(
+ name,
+ meta,
+ defaultOptions,
+ {
+ ClassProperty: checkProperty,
+ TSArrayType: checkArrayOrTupleType,
+ TSIndexSignature: checkProperty,
+ TSInterfaceDeclaration: checkTypeDeclaration,
+ TSMappedType: checkMappedType,
+ TSParameterProperty: checkProperty,
+ TSPropertySignature: checkProperty,
+ TSTupleType: checkArrayOrTupleType,
+ TSTypeAliasDeclaration: checkTypeDeclaration,
+ TSTypeReference: checkTypeReference,
+ }
+);
diff --git a/src/rules/prefer-readonly-type.ts b/src/rules/prefer-readonly-type.ts
index 82fb28086..7c1792a86 100644
--- a/src/rules/prefer-readonly-type.ts
+++ b/src/rules/prefer-readonly-type.ts
@@ -89,6 +89,11 @@ const errorMessages = {
// The meta data for this rule.
const meta: RuleMetaData = {
+ deprecated: true,
+ replacedBy: [
+ "prefer-readonly-type-declaration",
+ "@typescript-eslint/prefer-readonly-parameter-types",
+ ],
type: "suggestion",
docs: {
description: "Prefer readonly array over mutable arrays.",
diff --git a/src/util/rule.ts b/src/util/rule.ts
index b736f1a61..c1ce3924a 100644
--- a/src/util/rule.ts
+++ b/src/util/rule.ts
@@ -138,6 +138,21 @@ export function getTypeOfNode>(
return constrained ?? nodeType;
}
+export function isReadonly>(
+ node: TSESTree.Node,
+ context: Context
+): boolean {
+ const { parserServices } = context;
+
+ if (parserServices === undefined || parserServices.program === undefined) {
+ return false;
+ }
+
+ const checker = parserServices.program.getTypeChecker();
+ const type = getTypeOfNode(node, context);
+ return ESLintUtils.isTypeReadonly(checker, type!);
+}
+
/**
* Get the es tree node from the given ts node.
*/
diff --git a/src/util/tree.ts b/src/util/tree.ts
index 88cf73dae..a4ccb26fa 100644
--- a/src/util/tree.ts
+++ b/src/util/tree.ts
@@ -9,7 +9,10 @@ import {
isMemberExpression,
isMethodDefinition,
isProperty,
+ isTSIndexSignature,
isTSInterfaceBody,
+ isTSInterfaceDeclaration,
+ isTSTypeAliasDeclaration,
} from "./typeguard";
/**
@@ -39,6 +42,45 @@ export function inFunctionBody(node: TSESTree.Node): boolean {
);
}
+/**
+ * Get the type alias or interface that the given node is in.
+ */
+export function getTypeDeclaration(
+ node: TSESTree.Node
+): TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration | null {
+ if (isTSTypeAliasDeclaration(node) || isTSInterfaceDeclaration(node)) {
+ return node;
+ }
+
+ return (getAncestorOfType(
+ (n): n is TSESTree.Node =>
+ n.parent !== undefined &&
+ n.parent !== null &&
+ ((isTSTypeAliasDeclaration(n.parent) && n.parent.typeAnnotation === n) ||
+ (isTSInterfaceDeclaration(n.parent) && n.parent.body === n)),
+ node
+ )?.parent ?? null) as
+ | TSESTree.TSInterfaceDeclaration
+ | TSESTree.TSTypeAliasDeclaration
+ | null;
+}
+
+/**
+ * Get the parent Index Signature that the given node is in.
+ */
+export function getParentIndexSignature(
+ node: TSESTree.Node
+): TSESTree.TSIndexSignature | null {
+ return (getAncestorOfType(
+ (n): n is TSESTree.Node =>
+ n.parent !== undefined &&
+ n.parent !== null &&
+ isTSIndexSignature(n.parent) &&
+ n.parent.typeAnnotation === n,
+ node
+ )?.parent ?? null) as TSESTree.TSIndexSignature | null;
+}
+
/**
* Test if the given node is in a class.
*/
diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts
index 0b7ff9e0f..30f9d5671 100644
--- a/src/util/typeguard.ts
+++ b/src/util/typeguard.ts
@@ -204,12 +204,24 @@ export function isTSIndexSignature(
return node.type === AST_NODE_TYPES.TSIndexSignature;
}
+export function isTSInterfaceDeclaration(
+ node: TSESTree.Node
+): node is TSESTree.TSInterfaceDeclaration {
+ return node.type === AST_NODE_TYPES.TSInterfaceDeclaration;
+}
+
export function isTSInterfaceBody(
node: TSESTree.Node
): node is TSESTree.TSInterfaceBody {
return node.type === AST_NODE_TYPES.TSInterfaceBody;
}
+export function isTSTypeAliasDeclaration(
+ node: TSESTree.Node
+): node is TSESTree.TSTypeAliasDeclaration {
+ return node.type === AST_NODE_TYPES.TSTypeAliasDeclaration;
+}
+
export function isTSNullKeyword(
node: TSESTree.Node
): node is TSESTree.TSNullKeyword {
diff --git a/tests/rules/prefer-readonly-type-declaration/index.test.ts b/tests/rules/prefer-readonly-type-declaration/index.test.ts
new file mode 100644
index 000000000..2ea99503c
--- /dev/null
+++ b/tests/rules/prefer-readonly-type-declaration/index.test.ts
@@ -0,0 +1,6 @@
+import { name, rule } from "~/rules/prefer-readonly-type-declaration";
+import { testUsing } from "~/tests/helpers/testers";
+
+import tsTests from "./ts";
+
+testUsing.typescript(name, rule, tsTests);
diff --git a/tests/rules/prefer-readonly-type-declaration/ts/index.ts b/tests/rules/prefer-readonly-type-declaration/ts/index.ts
new file mode 100644
index 000000000..40a005f71
--- /dev/null
+++ b/tests/rules/prefer-readonly-type-declaration/ts/index.ts
@@ -0,0 +1,7 @@
+import invalid from "./invalid";
+import valid from "./valid";
+
+export default {
+ valid,
+ invalid,
+};
diff --git a/tests/rules/prefer-readonly-type-declaration/ts/invalid.ts b/tests/rules/prefer-readonly-type-declaration/ts/invalid.ts
new file mode 100644
index 000000000..e5be11791
--- /dev/null
+++ b/tests/rules/prefer-readonly-type-declaration/ts/invalid.ts
@@ -0,0 +1,1293 @@
+import dedent from "dedent";
+
+import type { InvalidTestCase } from "~/tests/helpers/util";
+
+const tests: ReadonlyArray = [
+ {
+ code: dedent`
+ function foo(...numbers: number[]) {
+ }`,
+ optionsSet: [[]],
+ output: dedent`
+ function foo(...numbers: readonly number[]) {
+ }`,
+ errors: [
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ function foo(...numbers: Array) {
+ }`,
+ optionsSet: [[]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray) {
+ }`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 26,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ function foo(numbers: Set) {
+ }`,
+ optionsSet: [[]],
+ output: dedent`
+ function foo(numbers: ReadonlySet) {
+ }`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 23,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ function foo(numbers: Map) {
+ }`,
+ optionsSet: [[]],
+ output: dedent`
+ function foo(numbers: ReadonlyMap) {
+ }`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ 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: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 11,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 17,
+ },
+ ],
+ },
+ // Should fail on Array type in index interface.
+ // https://github.com/typescript-eslint/typescript-eslint/issues/3714
+ // {
+ // code: dedent`
+ // interface Foo {
+ // readonly [key: string]: {
+ // readonly groups: Array
+ // }
+ // }`,
+ // optionsSet: [[]],
+ // output: dedent`
+ // interface Foo {
+ // readonly [key: string]: {
+ // readonly groups: ReadonlyArray
+ // }
+ // }`,
+ // errors: [
+ // {
+ // messageId: "aliasShouldBeReadonly",
+ // type: "Identifier",
+ // line: 1,
+ // column: 11,
+ // },
+ // {
+ // messageId: "typeShouldBeReadonly",
+ // 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(): Array {
+ interface Foo {
+ readonly bar: ReadonlyArray
+ }
+ }`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 13,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ 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 = (): Array => {
+ interface Foo {
+ readonly bar: ReadonlyArray
+ }
+ }`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 13,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 3,
+ column: 19,
+ },
+ ],
+ },
+ // Should fail on shorthand syntax Array type as return type.
+ {
+ code: dedent`
+ function foo(): number[] {
+ }`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(): readonly number[] {
+ }`,
+ errors: [
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ // Should fail on shorthand syntax Array type as return type.
+ {
+ code: `const foo = (): number[] => {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: `const foo = (): readonly number[] => {}`,
+ errors: [
+ {
+ messageId: "arrayShouldBeReadonly",
+ 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: "typeShouldBeReadonly",
+ 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: "tupleShouldBeReadonly",
+ 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: "tupleShouldBeReadonly",
+ type: "TSTupleType",
+ line: 1,
+ column: 21,
+ },
+ {
+ messageId: "tupleShouldBeReadonly",
+ 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: "tupleShouldBeReadonly",
+ 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: "tupleShouldBeReadonly",
+ 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: Array,
+ readonly baz: ReadonlyArray
+ } {
+ let foo: {
+ readonly bar: ReadonlyArray,
+ readonly baz: ReadonlyArray
+ } = {
+ bar: ["hello"],
+ baz: ["world"]
+ };
+ return foo;
+ }`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 3,
+ column: 19,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 11,
+ column: 19,
+ },
+ ],
+ },
+ // Should fail on Array type alias.
+ {
+ code: `type Foo = Array;`,
+ optionsSet: [[]],
+ output: `type Foo = ReadonlyArray;`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ 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: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 8,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ 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: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 8,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ 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: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 8,
+ },
+ {
+ messageId: "typeShouldBeReadonly",
+ 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: "typeShouldBeReadonly",
+ 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: "arrayShouldBeReadonly",
+ 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: "typeShouldBeReadonly",
+ 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: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ // 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: "propertyShouldBeReadonly",
+ type: "ClassProperty",
+ line: 2,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "ClassProperty",
+ line: 3,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "ClassProperty",
+ line: 4,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ 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: "propertyShouldBeReadonly",
+ type: "TSParameterProperty",
+ line: 3,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSParameterProperty",
+ line: 4,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ 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]: { prop: string }
+ }`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 11,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSIndexSignature",
+ line: 2,
+ column: 3,
+ },
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 4,
+ column: 11,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSIndexSignature",
+ line: 5,
+ column: 3,
+ },
+ ],
+ },
+ // Function Index Signatures.
+ {
+ code: dedent`
+ function bar(param: { [source: string]: string }): void {
+ return undefined;
+ }`,
+ optionsSet: [[]],
+ output: dedent`
+ function bar(param: { readonly [source: string]: string }): void {
+ return undefined;
+ }`,
+ errors: [
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSIndexSignature",
+ line: 1,
+ 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: "propertyShouldBeReadonly",
+ 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: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 2,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ 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: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 2,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 3,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 4,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 5,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSIndexSignature",
+ line: 6,
+ column: 3,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 8,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 9,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 10,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 11,
+ column: 5,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSIndexSignature",
+ line: 12,
+ column: 5,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ function foo(bar: { x: number }) {
+ };`,
+ optionsSet: [[{ allowLocalMutation: true }]],
+ output: dedent`
+ function foo(bar: { readonly x: number }) {
+ };`,
+ errors: [
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 1,
+ column: 21,
+ },
+ ],
+ },
+ // Mapped type without readonly.
+ {
+ code: dedent`
+ const func = (x: { [key in string]: number }) => {}`,
+ optionsSet: [[]],
+ output: dedent`
+ const func = (x: { readonly [key in string]: number }) => {}`,
+ errors: [
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSMappedType",
+ line: 1,
+ column: 18,
+ },
+ ],
+ },
+ // Flag non class fields.
+ {
+ code: dedent`
+ class Klass {
+ foo() {
+ let bar: {
+ foo: number;
+ };
+ }
+ }`,
+ optionsSet: [[{ ignoreClass: "fieldsOnly" }]],
+ output: dedent`
+ class Klass {
+ foo() {
+ let bar: {
+ readonly foo: number;
+ };
+ }
+ }`,
+ errors: [
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 4,
+ column: 7,
+ },
+ ],
+ },
+ // Computed properties.
+ {
+ code: dedent`
+ const propertyName = 'myProperty';
+ type Foo = {
+ [propertyName]: string;
+ };`,
+ optionsSet: [[]],
+ output: dedent`
+ const propertyName = 'myProperty';
+ type Foo = {
+ readonly [propertyName]: string;
+ };`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 2,
+ column: 6,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 3,
+ column: 3,
+ },
+ ],
+ },
+ // Don't allow mutable return type.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): Array {}
+ function bar(...numbers: readonly number[]): number[] {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray): ReadonlyArray {}
+ function bar(...numbers: readonly number[]): readonly number[] {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 50,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 46,
+ },
+ ],
+ },
+ // Don't allow mutable return type.
+ {
+ code: dedent`
+ const foo = function(...numbers: ReadonlyArray): Array {}
+ const bar = function(...numbers: readonly number[]): number[] {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ const foo = function(...numbers: ReadonlyArray): ReadonlyArray {}
+ const bar = function(...numbers: readonly number[]): readonly number[] {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 58,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 54,
+ },
+ ],
+ },
+ // Don't allow mutable return type.
+ {
+ code: dedent`
+ const foo = (...numbers: ReadonlyArray): Array => {}
+ const bar = (...numbers: readonly number[]): number[] => {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ const foo = (...numbers: ReadonlyArray): ReadonlyArray => {}
+ const bar = (...numbers: readonly number[]): readonly number[] => {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 50,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 46,
+ },
+ ],
+ },
+ // Don't allow mutable return type.
+ {
+ code: dedent`
+ class Foo {
+ foo(...numbers: ReadonlyArray): Array {
+ }
+ }
+ class Bar {
+ foo(...numbers: readonly number[]): number[] {
+ }
+ }`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ class Foo {
+ foo(...numbers: ReadonlyArray): ReadonlyArray {
+ }
+ }
+ class Bar {
+ foo(...numbers: readonly number[]): readonly number[] {
+ }
+ }`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 43,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 6,
+ column: 39,
+ },
+ ],
+ },
+ // Don't allow mutable return type with Type Arguments.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): Promise> {}
+ function foo(...numbers: ReadonlyArray): Promise {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray): Promise> {}
+ function foo(...numbers: ReadonlyArray): Promise {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 58,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 58,
+ },
+ ],
+ },
+ // Don't allow mutable return type with deep Type Arguments.
+ {
+ code: dedent`
+ type Foo = { readonly x: T; };
+ function foo(...numbers: ReadonlyArray): Promise>> {}
+ function foo(...numbers: ReadonlyArray): Promise> {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ type Foo = { readonly x: T; };
+ function foo(...numbers: ReadonlyArray): Promise>> {}
+ function foo(...numbers: ReadonlyArray): Promise> {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 62,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 3,
+ column: 62,
+ },
+ ],
+ },
+ // Don't allow mutable 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: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray): readonly [number, ReadonlyArray, number] {}
+ function foo(...numbers: ReadonlyArray): readonly [number, readonly number[], number] {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 68,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 68,
+ },
+ ],
+ },
+ // Don't allow mutable return type with Type Arguments Union.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: Array } | { readonly b: string[] } {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: ReadonlyArray } | { readonly b: readonly string[] } {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 64,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 96,
+ },
+ ],
+ },
+ // Don't allow mutable return type with Type Arguments Intersection.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: Array } & { readonly b: string[] } {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: ReadonlyArray } & { readonly b: readonly string[] } {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 64,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 96,
+ },
+ ],
+ },
+ // Don't allow mutable return type with Type Arguments Conditional.
+ {
+ code: dedent`
+ function foo(x: T): T extends Array ? string : number[] {}`,
+ optionsSet: [[{ allowMutableReturnType: false }]],
+ output: dedent`
+ function foo(x: T): T extends ReadonlyArray ? string : readonly number[] {}`,
+ errors: [
+ {
+ messageId: "typeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 34,
+ },
+ {
+ messageId: "arrayShouldBeReadonly",
+ type: "TSArrayType",
+ line: 1,
+ column: 59,
+ },
+ ],
+ },
+ // Readonly types should not be mutable.
+ {
+ code: dedent`
+ type MyType = {
+ a: string;
+ };`,
+ optionsSet: [[]],
+ output: dedent`
+ type MyType = {
+ readonly a: string;
+ };`,
+ errors: [
+ {
+ messageId: "aliasShouldBeReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ {
+ messageId: "propertyShouldBeReadonly",
+ type: "TSPropertySignature",
+ line: 2,
+ column: 3,
+ },
+ ],
+ },
+ // Mutable types should not be readonly.
+ {
+ code: dedent`
+ type MyType = {
+ readonly a: string;
+ };`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ requireOthersToBeMutable: true,
+ },
+ mustBeMutable: {
+ requireOthersToBeReadonly: false,
+ },
+ },
+ },
+ ],
+ ],
+ errors: [
+ {
+ messageId: "aliasShouldBeMutable",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ ],
+ },
+ // Mutable types should not be readonly.
+ {
+ code: dedent`
+ type MutableMyType = {
+ readonly a: string;
+ };`,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "aliasShouldBeMutable",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ ],
+ },
+ // Needs Explicit Marking.
+ {
+ code: dedent`
+ type MyType = {};`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ requireOthersToBeMutable: true,
+ },
+ mustBeMutable: {
+ requireOthersToBeReadonly: true,
+ },
+ },
+ },
+ ],
+ ],
+ errors: [
+ {
+ messageId: "aliasNeedsExplicitMarking",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ ],
+ },
+ // Both Mutable and Readonly error.
+ {
+ code: dedent`
+ type MyType = {};`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ pattern: ".*",
+ },
+ mustBeMutable: {
+ pattern: ".*",
+ },
+ },
+ },
+ ],
+ ],
+ errors: [
+ {
+ messageId: "aliasConfigErrorMutableReadonly",
+ type: "Identifier",
+ line: 1,
+ column: 6,
+ },
+ ],
+ },
+];
+
+export default tests;
diff --git a/tests/rules/prefer-readonly-type-declaration/ts/valid.ts b/tests/rules/prefer-readonly-type-declaration/ts/valid.ts
new file mode 100644
index 000000000..814ebea47
--- /dev/null
+++ b/tests/rules/prefer-readonly-type-declaration/ts/valid.ts
@@ -0,0 +1,459 @@
+import dedent from "dedent";
+
+import type { ValidTestCase } from "~/tests/helpers/util";
+
+const tests: 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: [[]],
+ },
+ // Allow mutable return type.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): Array {}
+ function bar(...numbers: readonly number[]): number[] {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type.
+ {
+ code: dedent`
+ const foo = function(...numbers: ReadonlyArray): Array {}
+ const bar = function(...numbers: readonly number[]): number[] {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type.
+ {
+ code: dedent`
+ const foo = (...numbers: ReadonlyArray): Array => {}
+ const bar = (...numbers: readonly number[]): number[] => {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type.
+ {
+ code: dedent`
+ class Foo {
+ foo(...numbers: ReadonlyArray): Array {
+ }
+ }
+ class Bar {
+ foo(...numbers: readonly number[]): number[] {
+ }
+ }`,
+ optionsSet: [],
+ },
+ // Allow mutable return type with Type Arguments.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): Promise> {}
+ function foo(...numbers: ReadonlyArray): Promise {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type with deep Type Arguments.
+ {
+ code: dedent`
+ type Foo = { readonly x: T; };
+ function foo(...numbers: ReadonlyArray): Promise>> {}
+ function foo(...numbers: ReadonlyArray): Promise> {}`,
+ optionsSet: [],
+ },
+ // Allow mutable 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: [],
+ },
+ // Allow mutable return type with Type Arguments Union.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: Array } | { readonly b: string[] } {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type with Type Arguments Intersection.
+ {
+ code: dedent`
+ function foo(...numbers: ReadonlyArray): { readonly a: Array } & { readonly b: string[] } {}`,
+ optionsSet: [],
+ },
+ // Allow mutable return type with Type Arguments Conditional.
+ {
+ code: dedent`
+ function foo(x: T): T extends Array ? string : number[] {}`,
+ optionsSet: [],
+ },
+ // Allow inline mutable return type.
+ {
+ code: dedent`
+ function foo(bar: string): { baz: number } {
+ return 1 as any;
+ }`,
+ optionsSet: [[{ allowMutableReturnType: 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.
+ // Waiting on https://github.com/typescript-eslint/typescript-eslint/issues/1758
+ // {
+ // code: dedent`
+ // interface Foo {
+ // (): void
+ // foo(): void
+ // }`,
+ // optionsSet: [
+ // [
+ // {
+ // treatMethodsAsReadonly: true,
+ // },
+ // ],
+ // ],
+ // },
+ // 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: [[]],
+ },
+ // Mapped types with readonly on members should not produce failures.
+ {
+ code: dedent`
+ const func = (x: { readonly [key in string]: number }) => {}`,
+ 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 }]],
+ },
+ // Allow 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: [[{ allowLocalMutation: true }]],
+ },
+ // Ignore Prefix.
+ {
+ code: dedent`
+ let mutableFoo: string[] = [];`,
+ optionsSet: [[{ ignorePattern: "^mutable" }]],
+ },
+ {
+ 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" }]],
+ },
+ {
+ code: dedent`
+ class Klass {
+ mutableA: string;
+ private mutableB: string;
+ }`,
+ optionsSet: [[{ ignorePattern: "^mutable" }]],
+ },
+ // Ignore Suffix.
+ {
+ code: dedent`
+ let fooMutable: string[] = [];`,
+ optionsSet: [[{ ignorePattern: "Mutable$" }]],
+ },
+ {
+ 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$" }]],
+ },
+ {
+ code: dedent`
+ class Klass {
+ AMutable: string;
+ private BMutable: string;
+ }`,
+ optionsSet: [[{ ignorePattern: "Mutable$" }]],
+ },
+ // Allow mutable TSIndexSignature.
+ {
+ code: dedent`
+ const mutableResult: {
+ [key: string]: string
+ } = {};`,
+ optionsSet: [[{ ignorePattern: "^mutable" }]],
+ },
+ // Ignore Mutable Collections (Array, Tuple, Set, Map)
+ {
+ code: dedent`const Foo: number[] = [];`,
+ optionsSet: [[{ ignoreCollections: true }]],
+ },
+ {
+ code: dedent`const Foo: [string, string] = ['foo', 'bar'];`,
+ optionsSet: [[{ ignoreCollections: true }]],
+ },
+ {
+ code: dedent`const Foo: Set = new Set();`,
+ optionsSet: [[{ ignoreCollections: true }]],
+ },
+ {
+ code: dedent`const Foo: Map = new Map();`,
+ optionsSet: [[{ ignoreCollections: true }]],
+ },
+ // Readonly types should be readonly.
+ {
+ code: dedent`
+ type MyType = {
+ readonly a: string;
+ };`,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ type ReadonlyMyType = {
+ readonly a: string;
+ };`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ requireOthersToBeMutable: true,
+ },
+ mustBeMutable: {
+ requireOthersToBeReadonly: false,
+ },
+ },
+ },
+ ],
+ ],
+ },
+ // Readonly types should be readonly and mutable types mutable.
+ {
+ code: dedent`
+ type MutableMyType = {
+ a: string;
+ };
+ type MyType = Readonly;`,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ type MyType = {
+ a: string;
+ };
+ type ReadonlyMyType = Readonly;`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ requireOthersToBeMutable: true,
+ },
+ mustBeMutable: {
+ requireOthersToBeReadonly: false,
+ },
+ },
+ },
+ ],
+ ],
+ },
+ // Readonly types should be readonly and mutable types mutable.
+ {
+ code: dedent`
+ type Mutable = { -readonly[P in keyof T]: T[P] };
+ type MyType = {
+ readonly a: string;
+ };
+ type MutableMyType = Mutable;`,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ type Mutable = { -readonly[P in keyof T]: T[P] };
+ type ReadonlyMyType = {
+ readonly a: string;
+ };
+ type MyType = Mutable;`,
+ optionsSet: [
+ [
+ {
+ aliases: {
+ mustBeReadonly: {
+ requireOthersToBeMutable: true,
+ },
+ mustBeMutable: {
+ requireOthersToBeReadonly: false,
+ },
+ },
+ },
+ ],
+ ],
+ },
+];
+
+export default tests;