Skip to content

Commit c4c79d1

Browse files
author
Rebecca Stevens
authored
Merge pull request #29 from jonaskello/rule/prefer-readonly-types
prefer-readonly-types
2 parents d9fa5b3 + 6c6322f commit c4c79d1

15 files changed

+1235
-1347
lines changed

.eslintrc

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
// Our rules.
3535
"ts-immutable/immutable-data": "error",
3636
"ts-immutable/no-let": "error",
37-
"ts-immutable/readonly-array": ["error", { "ignoreReturnType": true }],
38-
"ts-immutable/readonly-keyword": "error",
37+
"ts-immutable/prefer-readonly-types": [
38+
"error",
39+
{ "ignoreReturnType": true }
40+
],
3941
"ts-immutable/no-method-signature": "error",
4042
"ts-immutable/no-this": "error",
4143
"ts-immutable/no-class": "error",

README.md

+6-7
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,12 @@ In addition to immutable rules this project also contains a few rules for enforc
9999

100100
### Immutability rules
101101

102-
| Name | Description | <span title="Recommended">:see_no_evil:</span> | <span title="Functional Lite">:hear_no_evil:</span> | <span title="Functional">:speak_no_evil:</span> | :wrench: | :blue_heart: |
103-
| ------------------------------------------------------------ | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: |
104-
| [`readonly-keyword`](./docs/rules/readonly-keyword.md) | Enforce readonly modifiers are used where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: |
105-
| [`readonly-array`](./docs/rules/readonly-array.md) | Enforce readonly array over mutable arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: |
106-
| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
107-
| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
108-
| [`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: |
102+
| Name | Description | <span title="Recommended">:see_no_evil:</span> | <span title="Functional Lite">:hear_no_evil:</span> | <span title="Functional">:speak_no_evil:</span> | :wrench: | :blue_heart: |
103+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: |
104+
| [`prefer-readonly-types`](./docs/rules/prefer-readonly-types.md) | Use readonly types and readonly modifiers where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: |
105+
| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
106+
| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
107+
| [`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: |
109108

110109
### Functional style rules
111110

docs/rules/readonly-keyword.md renamed to docs/rules/prefer-readonly-types.md

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
# Enforce readonly modifiers are used where possible (readonly-keyword)
1+
# Prefer readonly types over mutable types (prefer-readonly-types)
22

3-
This rule enforces use of the `readonly` modifier. The `readonly` modifier can appear on property signatures in interfaces, property declarations in classes, and index signatures.
3+
This rule enforces use of the readonly modifier and readonly types.
44

55
## Rule Details
66

7-
Below is some information about the `readonly` modifier and the benefits of using it:
7+
This rule enforces use of `readonly T[]` (`ReadonlyArray<T>`) over `T[]` (`Array<T>`).
8+
9+
The readonly modifier must appear on property signatures in interfaces, property declarations in classes, and index signatures.
10+
11+
### Benefits of using the `readonly` modifier
812

913
You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`.
1014

@@ -52,25 +56,66 @@ const foo: { readonly [key: string]: number } = { a: 1, b: 2 };
5256
foo["a"] = 3; // Error: Index signature only permits reading
5357
```
5458

59+
### Benefits of using `readonly T[]`
60+
61+
Even if an array is declared with `const` it is still possible to mutate the contents of the array.
62+
63+
```typescript
64+
interface Point {
65+
readonly x: number;
66+
readonly y: number;
67+
}
68+
const points: Array<Point> = [{ x: 23, y: 44 }];
69+
points.push({ x: 1, y: 2 }); // This is legal
70+
```
71+
72+
Using the `ReadonlyArray<T>` type or `readonly T[]` will stop this mutation:
73+
74+
```typescript
75+
interface Point {
76+
readonly x: number;
77+
readonly y: number;
78+
}
79+
80+
const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }];
81+
// const points: readonly Point[] = [{ x: 23, y: 44 }]; // This is the alternative syntax for the line above
82+
83+
points.push({ x: 1, y: 2 }); // Unresolved method push()
84+
```
85+
5586
## Options
5687

5788
The rule accepts an options object with the following properties:
5889

5990
```typescript
6091
type Options = {
92+
readonly checkImplicit: boolean
6193
readonly ignoreClass?: boolean;
6294
readonly ignoreInterface?: boolean;
6395
readonly ignoreLocal?: boolean;
6496
readonly ignorePattern?: string | Array<string>;
97+
readonly ignoreReturnType?: boolean;
6598
};
6699

67100
const defaults = {
101+
checkImplicit: false,
68102
ignoreClass: false,
69103
ignoreInterface: false,
70-
ignoreLocal: false
104+
ignoreLocal: false,
105+
ignoreReturnType: false
71106
};
72107
```
73108

109+
### `checkImplicit`
110+
111+
By default, this function only checks explicit types. Enabling this option will make the rule also check implicit types.
112+
113+
Note: Checking implicit types is more expensive (slow).
114+
115+
### `ignoreReturnType`
116+
117+
Doesn't check the return type of functions.
118+
74119
### `ignoreClass`
75120

76121
A boolean to specify if checking for `readonly` should apply to classes. `false` by default.

docs/rules/readonly-array.md

-69
This file was deleted.

src/configs/all.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const config = {
1717
rules: {
1818
"ts-immutable/no-method-signature": "error",
1919
"ts-immutable/no-mixed-interface": "error",
20-
"ts-immutable/readonly-array": "error",
21-
"ts-immutable/readonly-keyword": "error"
20+
"ts-immutable/prefer-readonly-types": "error"
2221
}
2322
}
2423
]

src/configs/immutable.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ const config = deepMerge([
1414
files: ["*.ts", "*.tsx"],
1515
rules: {
1616
"ts-immutable/no-method-signature": "warn",
17-
"ts-immutable/readonly-array": "error",
18-
"ts-immutable/readonly-keyword": "error"
17+
"ts-immutable/prefer-readonly-types": "error"
1918
}
2019
}
2120
]

src/rules/index.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,9 @@ import { name as noThisRuleName, rule as noThisRule } from "./no-this";
3737
import { name as noThrowRuleName, rule as noThrowRule } from "./no-throw";
3838
import { name as noTryRuleName, rule as noTryRule } from "./no-try";
3939
import {
40-
name as readonlyArrayRuleName,
41-
rule as readonlyArrayRule
42-
} from "./readonly-array";
43-
import {
44-
name as readonlyKeywordRuleName,
45-
rule as readonlyKeywordRule
46-
} from "./readonly-keyword";
40+
name as preferReadonlyTypesRuleName,
41+
rule as preferReadonlyTypesRule
42+
} from "./prefer-readonly-types";
4743

4844
/**
4945
* All of the custom rules.
@@ -63,6 +59,5 @@ export const rules = {
6359
[noThisRuleName]: noThisRule,
6460
[noThrowRuleName]: noThrowRule,
6561
[noTryRuleName]: noTryRule,
66-
[readonlyArrayRuleName]: readonlyArrayRule,
67-
[readonlyKeywordRuleName]: readonlyKeywordRule
62+
[preferReadonlyTypesRuleName]: preferReadonlyTypesRule
6863
};

src/rules/readonly-array.ts renamed to src/rules/prefer-readonly-types.ts

+52-7
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ import {
2121
isFunctionLike,
2222
isIdentifier,
2323
isTSArrayType,
24+
isTSIndexSignature,
25+
isTSParameterProperty,
26+
isTSTupleType,
2427
isTSTypeOperator
2528
} from "../util/typeguard";
2629

2730
// The name of this rule.
28-
export const name = "readonly-array" as const;
31+
export const name = "prefer-readonly-types" as const;
2932

3033
// The options this rule can take.
3134
type Options = ignore.IgnoreLocalOption &
3235
ignore.IgnorePatternOption &
36+
ignore.IgnoreClassOption &
37+
ignore.IgnoreInterfaceOption &
3338
ignore.IgnoreReturnTypeOption & {
3439
readonly checkImplicit: boolean;
3540
};
@@ -39,6 +44,8 @@ const schema: JSONSchema4 = [
3944
deepMerge([
4045
ignore.ignoreLocalOptionSchema,
4146
ignore.ignorePatternOptionSchema,
47+
ignore.ignoreClassOptionSchema,
48+
ignore.ignoreInterfaceOptionSchema,
4249
ignore.ignoreReturnTypeOptionSchema,
4350
{
4451
type: "object",
@@ -54,15 +61,19 @@ const schema: JSONSchema4 = [
5461

5562
// The default options for the rule.
5663
const defaultOptions: Options = {
64+
checkImplicit: false,
65+
ignoreClass: false,
66+
ignoreInterface: false,
5767
ignoreLocal: false,
58-
ignoreReturnType: false,
59-
checkImplicit: false
68+
ignoreReturnType: false
6069
};
6170

6271
// The possible error messages.
6372
const errorMessages = {
64-
generic: "Only readonly arrays allowed.",
65-
implicit: "Implicitly a mutable array. Only readonly arrays allowed."
73+
array: "Only readonly arrays allowed.",
74+
tuple: "Only readonly tuples allowed.",
75+
implicit: "Implicitly a mutable array. Only readonly arrays allowed.",
76+
property: "A readonly modifier is required."
6677
} as const;
6778

6879
// The meta data for this rule.
@@ -96,7 +107,7 @@ function checkArrayOrTupleType(
96107
? [
97108
{
98109
node,
99-
messageId: "generic",
110+
messageId: isTSTupleType(node) ? "tuple" : "array",
100111
fix:
101112
node.parent && isTSArrayType(node.parent)
102113
? fixer => [
@@ -127,14 +138,43 @@ function checkTypeReference(
127138
? [
128139
{
129140
node,
130-
messageId: "generic",
141+
messageId: "array",
131142
fix: fixer => fixer.insertTextBefore(node, "Readonly")
132143
}
133144
]
134145
: []
135146
};
136147
}
137148

149+
/**
150+
* Check if the given property/signature node violates this rule.
151+
*/
152+
function checkProperty(
153+
node:
154+
| TSESTree.ClassProperty
155+
| TSESTree.TSIndexSignature
156+
| TSESTree.TSParameterProperty
157+
| TSESTree.TSPropertySignature,
158+
context: RuleContext<keyof typeof errorMessages, Options>
159+
): RuleResult<keyof typeof errorMessages, Options> {
160+
return {
161+
context,
162+
descriptors: node.readonly
163+
? []
164+
: [
165+
{
166+
node,
167+
messageId: "property",
168+
fix: isTSIndexSignature(node)
169+
? fixer => fixer.insertTextBefore(node, "readonly ")
170+
: isTSParameterProperty(node)
171+
? fixer => fixer.insertTextBefore(node.parameter, "readonly ")
172+
: fixer => fixer.insertTextBefore(node.key, "readonly ")
173+
}
174+
]
175+
};
176+
}
177+
138178
/**
139179
* Check if the given TypeReference violates this rule.
140180
*/
@@ -203,12 +243,17 @@ export const rule = createRule<keyof typeof errorMessages, Options>(
203243
options
204244
);
205245
const _checkTypeReference = checkNode(checkTypeReference, context, options);
246+
const _checkProperty = checkNode(checkProperty, context, options);
206247
const _checkImplicitType = checkNode(checkImplicitType, context, options);
207248

208249
return {
209250
TSArrayType: _checkArrayOrTupleType,
210251
TSTupleType: _checkArrayOrTupleType,
211252
TSTypeReference: _checkTypeReference,
253+
ClassProperty: _checkProperty,
254+
TSIndexSignature: _checkProperty,
255+
TSParameterProperty: _checkProperty,
256+
TSPropertySignature: _checkProperty,
212257
...(options.checkImplicit
213258
? {
214259
VariableDeclaration: _checkImplicitType,

0 commit comments

Comments
 (0)