Skip to content

Commit 76b89a5

Browse files
Josh Goldbergbradzacher
Josh Goldberg
authored andcommitted
feat(eslint-plugin): added new rule prefer-readonly (#555)
* feat(eslint-plugin): added new rule prefer-readonly Adds the equivalent of TSLint's `prefer-readonly` rule. * Added docs, auto-fixing * Updated docs; love the new build time checks! * Fixed linting errors (ha) and corrected internal source * PR feedback: non recommended; :exit; some test coverage * I guess tslintRuleName isn't allowed now? * Added back recommended as false * Removed :exit; fixed README.md table
1 parent a53fc71 commit 76b89a5

File tree

12 files changed

+1010
-13
lines changed

12 files changed

+1010
-13
lines changed

packages/eslint-plugin-tslint/src/custom-linter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Program } from 'typescript';
44
const TSLintLinter = Linter as any;
55

66
export class CustomLinter extends TSLintLinter {
7-
constructor(options: ILinterOptions, private program: Program) {
7+
constructor(options: ILinterOptions, private readonly program: Program) {
88
super(options, program);
99
}
1010

packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
171171
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
172172
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: |
173173
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | |
174+
| [`@typescript-eslint/prefer-readonly`](./docs/rules/prefer-readonly.md) | Requires that private members are marked as `readonly` if they're never modified outside of the constructor | | :wrench: | :thought_balloon: |
174175
| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided | | | :thought_balloon: |
175176
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | :thought_balloon: |
176177
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: |

packages/eslint-plugin/ROADMAP.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@
122122
| [`no-require-imports`] || [`@typescript-eslint/no-require-imports`] |
123123
| [`object-literal-sort-keys`] | 🌓 | [`sort-keys`][sort-keys] <sup>[2]</sup> |
124124
| [`prefer-const`] | 🌟 | [`prefer-const`][prefer-const] |
125-
| [`prefer-readonly`] | 🛑 | N/A |
125+
| [`prefer-readonly`] | | [`@typescript-eslint/prefer-readonly`] |
126126
| [`trailing-comma`] | 🌓 | [`comma-dangle`][comma-dangle] or [Prettier] |
127127

128128
<sup>[1]</sup> Only warns when importing deprecated symbols<br>
@@ -611,6 +611,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
611611
[`@typescript-eslint/prefer-interface`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-interface.md
612612
[`@typescript-eslint/no-array-constructor`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md
613613
[`@typescript-eslint/prefer-function-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-function-type.md
614+
[`@typescript-eslint/prefer-readonly`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly.md
614615
[`@typescript-eslint/no-for-in-array`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-for-in-array.md
615616
[`@typescript-eslint/no-unnecessary-qualifier`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
616617
[`@typescript-eslint/semi`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# require never-modified private members be marked as `readonly`
2+
3+
This rule enforces that private members are marked as `readonly` if they're never modified outside of the constructor.
4+
5+
## Rule Details
6+
7+
Member variables with the privacy `private` are never permitted to be modified outside of their declaring class.
8+
If that class never modifies their value, they may safely be marked as `readonly`.
9+
10+
Examples of **incorrect** code for this rule:
11+
12+
```ts
13+
class Container {
14+
// These member variables could be marked as readonly
15+
private neverModifiedMember = true;
16+
private onlyModifiedInConstructor: number;
17+
18+
public constructor(
19+
onlyModifiedInConstructor: number,
20+
// Private parameter properties can also be marked as reaodnly
21+
private neverModifiedParameter: string,
22+
) {
23+
this.onlyModifiedInConstructor = onlyModifiedInConstructor;
24+
}
25+
}
26+
```
27+
28+
Examples of **correct** code for this rule:
29+
30+
```ts
31+
class Container {
32+
// Public members might be modified externally
33+
public publicMember: boolean;
34+
35+
// Protected members might be modified by child classes
36+
protected protectedMember: number;
37+
38+
// This is modified later on by the class
39+
private modifiedLater = 'unchanged';
40+
41+
public mutate() {
42+
this.modifiedLater = 'mutated';
43+
}
44+
}
45+
```
46+
47+
## Options
48+
49+
This rule, in its default state, does not require any argument.
50+
51+
### onlyInlineLambdas
52+
53+
You may pass `"onlyInlineLambdas": true` as a rule option within an object to restrict checking only to members immediately assigned a lambda value.
54+
55+
```cjson
56+
{
57+
"@typescript-eslint/prefer-readonly": ["error", { "onlyInlineLambdas": true }]
58+
}
59+
```
60+
61+
Example of **correct** code for the `{ "onlyInlineLambdas": true }` options:
62+
63+
```ts
64+
class Container {
65+
private neverModifiedPrivate = 'unchanged';
66+
}
67+
```
68+
69+
Example of **incorrect** code for the `{ "onlyInlineLambdas": true }` options:
70+
71+
```ts
72+
class Container {
73+
private onClick = () => {
74+
/* ... */
75+
};
76+
}
77+
```
78+
79+
## Related to
80+
81+
- TSLint: ['prefer-readonly'](https://palantir.github.io/tslint/rules/prefer-readonly)

packages/eslint-plugin/src/configs/all.json

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@typescript-eslint/prefer-function-type": "error",
5858
"@typescript-eslint/prefer-includes": "error",
5959
"@typescript-eslint/prefer-namespace-keyword": "error",
60+
"@typescript-eslint/prefer-readonly": "error",
6061
"@typescript-eslint/prefer-regexp-exec": "error",
6162
"@typescript-eslint/prefer-string-starts-ends-with": "error",
6263
"@typescript-eslint/promise-function-async": "error",

packages/eslint-plugin/src/rules/indent-new-do-not-use/OffsetStorage.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { TokenInfo } from './TokenInfo';
99
* A class to store information on desired offsets of tokens from each other
1010
*/
1111
export class OffsetStorage {
12-
private tokenInfo: TokenInfo;
13-
private indentSize: number;
14-
private indentType: string;
15-
private tree: BinarySearchTree;
16-
private lockedFirstTokens: WeakMap<TokenOrComment, TokenOrComment>;
17-
private desiredIndentCache: WeakMap<TokenOrComment, string>;
18-
private ignoredTokens: WeakSet<TokenOrComment>;
12+
private readonly tokenInfo: TokenInfo;
13+
private readonly indentSize: number;
14+
private readonly indentType: string;
15+
private readonly tree: BinarySearchTree;
16+
private readonly lockedFirstTokens: WeakMap<TokenOrComment, TokenOrComment>;
17+
private readonly desiredIndentCache: WeakMap<TokenOrComment, string>;
18+
private readonly ignoredTokens: WeakSet<TokenOrComment>;
1919
/**
2020
* @param tokenInfo a TokenInfo instance
2121
* @param indentSize The desired size of each indentation level

packages/eslint-plugin/src/rules/indent-new-do-not-use/TokenInfo.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { TokenOrComment } from './BinarySearchTree';
88
* A helper class to get token-based info related to indentation
99
*/
1010
export class TokenInfo {
11-
private sourceCode: TSESLint.SourceCode;
11+
private readonly sourceCode: TSESLint.SourceCode;
1212
public firstTokensByLineNumber: Map<number, TSESTree.Token>;
1313

1414
constructor(sourceCode: TSESLint.SourceCode) {

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import preferFunctionType from './prefer-function-type';
4646
import preferIncludes from './prefer-includes';
4747
import preferInterface from './prefer-interface';
4848
import preferNamespaceKeyword from './prefer-namespace-keyword';
49+
import preferReadonly from './prefer-readonly';
4950
import preferRegexpExec from './prefer-regexp-exec';
5051
import preferStringStartsEndsWith from './prefer-string-starts-ends-with';
5152
import promiseFunctionAsync from './promise-function-async';
@@ -105,6 +106,7 @@ export default {
105106
'prefer-includes': preferIncludes,
106107
'prefer-interface': preferInterface,
107108
'prefer-namespace-keyword': preferNamespaceKeyword,
109+
'prefer-readonly': preferReadonly,
108110
'prefer-regexp-exec': preferRegexpExec,
109111
'prefer-string-starts-ends-with': preferStringStartsEndsWith,
110112
'promise-function-async': promiseFunctionAsync,

0 commit comments

Comments
 (0)