diff --git a/.eslintrc.json b/.eslintrc.json
index 799a4a33f..8936a30d2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -90,6 +90,13 @@
"jsdoc/require-jsdoc": "off"
}
},
+ {
+ "files": ["**/*.md/**"],
+ "rules": {
+ "@typescript-eslint/array-type": "off",
+ "functional/no-mixed-type": "off"
+ }
+ },
// FIXME: This override is defined in the upsteam; it shouldn't need to be redefined here. Why?
{
"files": ["./**/*.md/**"],
diff --git a/README.md b/README.md
index 8ee950803..a8bb0cfa1 100644
--- a/README.md
+++ b/README.md
@@ -186,12 +186,13 @@ 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`](./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: |
+| [`prefer-readonly-return-types`](./docs/rules/prefer-readonly-return-types.md) | Enforce use of readonly types for function return values | | | | | :thought_balloon: |
### No Object-Orientation Rules
diff --git a/docs/rules/prefer-readonly-return-types.md b/docs/rules/prefer-readonly-return-types.md
new file mode 100644
index 000000000..939c5ca4e
--- /dev/null
+++ b/docs/rules/prefer-readonly-return-types.md
@@ -0,0 +1,268 @@
+# Requires that function return values are typed as readonly (prefer-readonly-return-types)
+
+This rules work just like [prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md).
+
+This rule should only be used in a purely functional environment to help ensure that values are not being mutated.
+
+## Rule Details
+
+This rule allows you to enforce that function return values resolve to a readonly type.
+
+Examples of **incorrect** code for this rule:
+
+
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: "error" */
+
+function array1(): string[] {} // array is not readonly
+function array2(): readonly string[][] {} // array element is not readonly
+function array3(): [string, number] {} // tuple is not readonly
+function array4(): readonly [string[], number] {} // tuple element is not readonly
+// the above examples work the same if you use ReadonlyArray instead
+
+function object1(): { prop: string } {} // property is not readonly
+function object2(): { readonly prop: string; prop2: string } {} // not all properties are readonly
+function object3(): { readonly prop: { prop2: string } } {} // nested property is not readonly
+// the above examples work the same if you use Readonly instead
+
+interface CustomArrayType extends ReadonlyArray {
+ prop: string; // note: this property is mutable
+}
+function custom1(): CustomArrayType {}
+
+interface CustomFunction {
+ (): void;
+ prop: string; // note: this property is mutable
+}
+function custom2(): CustomFunction {}
+
+function union(): string[] | ReadonlyArray {} // not all types are readonly
+
+// rule also checks function types
+interface Foo {
+ (): string[];
+}
+interface Foo {
+ new (): string[];
+}
+const x = { foo(): string[]; };
+function foo(): string[];
+type Foo = () => string[];
+interface Foo {
+ foo(): string[];
+}
+```
+
+Examples of **correct** code for this rule:
+
+```ts
+/* eslint functional/prefer-readonly-return-types: "error" */
+
+function array1(): readonly string[] {}
+function array2(): readonly (readonly string[])[] {}
+function array3(): readonly [string, number] {}
+function array4(): readonly [readonly string[], number] {}
+// the above examples work the same if you use ReadonlyArray instead
+
+function object1(): { readonly prop: string } {}
+function object2(): { readonly prop: string; readonly prop2: string } {}
+function object3(): { readonly prop: { readonly prop2: string } } {}
+// the above examples work the same if you use Readonly instead
+
+interface CustomArrayType extends ReadonlyArray {
+ readonly prop: string;
+}
+function custom1(): Readonly {}
+// interfaces that extend the array types are not considered arrays, and thus must be made readonly.
+
+interface CustomFunction {
+ (): void;
+ readonly prop: string;
+}
+function custom2(): CustomFunction {}
+
+function union(): readonly string[] | ReadonlyArray {}
+
+function primitive1(): string {}
+function primitive2(): number {}
+function primitive3(): boolean {}
+function primitive4(): unknown {}
+function primitive5(): null {}
+function primitive6(): undefined {}
+function primitive7(): any {}
+function primitive8(): never {}
+function primitive9(): number | string | undefined {}
+
+function fnSig(): () => void {}
+
+enum Foo { A, B }
+function enum1(): Foo {}
+
+function symb1(): symbol {}
+const customSymbol = Symbol('a');
+function symb2(): typeof customSymbol {}
+
+// function types
+interface Foo {
+ (): readonly string[];
+}
+interface Foo {
+ new (): readonly string[];
+}
+const x = { foo(): readonly string[]; };
+function foo(): readonly string[];
+type Foo = () => readonly string[];
+interface Foo {
+ foo(): readonly string[];
+}
+```
+
+The default options:
+
+```ts
+const defaults = {
+ allowLocalMutation: false,
+ ignoreClass: false,
+ ignoreCollections: false,
+ ignoreInferredTypes: false,
+ ignoreInterface: false,
+ treatMethodsAsReadonly: false,
+}
+```
+
+### `treatMethodsAsReadonly`
+
+This option allows you to treat all mutable methods as though they were readonly. This may be desirable in when you are never reassigning methods.
+
+Examples of **incorrect** code for this rule with `{treatMethodsAsReadonly: false}`:
+
+```ts
+type MyType = {
+ readonly prop: string;
+ method(): string; // note: this method is mutable
+};
+function foo(arg: MyType) {}
+```
+
+Examples of **correct** code for this rule with `{treatMethodsAsReadonly: false}`:
+
+```ts
+type MyType = Readonly<{
+ prop: string;
+ method(): string;
+}>;
+function foo(): MyType {}
+type MyOtherType = {
+ readonly prop: string;
+ readonly method: () => string;
+};
+function bar(): MyOtherType {}
+```
+
+Examples of **correct** code for this rule with `{treatMethodsAsReadonly: true}`:
+
+```ts
+type MyType = {
+ readonly prop: string;
+ method(): string; // note: this method is mutable
+};
+function foo(): MyType {}
+```
+
+### `ignoreClass`
+
+If set, classes will not be checked.
+
+Examples of **incorrect** code for the `{ "ignoreClass": false }` option:
+
+
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreClass": false }] */
+
+class {
+ myprop: string;
+}
+```
+
+Examples of **correct** code for the `{ "ignoreClass": true }` option:
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: ["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/prefer-readonly-type-declaration: ["error", { "ignoreInterface": false }] */
+
+interface I {
+ myprop: string;
+}
+```
+
+Examples of **correct** code for the `{ "ignoreInterface": true }` option:
+
+```ts
+/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreInterface": true }] */
+
+interface I {
+ 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/prefer-readonly-type-declaration: ["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/prefer-readonly-type-declaration: ["error", { "ignoreCollections": true }] */
+
+const foo: number[] = [];
+const bar: [string, string] = ["foo", "bar"];
+const baz: Set = new Set();
+const qux: Map = new Map();
+```
+
+### `ignoreInferredTypes`
+
+This option allows you to ignore types that aren't explicitly specified.
+
+### `allowLocalMutation`
+
+See the [allowLocalMutation](./options/allow-local-mutation.md) docs.
+
+### `ignorePattern`
+
+Use the given regex pattern(s) to match against the type's name (for objects this is the property's name not the object's name).
+
+Note: If using this option to require mutable properties are marked as mutable via a naming convention (e.g. `{ "ignorePattern": "^[Mm]utable.+" }`),
+type aliases and interfaces names will still need to comply with the `readonlyAliasPatterns` and `mutableAliasPatterns` options.
+
+See the [ignorePattern](./options/ignore-pattern.md) docs for more info.
diff --git a/package.json b/package.json
index 4b97a96a7..ddfd9e648 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
},
"dependencies": {
"@typescript-eslint/utils": "^5.10.0",
+ "@typescript-eslint/type-utils": "^5.10.0",
"deepmerge-ts": "^2.0.1",
"escape-string-regexp": "^4.0.0"
},
diff --git a/src/common/ignore-options.ts b/src/common/ignore-options.ts
index 2759d7205..97a6db9d8 100644
--- a/src/common/ignore-options.ts
+++ b/src/common/ignore-options.ts
@@ -125,6 +125,22 @@ export const ignoreInterfaceOptionSchema: JSONSchema4["properties"] = {
},
};
+/**
+ * The option to ignore inferred types.
+ */
+export type IgnoreInferredTypesOption = {
+ readonly ignoreInferredTypes: boolean;
+};
+
+/**
+ * The schema for the option to ignore inferred types.
+ */
+export const ignoreInferredTypesOptionSchema: JSONSchema4["properties"] = {
+ ignoreInferredTypes: {
+ type: "boolean",
+ },
+};
+
/**
* Get the identifier text of the given node.
*/
@@ -322,6 +338,19 @@ export function shouldIgnoreInterface(
return ignoreInterface === true && inInterface(node);
}
+/**
+ * Should the given node be allowed base off the following rule options?
+ *
+ * - IgnoreInterfaceOption.
+ */
+export function shouldIgnoreInferredTypes(
+ node: ReadonlyDeep,
+ context: ReadonlyDeep>,
+ { ignoreInferredTypes }: Partial
+) {
+ return ignoreInferredTypes === true && node === null;
+}
+
/**
* Should the given node be allowed base off the following rule options?
*
diff --git a/src/configs/all.ts b/src/configs/all.ts
index 0db73d9d0..952b37034 100644
--- a/src/configs/all.ts
+++ b/src/configs/all.ts
@@ -21,6 +21,7 @@ const config: Linter.Config = {
rules: {
"functional/no-method-signature": "error",
"functional/no-mixed-type": "error",
+ "functional/prefer-readonly-return-types": "error",
"functional/prefer-readonly-type": "error",
"functional/prefer-tacit": ["error", { assumeTypes: false }],
"functional/no-return-void": "error",
diff --git a/src/rules/index.ts b/src/rules/index.ts
index 06eb7baef..ea40af84d 100644
--- a/src/rules/index.ts
+++ b/src/rules/index.ts
@@ -12,6 +12,7 @@ import * as noReturnVoid from "./no-return-void";
import * as noThisExpression from "./no-this-expression";
import * as noThrowStatement from "./no-throw-statement";
import * as noTryStatement from "./no-try-statement";
+import * as preferReadonlyReturnTypes from "./prefer-readonly-return-types";
import * as preferReadonlyTypes from "./prefer-readonly-type";
import * as preferTacit from "./prefer-tacit";
@@ -33,6 +34,7 @@ export const rules = {
[noThisExpression.name]: noThisExpression.rule,
[noThrowStatement.name]: noThrowStatement.rule,
[noTryStatement.name]: noTryStatement.rule,
+ [preferReadonlyReturnTypes.name]: preferReadonlyReturnTypes.rule,
[preferReadonlyTypes.name]: preferReadonlyTypes.rule,
[preferTacit.name]: preferTacit.rule,
};
diff --git a/src/rules/prefer-readonly-return-types.ts b/src/rules/prefer-readonly-return-types.ts
new file mode 100644
index 000000000..d80717862
--- /dev/null
+++ b/src/rules/prefer-readonly-return-types.ts
@@ -0,0 +1,158 @@
+import type { ReadonlynessOptions } from "@typescript-eslint/type-utils";
+import {
+ readonlynessOptionsDefaults,
+ readonlynessOptionsSchema,
+} from "@typescript-eslint/type-utils";
+import type { ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils";
+import { deepmerge } from "deepmerge-ts";
+import type { JSONSchema4 } from "json-schema";
+import type { ReadonlyDeep } from "type-fest";
+
+import type {
+ AllowLocalMutationOption,
+ IgnoreClassOption,
+ IgnoreInferredTypesOption,
+ IgnoreInterfaceOption,
+ IgnorePatternOption,
+} from "~/common/ignore-options";
+import {
+ allowLocalMutationOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInferredTypesOptionSchema,
+ ignoreInterfaceOptionSchema,
+ ignorePatternOptionSchema,
+ shouldIgnoreClass,
+ shouldIgnoreInferredTypes,
+ shouldIgnoreInterface,
+ shouldIgnoreLocalMutation,
+ shouldIgnorePattern,
+} from "~/common/ignore-options";
+import type { RuleResult } from "~/util/rule";
+import { isReadonly, createRule } from "~/util/rule";
+import { isFunctionLike, isTSFunctionType } from "~/util/typeguard";
+
+/**
+ * The name of this rule.
+ */
+export const name = "prefer-readonly-return-types" as const;
+
+/**
+ * The options this rule can take.
+ */
+type Options = readonly [
+ AllowLocalMutationOption &
+ IgnoreClassOption &
+ IgnoreInferredTypesOption &
+ IgnoreInterfaceOption &
+ IgnorePatternOption &
+ ReadonlynessOptions
+];
+
+/**
+ * The schema for the rule options.
+ */
+const schema: JSONSchema4 = [
+ {
+ type: "object",
+ properties: deepmerge(
+ allowLocalMutationOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInferredTypesOptionSchema,
+ ignoreInterfaceOptionSchema,
+ ignorePatternOptionSchema,
+ readonlynessOptionsSchema.properties
+ ),
+ additionalProperties: false,
+ },
+];
+
+/**
+ * The default options for the rule.
+ */
+const defaultOptions: Options = [
+ {
+ ignoreClass: false,
+ ignoreInterface: false,
+ allowLocalMutation: false,
+ ignoreInferredTypes: false,
+ ...readonlynessOptionsDefaults,
+ },
+];
+
+/**
+ * The possible error messages.
+ */
+const errorMessages = {
+ returnTypeShouldBeReadonly: "Return type should be readonly.",
+} as const;
+
+/**
+ * The meta data for this rule.
+ */
+const meta: ESLintUtils.NamedCreateRuleMeta = {
+ type: "suggestion",
+ docs: {
+ description: "Prefer readonly return types over mutable one.",
+ recommended: "error",
+ },
+ messages: errorMessages,
+ fixable: "code",
+ schema,
+};
+
+/**
+ * Check if the given TypeAnnotation violates this rule.
+ */
+function checkTypeAnnotation(
+ node: ReadonlyDeep,
+ context: ReadonlyDeep<
+ TSESLint.RuleContext
+ >,
+ [optionsObject]: Options
+): RuleResult {
+ if (
+ !isReturnType(node) ||
+ shouldIgnoreInferredTypes(node.typeAnnotation, context, optionsObject) ||
+ shouldIgnoreClass(node.typeAnnotation, context, optionsObject) ||
+ shouldIgnoreInterface(node.typeAnnotation, context, optionsObject) ||
+ shouldIgnoreLocalMutation(node.typeAnnotation, context, optionsObject) ||
+ shouldIgnorePattern(node.typeAnnotation, context, optionsObject) ||
+ isReadonly(node.typeAnnotation, context, optionsObject)
+ ) {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.typeAnnotation,
+ messageId: "returnTypeShouldBeReadonly",
+ },
+ ],
+ };
+}
+
+/**
+ * Is the given node a return type?
+ */
+function isReturnType(node: ReadonlyDeep) {
+ return (
+ node.parent !== undefined &&
+ (isFunctionLike(node.parent) || isTSFunctionType(node.parent)) &&
+ node.parent.returnType === node
+ );
+}
+
+// Create the rule.
+export const rule = createRule(
+ name,
+ meta,
+ defaultOptions,
+ {
+ TSTypeAnnotation: checkTypeAnnotation,
+ }
+);
diff --git a/src/util/rule.ts b/src/util/rule.ts
index 6042ef035..621109fd1 100644
--- a/src/util/rule.ts
+++ b/src/util/rule.ts
@@ -1,3 +1,5 @@
+import type { ReadonlynessOptions } from "@typescript-eslint/type-utils";
+import { isTypeReadonly } from "@typescript-eslint/type-utils";
import type { TSESLint, TSESTree } from "@typescript-eslint/utils";
import { ESLintUtils } from "@typescript-eslint/utils";
import type { Rule } from "eslint";
@@ -132,6 +134,27 @@ export function getTypeOfNode<
return constrained ?? nodeType;
}
+/**
+ * Is the given node readonly?
+ */
+export function isReadonly<
+ Context extends ReadonlyDeep>
+>(
+ node: ReadonlyDeep,
+ context: Context,
+ readonlynessOptions: ReadonlyDeep
+): boolean {
+ const { parserServices } = context;
+
+ if (parserServices === undefined || parserServices.program === undefined) {
+ return false;
+ }
+
+ const checker = parserServices.program.getTypeChecker();
+ const type = getTypeOfNode(node, context);
+ return isTypeReadonly(checker, type!, readonlynessOptions);
+}
+
/**
* Get the es tree node from the given ts node.
*/
diff --git a/tests/rules/prefer-readonly-return-types/index.test.ts b/tests/rules/prefer-readonly-return-types/index.test.ts
new file mode 100644
index 000000000..ae02f22c3
--- /dev/null
+++ b/tests/rules/prefer-readonly-return-types/index.test.ts
@@ -0,0 +1,6 @@
+import { name, rule } from "~/rules/prefer-readonly-return-types";
+import { testUsing } from "~/tests/helpers/testers";
+
+import tsTests from "./ts";
+
+testUsing.typescript(name, rule, tsTests);
diff --git a/tests/rules/prefer-readonly-return-types/ts/index.ts b/tests/rules/prefer-readonly-return-types/ts/index.ts
new file mode 100644
index 000000000..40a005f71
--- /dev/null
+++ b/tests/rules/prefer-readonly-return-types/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-return-types/ts/invalid.ts b/tests/rules/prefer-readonly-return-types/ts/invalid.ts
new file mode 100644
index 000000000..61455aa1b
--- /dev/null
+++ b/tests/rules/prefer-readonly-return-types/ts/invalid.ts
@@ -0,0 +1,282 @@
+import dedent from "dedent";
+
+import type { InvalidTestCase } from "~/tests/helpers/util";
+
+const tests: ReadonlyArray = [
+ // Don't allow mutable return type.
+ {
+ code: dedent`
+ function foo(): Array {}
+ function bar(): number[] {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 17,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 17,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ const foo = function(): Array {}
+ const bar = function(): number[] {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 25,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 25,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ const foo = (): Array => {}
+ const bar = (): number[] => {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 17,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 17,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ class Foo {
+ foo(): Array {}
+ }
+ class Bar {
+ bar(): number[] {}
+ }
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 10,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 5,
+ column: 10,
+ },
+ ],
+ },
+ // Interface with functions with mutable return types should fail.
+ {
+ code: dedent`
+ interface Foo {
+ a: () => string[],
+ }
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 12,
+ },
+ ],
+ },
+ // Type aliases with functions with mutable return types should fail.
+ {
+ code: dedent`
+ type Foo = {
+ a: () => string[],
+ };
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSArrayType",
+ line: 2,
+ column: 12,
+ },
+ ],
+ },
+ // Don't allow mutable return type with Type Arguments.
+ {
+ code: dedent`
+ function foo(): Promise> {}
+ function foo(): Promise {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 1,
+ column: 17,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 17,
+ },
+ ],
+ },
+ // Don't allow mutable return type with deep Type Arguments.
+ {
+ code: dedent`
+ type Foo = { readonly x: T; };
+ function foo(): Promise>> {}
+ function bar(): Promise> {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 2,
+ column: 17,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeReference",
+ line: 3,
+ column: 17,
+ },
+ ],
+ },
+ // Don't allow mutable Type Arguments in a tuple return type.
+ {
+ code: dedent`
+ function foo(): readonly [number, Array, number] {}
+ function bar(): readonly [number, number[], number] {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeOperator",
+ line: 1,
+ column: 17,
+ },
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeOperator",
+ line: 2,
+ column: 17,
+ },
+ ],
+ },
+ // Don't allow mutable Union Type return type.
+ {
+ code: dedent`
+ function foo(): { readonly a: Array } | { readonly b: string[] } {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSUnionType",
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ // Don't allow mutable Intersection Type return type.
+ {
+ code: dedent`
+ function foo(): { readonly a: Array } & { readonly b: string[] } {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSIntersectionType",
+ line: 1,
+ column: 17,
+ },
+ ],
+ },
+ // Don't allow mutable Conditional Type return type.
+ {
+ code: dedent`
+ function foo(): T extends Array ? string : number[] {}
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSConditionalType",
+ line: 1,
+ column: 20,
+ },
+ ],
+ },
+ // Mutable method signature should fail.
+ {
+ code: dedent`
+ type Foo = {
+ a: () => {
+ methodSignature(): string;
+ },
+ }
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSTypeLiteral",
+ line: 2,
+ column: 12,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ type Foo = {
+ a: () => {
+ methodSignature1(): string;
+ } | {
+ methodSignature2(): number;
+ },
+ };
+ `,
+ optionsSet: [[]],
+ errors: [
+ {
+ messageId: "returnTypeShouldBeReadonly",
+ type: "TSUnionType",
+ line: 2,
+ column: 12,
+ },
+ ],
+ },
+];
+
+export default tests;
diff --git a/tests/rules/prefer-readonly-return-types/ts/valid.ts b/tests/rules/prefer-readonly-return-types/ts/valid.ts
new file mode 100644
index 000000000..9b6a44914
--- /dev/null
+++ b/tests/rules/prefer-readonly-return-types/ts/valid.ts
@@ -0,0 +1,153 @@
+import dedent from "dedent";
+
+import type { ValidTestCase } from "~/tests/helpers/util";
+
+const tests: ReadonlyArray = [
+ // Should not fail on shorthand syntax readonly array type as return type.
+ {
+ code: dedent`
+ function foo(): readonly number[] {}
+ `,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ const foo = (): readonly number[] => {}
+ `,
+ optionsSet: [[]],
+ },
+ // Should not fail on longhand syntax readonly array type as return type.
+ {
+ code: dedent`
+ function foo(): ReadonlyArray {}
+ `,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ const foo = (): ReadonlyArray => {}
+ `,
+ optionsSet: [[]],
+ },
+ // Allow inline immutable return type.
+ {
+ code: dedent`
+ function foo(bar: string): { readonly baz: number } {}
+ `,
+ optionsSet: [[]],
+ },
+ // Interfaces with functions with immutable return types should not produce failures.
+ {
+ code: dedent`
+ interface Foo {
+ a: () => readonly string[],
+ }
+ `,
+ optionsSet: [[]],
+ },
+ // Type aliases with functions with immutable return types should not produce failures.
+ {
+ code: dedent`
+ type Foo = {
+ a: () => readonly string[],
+ };
+ `,
+ optionsSet: [[]],
+ },
+ // Ignore Classes.
+ {
+ code: dedent`
+ class Klass {
+ a(): string[] {}
+ }
+ `,
+ optionsSet: [[{ ignoreClass: true }]],
+ },
+ // Ignore Interfaces.
+ {
+ code: dedent`
+ interface Foo {
+ a: () => string[],
+ }
+ `,
+ optionsSet: [[{ ignoreInterface: true }]],
+ },
+ // TODO: Allow Local.
+ // TODO: Ignore Prefix.
+ // TODO: Ignore Suffix.
+ // Readonly method signature.
+ {
+ code: dedent`
+ type Foo = {
+ a: () => Readonly<{
+ methodSignature(): string;
+ }>,
+ }
+ `,
+ optionsSet: [[]],
+ },
+ {
+ code: dedent`
+ type Foo = {
+ a: () => Readonly<{
+ methodSignature1(): string;
+ }> | Readonly<{
+ methodSignature2(): number;
+ }>,
+ };
+ `,
+ optionsSet: [[]],
+ },
+ // Mutable method signature treated as readonly.
+ {
+ code: dedent`
+ type Foo = {
+ a: () => {
+ methodSignature(): string;
+ },
+ }
+ `,
+ optionsSet: [
+ [
+ {
+ treatMethodsAsReadonly: true,
+ },
+ ],
+ ],
+ },
+ {
+ code: dedent`
+ type Foo = {
+ a: () => {
+ methodSignature1(): string;
+ } | {
+ methodSignature2(): number;
+ },
+ };
+ `,
+ optionsSet: [
+ [
+ {
+ treatMethodsAsReadonly: true,
+ },
+ ],
+ ],
+ },
+ // Ignore inferred types.
+ {
+ code: dedent`
+ function foo() {
+ return [1, 2, 3]
+ }
+ `,
+ optionsSet: [
+ [
+ {
+ ignoreInferredTypes: true,
+ },
+ ],
+ ],
+ },
+];
+
+export default tests;
diff --git a/yarn.lock b/yarn.lock
index 4ee741e6d..a940e4db0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1541,7 +1541,7 @@
"@typescript-eslint/types" "5.10.0"
"@typescript-eslint/visitor-keys" "5.10.0"
-"@typescript-eslint/type-utils@5.10.0":
+"@typescript-eslint/type-utils@5.10.0", "@typescript-eslint/type-utils@^5.10.0":
version "5.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz#8524b9479c19c478347a7df216827e749e4a51e5"
integrity sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ==