diff --git a/.eslintrc.json b/.eslintrc.json
index 12616a7d2..eeb9f087a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -59,6 +59,13 @@
"extends": ["plugin:eslint-plugin/recommended"],
"rules": {}
},
+ {
+ "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 d11d0a496..b89e56ad3 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 180138dd2..a4989affe 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,8 @@
"verify": "yarn build && yarn lint && yarn build-tests && yarn test-compiled && rimraf build"
},
"dependencies": {
- "@typescript-eslint/experimental-utils": "^5.0.0",
+ "@typescript-eslint/experimental-utils": "^5.9.1",
+ "@typescript-eslint/type-utils": "^5.9.1",
"deepmerge-ts": "^2.0.1",
"escape-string-regexp": "^4.0.0"
},
@@ -87,8 +88,8 @@
"@types/estree": "^0.0.50",
"@types/node": "16.11.0",
"@types/rollup-plugin-auto-external": "^2.0.2",
- "@typescript-eslint/eslint-plugin": "^5.0.0",
- "@typescript-eslint/parser": "^5.0.0",
+ "@typescript-eslint/eslint-plugin": "^5.9.1",
+ "@typescript-eslint/parser": "^5.9.1",
"ava": "^3.15.0",
"babel-eslint": "^10.1.0",
"chalk": "^4.1.2",
diff --git a/src/common/ignore-options.ts b/src/common/ignore-options.ts
index 227091f30..4c31a4699 100644
--- a/src/common/ignore-options.ts
+++ b/src/common/ignore-options.ts
@@ -114,6 +114,20 @@ export const ignoreInterfaceOptionSchema: JSONSchema4 = {
additionalProperties: false,
};
+export type IgnoreInferredTypesOption = {
+ readonly ignoreInferredTypes: boolean;
+};
+
+export const ignoreInferredTypesOptionOptionSchema: JSONSchema4 = {
+ type: "object",
+ properties: {
+ ignoreInferredTypes: {
+ type: "boolean",
+ },
+ },
+ additionalProperties: false,
+};
+
/**
* Get the identifier text of the given node.
*/
@@ -311,6 +325,19 @@ export function shouldIgnoreInterface(
return options.ignoreInterface === true && inInterface(node);
}
+/**
+ * Should the given node be allowed base off the following rule options?
+ *
+ * - IgnoreInterfaceOption.
+ */
+export function shouldIgnoreInferredTypes(
+ node: TSESTree.TypeNode,
+ context: RuleContext,
+ options: Partial
+) {
+ return options.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..12aaa133c
--- /dev/null
+++ b/src/rules/prefer-readonly-return-types.ts
@@ -0,0 +1,135 @@
+import type { TSESTree } from "@typescript-eslint/experimental-utils";
+import type { ReadonlynessOptions } from "@typescript-eslint/type-utils";
+import {
+ readonlynessOptionsDefaults,
+ readonlynessOptionsSchema,
+} from "@typescript-eslint/type-utils";
+import { deepmerge } from "deepmerge-ts";
+import type { JSONSchema4 } from "json-schema";
+
+import type {
+ AllowLocalMutationOption,
+ IgnoreClassOption,
+ IgnoreInferredTypesOption,
+ IgnoreInterfaceOption,
+ IgnorePatternOption,
+} from "~/common/ignore-options";
+import {
+ allowLocalMutationOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInferredTypesOptionOptionSchema,
+ ignoreInterfaceOptionSchema,
+ ignorePatternOptionSchema,
+ shouldIgnoreClass,
+ shouldIgnoreInferredTypes,
+ shouldIgnoreInterface,
+ shouldIgnoreLocalMutation,
+ shouldIgnorePattern,
+} from "~/common/ignore-options";
+import type { RuleContext, RuleMetaData, 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 = AllowLocalMutationOption &
+ IgnoreClassOption &
+ IgnoreInferredTypesOption &
+ IgnoreInterfaceOption &
+ IgnorePatternOption &
+ ReadonlynessOptions;
+
+// The schema for the rule options.
+const schema: JSONSchema4 = [
+ deepmerge(
+ allowLocalMutationOptionSchema,
+ ignoreClassOptionSchema,
+ ignoreInferredTypesOptionOptionSchema,
+ ignoreInterfaceOptionSchema,
+ ignorePatternOptionSchema,
+ readonlynessOptionsSchema
+ ),
+];
+
+// 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: RuleMetaData = {
+ 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: TSESTree.TSTypeAnnotation,
+ context: RuleContext,
+ options: Options
+): RuleResult {
+ if (
+ !isReturnType(node) ||
+ shouldIgnoreInferredTypes(node.typeAnnotation, context, options) ||
+ shouldIgnoreClass(node.typeAnnotation, context, options) ||
+ shouldIgnoreInterface(node.typeAnnotation, context, options) ||
+ shouldIgnoreLocalMutation(node.typeAnnotation, context, options) ||
+ shouldIgnorePattern(node.typeAnnotation, context, options) ||
+ isReadonly(node.typeAnnotation, context, options)
+ ) {
+ return {
+ context,
+ descriptors: [],
+ };
+ }
+
+ return {
+ context,
+ descriptors: [
+ {
+ node: node.typeAnnotation,
+ messageId: "returnTypeShouldBeReadonly",
+ },
+ ],
+ };
+}
+
+/**
+ * Is the given node a return type?
+ */
+function isReturnType(node: TSESTree.TSTypeAnnotation) {
+ 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/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 e3861f744..02365dbea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1508,13 +1508,14 @@
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@^5.0.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.3.0.tgz#a55ae72d28ffeb6badd817fe4566c9cced1f5e29"
- integrity sha512-ARUEJHJrq85aaiCqez7SANeahDsJTD3AEua34EoQN9pHS6S5Bq9emcIaGGySt/4X2zSi+vF5hAH52sEen7IO7g==
- dependencies:
- "@typescript-eslint/experimental-utils" "5.3.0"
- "@typescript-eslint/scope-manager" "5.3.0"
+"@typescript-eslint/eslint-plugin@^5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.9.1.tgz#e5a86d7e1f9dc0b3df1e6d94feaf20dd838d066c"
+ integrity sha512-Xv9tkFlyD4MQGpJgTo6wqDqGvHIRmRgah/2Sjz1PUnJTawjHWIwBivUE9x0QtU2WVii9baYgavo/bHjrZJkqTw==
+ dependencies:
+ "@typescript-eslint/experimental-utils" "5.9.1"
+ "@typescript-eslint/scope-manager" "5.9.1"
+ "@typescript-eslint/type-utils" "5.9.1"
debug "^4.3.2"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
@@ -1522,94 +1523,69 @@
semver "^7.3.5"
tsutils "^3.21.0"
-"@typescript-eslint/experimental-utils@5.3.0", "@typescript-eslint/experimental-utils@^5.0.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.0.tgz#ee56b4957547ed2b0fc7451205e41502e664f546"
- integrity sha512-NFVxYTjKj69qB0FM+piah1x3G/63WB8vCBMnlnEHUsiLzXSTWb9FmFn36FD9Zb4APKBLY3xRArOGSMQkuzTF1w==
+"@typescript-eslint/experimental-utils@5.9.1", "@typescript-eslint/experimental-utils@^5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.9.1.tgz#8c407c4dd5ffe522329df6e4c9c2b52206d5f7f1"
+ integrity sha512-cb1Njyss0mLL9kLXgS/eEY53SZQ9sT519wpX3i+U457l2UXRDuo87hgKfgRazmu9/tQb0x2sr3Y0yrU+Zz0y+w==
dependencies:
"@types/json-schema" "^7.0.9"
- "@typescript-eslint/scope-manager" "5.3.0"
- "@typescript-eslint/types" "5.3.0"
- "@typescript-eslint/typescript-estree" "5.3.0"
+ "@typescript-eslint/scope-manager" "5.9.1"
+ "@typescript-eslint/types" "5.9.1"
+ "@typescript-eslint/typescript-estree" "5.9.1"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
-"@typescript-eslint/parser@^5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.0.0.tgz#50d1be2e0def82d73e863cceba74aeeac9973592"
- integrity sha512-B6D5rmmQ14I1fdzs71eL3DAuvnPHTY/t7rQABrL9BLnx/H51Un8ox1xqYAchs0/V2trcoyxB1lMJLlrwrJCDgw==
+"@typescript-eslint/parser@^5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.9.1.tgz#b114011010a87e17b3265ca715e16c76a9834cef"
+ integrity sha512-PLYO0AmwD6s6n0ZQB5kqPgfvh73p0+VqopQQLuNfi7Lm0EpfKyDalchpVwkE+81k5HeiRrTV/9w1aNHzjD7C4g==
dependencies:
- "@typescript-eslint/scope-manager" "5.0.0"
- "@typescript-eslint/types" "5.0.0"
- "@typescript-eslint/typescript-estree" "5.0.0"
- debug "^4.3.1"
-
-"@typescript-eslint/scope-manager@5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.0.0.tgz#aea0fb0e2480c1169a02e89d9005ac3f2835713f"
- integrity sha512-5RFjdA/ain/MDUHYXdF173btOKncIrLuBmA9s6FJhzDrRAyVSA+70BHg0/MW6TE+UiKVyRtX91XpVS0gVNwVDQ==
- dependencies:
- "@typescript-eslint/types" "5.0.0"
- "@typescript-eslint/visitor-keys" "5.0.0"
+ "@typescript-eslint/scope-manager" "5.9.1"
+ "@typescript-eslint/types" "5.9.1"
+ "@typescript-eslint/typescript-estree" "5.9.1"
+ debug "^4.3.2"
-"@typescript-eslint/scope-manager@5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.0.tgz#97d0ccc7c9158e89e202d5e24ce6ba49052d432e"
- integrity sha512-22Uic9oRlTsPppy5Tcwfj+QET5RWEnZ5414Prby465XxQrQFZ6nnm5KnXgnsAJefG4hEgMnaxTB3kNEyjdjj6A==
+"@typescript-eslint/scope-manager@5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.9.1.tgz#6c27be89f1a9409f284d95dfa08ee3400166fe69"
+ integrity sha512-8BwvWkho3B/UOtzRyW07ffJXPaLSUKFBjpq8aqsRvu6HdEuzCY57+ffT7QoV4QXJXWSU1+7g3wE4AlgImmQ9pQ==
dependencies:
- "@typescript-eslint/types" "5.3.0"
- "@typescript-eslint/visitor-keys" "5.3.0"
-
-"@typescript-eslint/types@5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.0.0.tgz#25d93f6d269b2d25fdc51a0407eb81ccba60eb0f"
- integrity sha512-dU/pKBUpehdEqYuvkojmlv0FtHuZnLXFBn16zsDmlFF3LXkOpkAQ2vrKc3BidIIve9EMH2zfTlxqw9XM0fFN5w==
-
-"@typescript-eslint/types@5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416"
- integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg==
+ "@typescript-eslint/types" "5.9.1"
+ "@typescript-eslint/visitor-keys" "5.9.1"
-"@typescript-eslint/typescript-estree@5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.0.0.tgz#bc20f413c6e572c7309dbe5fa3be027984952af3"
- integrity sha512-V/6w+PPQMhinWKSn+fCiX5jwvd1vRBm7AX7SJQXEGQtwtBvjMPjaU3YTQ1ik2UF1u96X7tsB96HMnulG3eLi9Q==
+"@typescript-eslint/type-utils@5.9.1", "@typescript-eslint/type-utils@^5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.9.1.tgz#c6832ffe655b9b1fec642d36db1a262d721193de"
+ integrity sha512-tRSpdBnPRssjlUh35rE9ug5HrUvaB9ntREy7gPXXKwmIx61TNN7+l5YKgi1hMKxo5NvqZCfYhA5FvyuJG6X6vg==
dependencies:
- "@typescript-eslint/types" "5.0.0"
- "@typescript-eslint/visitor-keys" "5.0.0"
- debug "^4.3.1"
- globby "^11.0.3"
- is-glob "^4.0.1"
- semver "^7.3.5"
+ "@typescript-eslint/experimental-utils" "5.9.1"
+ debug "^4.3.2"
tsutils "^3.21.0"
-"@typescript-eslint/typescript-estree@5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf"
- integrity sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug==
+"@typescript-eslint/types@5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.9.1.tgz#1bef8f238a2fb32ebc6ff6d75020d9f47a1593c6"
+ integrity sha512-SsWegWudWpkZCwwYcKoDwuAjoZXnM1y2EbEerTHho19Hmm+bQ56QG4L4jrtCu0bI5STaRTvRTZmjprWlTw/5NQ==
+
+"@typescript-eslint/typescript-estree@5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.1.tgz#d5b996f49476495070d2b8dd354861cf33c005d6"
+ integrity sha512-gL1sP6A/KG0HwrahVXI9fZyeVTxEYV//6PmcOn1tD0rw8VhUWYeZeuWHwwhnewnvEMcHjhnJLOBhA9rK4vmb8A==
dependencies:
- "@typescript-eslint/types" "5.3.0"
- "@typescript-eslint/visitor-keys" "5.3.0"
+ "@typescript-eslint/types" "5.9.1"
+ "@typescript-eslint/visitor-keys" "5.9.1"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
-"@typescript-eslint/visitor-keys@5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.0.0.tgz#b789f7cd105e59bee5c0983a353942a5a48f56df"
- integrity sha512-yRyd2++o/IrJdyHuYMxyFyBhU762MRHQ/bAGQeTnN3pGikfh+nEmM61XTqaDH1XDp53afZ+waXrk0ZvenoZ6xw==
- dependencies:
- "@typescript-eslint/types" "5.0.0"
- eslint-visitor-keys "^3.0.0"
-
-"@typescript-eslint/visitor-keys@5.3.0":
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz#a6258790f3b7b2547f70ed8d4a1e0c3499994523"
- integrity sha512-oVIAfIQuq0x2TFDNLVavUn548WL+7hdhxYn+9j3YdJJXB7mH9dAmZNJsPDa7Jc+B9WGqoiex7GUDbyMxV0a/aw==
+"@typescript-eslint/visitor-keys@5.9.1":
+ version "5.9.1"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.1.tgz#f52206f38128dd4f675cf28070a41596eee985b7"
+ integrity sha512-Xh37pNz9e9ryW4TVdwiFzmr4hloty8cFj8GTWMXh3Z8swGwyQWeCcNgF0hm6t09iZd6eiZmIf4zHedQVP6TVtg==
dependencies:
- "@typescript-eslint/types" "5.3.0"
+ "@typescript-eslint/types" "5.9.1"
eslint-visitor-keys "^3.0.0"
JSONStream@^1.0.4:
@@ -4107,7 +4083,7 @@ globals@^13.6.0, globals@^13.9.0:
dependencies:
type-fest "^0.20.2"
-globby@^11.0.0, globby@^11.0.1, globby@^11.0.3, globby@^11.0.4:
+globby@^11.0.0, globby@^11.0.1, globby@^11.0.4:
version "11.0.4"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==