Skip to content

Commit

Permalink
feat(prefer-readonly-return-types): create new rule
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed Jan 12, 2022
1 parent bac53a8 commit 78f2342
Show file tree
Hide file tree
Showing 14 changed files with 972 additions and 85 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.json
Expand Up @@ -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/**"],
Expand Down
13 changes: 7 additions & 6 deletions README.md
Expand Up @@ -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 | <span title="No Mutations">:see_no_evil:</span> | <span title="Lite">:hear_no_evil:</span> | <span title="Recommended">:speak_no_evil:</span> | :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 | <span title="No Mutations">:see_no_evil:</span> | <span title="Lite">:hear_no_evil:</span> | <span title="Recommended">:speak_no_evil:</span> | :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

Expand Down
268 changes: 268 additions & 0 deletions 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:

<!-- eslint-skip -->

```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<T> 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<T> instead

interface CustomArrayType extends ReadonlyArray<string> {
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<number[]> {} // 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<T> 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<T> instead

interface CustomArrayType extends ReadonlyArray<string> {
readonly prop: string;
}
function custom1(): Readonly<CustomArrayType> {}
// 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<number[]> {}

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:

<!-- eslint-skip -->

```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:

<!-- eslint-skip -->

```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:

<!-- eslint-skip -->

```ts
/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreCollections": false }] */

const foo: number[] = [];
const bar: [string, string] = ["foo", "bar"];
const baz: Set<string, string> = new Set();
const qux: Map<string, string> = 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<string, string> = new Set();
const qux: Map<string, string> = 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.
7 changes: 4 additions & 3 deletions package.json
Expand Up @@ -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"
},
Expand All @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions src/common/ignore-options.ts
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<string, BaseOptions>,
options: Partial<IgnoreInferredTypesOption>
) {
return options.ignoreInferredTypes === true && node === null;
}

/**
* Should the given node be allowed base off the following rule options?
*
Expand Down
1 change: 1 addition & 0 deletions src/configs/all.ts
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Expand Up @@ -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";

Expand All @@ -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,
};

0 comments on commit 78f2342

Please sign in to comment.