diff --git a/.eslintrc.json b/.eslintrc.json index bd6c7fc07..22394e841 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -126,6 +126,7 @@ "functional/no-expression-statement": "off", "functional/no-let": "off", "functional/no-loop-statement": "off", + "functional/no-mixed-type": "off", "functional/no-return-void": "off", "functional/no-this-expression": "off", "functional/no-throw-statement": "off", diff --git a/README.md b/README.md index 798121699..a5308487c 100644 --- a/README.md +++ b/README.md @@ -186,12 +186,14 @@ 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-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :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: | +| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Object-Orientation Rules diff --git a/docs/rules/prefer-immutable-parameter-types.md b/docs/rules/prefer-immutable-parameter-types.md new file mode 100644 index 000000000..7f1e68892 --- /dev/null +++ b/docs/rules/prefer-immutable-parameter-types.md @@ -0,0 +1,195 @@ +# Prefer immutable parameter types over mutable ones (prefer-immutable-parameter-types) + +Although other rules can be used to ensure parameter are not mutated, it is +best to explicitly declare that the parameters are immutable. + +It is also worth noting that as immutable types are not assignable to mutable +ones, users will not be able to pass something like a readonly array to a +functional that wants a mutable array; even if the function does not actually +mutate said array. + +## Rule Details + +This rule differs from the +[@typescript-eslint/prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md) +rule by the fact that it uses the +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to +calculated immutability. This library allows for more powerful and customizable +immutability enforcements to be made. + +This rule is designed to replace the aforementioned rule. + +Examples of **incorrect** code for this rule: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: "error" */ + +function array1(arg: string[]) {} // array is not readonly +function array2(arg: ReadonlyArray) {} // array element is not readonly +function array3(arg: [string, number]) {} // tuple is not readonly +function array4(arg: readonly [string[], number]) {} // tuple element is not readonly +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { prop: string }) {} // property is not readonly +function object2(arg: { readonly prop: string; prop2: string }) {} // not all properties are readonly +function object3(arg: { 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(arg: CustomArrayType) {} + +interface CustomFunction { + (): void; + prop: string; // note: this property is mutable +} +function custom2(arg: CustomFunction) {} + +function union(arg: string[] | ReadonlyArray) {} // not all types are readonly + +// rule also checks function types +interface Foo1 { + (arg: string[]): void; +} +interface Foo2 { + new (arg: string[]): void; +} +const x = { foo(arg: string[]): void; }; +function foo(arg: string[]); +type Foo3 = (arg: string[]) => void; +interface Foo4 { + foo(arg: string[]): void; +} +``` + +Examples of **correct** code for this rule: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: "error" */ + +function array1(arg: ReadonlyArray) {} +function array2(arg: ReadonlyArray>) {} +function array3(arg: readonly [string, number]) {} +function array4(arg: readonly [ReadonlyArray, number]) {} +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { readonly prop: string }) {} +function object2(arg: { readonly prop: string; readonly prop2: string }) {} +function object3(arg: { 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(arg: 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(arg: CustomFunction) {} + +function union(arg: ReadonlyArray | ReadonlyArray) {} + +function primitive1(arg: string) {} +function primitive2(arg: number) {} +function primitive3(arg: boolean) {} +function primitive4(arg: unknown) {} +function primitive5(arg: null) {} +function primitive6(arg: undefined) {} +function primitive7(arg: any) {} +function primitive8(arg: never) {} +function primitive9(arg: string | number | undefined) {} + +function fnSig(arg: () => void) {} + +enum Foo { a, b } +function enum1(arg: Foo) {} + +function symb1(arg: symbol) {} +const customSymbol = Symbol('a'); +function symb2(arg: typeof customSymbol) {} + +// function types +interface Foo1 { + (arg: ReadonlyArray): void; +} +interface Foo2 { + new (arg: ReadonlyArray): void; +} +const x = { foo(arg: ReadonlyArray): void; }; +function foo(arg: ReadonlyArray); +type Foo3 = (arg: ReadonlyArray) => void; +interface Foo4 { + foo(arg: ReadonlyArray): void; +} +``` + +## Settings + +This rule can leverage shared settings to configure immutability settings. + +See the [immutability](./settings/immutability.md) docs. + +## Options + +This rule accepts an options object of the following type: + +```ts +type Options = { + enforcement: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; +} +``` + +The default options: + +```ts +const defaults = { + enforcement: "ReadonlyDeep", +} +``` + +### `enforcement` + +The level of immutability that should be enforced. + +**incorrect**: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ + +function array(arg: ReadonlyArray) {} // ReadonlyArray is not immutable +function set(arg: ReadonlySet) {} // ReadonlySet is not immutable +function map(arg: ReadonlyMap) {} // ReadonlyMap is not immutable +``` + +**correct**: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ + +function set(arg: Readonly>) {} +function map(arg: Readonly>) {} +function object(arg: Readonly<{ prop: string }>) {} +``` + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "ReadonlyShallow" }] */ + +function array(arg: ReadonlyArray<{ foo: string; }>) {} +function set(arg: ReadonlySet<{ foo: string; }>) {} +function map(arg: ReadonlyMap<{ foo: string; }>) {} +function object(arg: Readonly<{ prop: { foo: string; }; }>) {} +``` diff --git a/docs/rules/settings/immutability.md b/docs/rules/settings/immutability.md new file mode 100644 index 000000000..c2ec9a416 --- /dev/null +++ b/docs/rules/settings/immutability.md @@ -0,0 +1,41 @@ +# Using the `immutability` setting + +We are using the +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to +determine the immutability of types. This library can be configure for all rules +at once using a shared setting. + +## Overrides + +For details see [the overrides +section](https://github.com/RebeccaStevens/is-immutable-type#overrides) of +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type). + +### Example of configuring immutability overrides + +In this example, we are configuring +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) to treat +any readonly array (regardless of the syntax used) as immutable in the case +where it was found to be deeply readonly. If it was only found to be shallowly +readonly, then no override will be applied. + +```jsonc +// .eslintrc.json +{ + // ... + "settings": { + "immutability": { + "overrides": [ + { + "name": "ReadonlyArray", + "to": "Immutable", + "from": "ReadonlyDeep" + } + ] + } + }, + "rules": { + // ... + } +} +``` diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md new file mode 100644 index 000000000..d204685c9 --- /dev/null +++ b/docs/rules/type-declaration-immutability.md @@ -0,0 +1,142 @@ +# Enforce a level of immutability for type declaration (type-declaration-immutability) + +Require type alias declarations and interfaces that imply some level of +immutability to comply to it. + +## Rule Details + +This rule enforces rules on type immutability based on the type's name. + +For details on what the different levels of immutability mean, see [the +immutability +section](https://github.com/RebeccaStevens/is-immutable-type#immutability) of +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type). + +Examples of **incorrect** code for this rule: + + + +```ts +/* eslint functional/type-declaration-immutability: "error" */ + +type ReadonlyElement = { + id: number; + data: string[]; +}; + +type ReadonlyDeepElement = Readonly<{ + id: number; + data: string[]; +}>; + +type MutableElement = Readonly<{ + id: number; + data: ReadonlyArray; +}>; +``` + +Examples of **correct** code for this rule: + + + +```ts +/* eslint functional/type-declaration-immutability: "error" */ + +type ReadonlyElement = Readonly<{ + id: number; + data: string[]; +}>; + +type ReadonlyDeepElement = Readonly<{ + id: number; + data: ReadonlyArray; +}>; + +type MutableElement = { + readonly id: number; + data: ReadonlyArray; +}; +``` + +## Settings + +This rule can leverage shared settings to configure immutability settings. + +See the [immutability](./settings/immutability.md) docs. + +## Options + +This rule accepts an options object of the following type: + +```ts +type Options = { + rules: Array<{ + identifier: string | string[]; + immutability: "Mutable" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + comparator?: "Less" | "AtMost" | "Exactly" | "AtLeast" | "More"; + }>; + ignoreInterfaces: boolean; + ignorePattern: string[] | string; +} +``` + +The default options: + +```ts +const defaults = { + rules: [ + { + identifier: "I?Immutable.+", + immutability: "Immutable", + comparator: "AtLeast", + }, + { + identifier: "I?ReadonlyDeep.+", + immutability: "ReadonlyDeep", + comparator: "AtLeast", + }, + { + identifier: "I?Readonly.+", + immutability: "ReadonlyShallow", + comparator: "AtLeast", + }, + { + identifier: "I?Mutable.+", + immutability: "Mutable", + comparator: "AtMost", + }, + ], + ignoreInterfaces: false, +} +``` + +### `rules` + +An array of rules to enforce immutability by. + +These rules should be sorted by precedence as each type declaration will only +enforce the first matching rule to it. + +#### `identifier` + +A regex pattern or an array of regex patterns that are used to match against the +name of the type declarations. + +#### `immutability` + +The level of immutability to compare against. This value will be compared to the +calculated immutability using the `comparator`. + +#### `comparator` + +The comparator to use to compare the calculated immutability to the desired +immutability. This can be thought of as `<`, `<=`, `==`, `>=` or `>`. + +### `ignoreInterfaces` + +A boolean to specify whether interfaces should be exempt from these rules. +`false` by default. + +### `ignorePattern` + +See the [ignorePattern](./options/ignore-pattern.md) docs.