Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): added new rule prefer-readonly #555

Merged
merged 10 commits into from Jun 24, 2019
2 changes: 1 addition & 1 deletion packages/eslint-plugin-tslint/src/custom-linter.ts
Expand Up @@ -4,7 +4,7 @@ import { Program } from 'typescript';
const TSLintLinter = Linter as any;

export class CustomLinter extends TSLintLinter {
constructor(options: ILinterOptions, private program: Program) {
constructor(options: ILinterOptions, private readonly program: Program) {
super(options, program);
}

Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -171,6 +171,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: |
| [`@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: | |
| [`@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: |
| [`@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: |
| [`@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: |
| [`@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: |
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin/ROADMAP.md
Expand Up @@ -122,7 +122,7 @@
| [`no-require-imports`] | ✅ | [`@typescript-eslint/no-require-imports`] |
| [`object-literal-sort-keys`] | 🌓 | [`sort-keys`][sort-keys] <sup>[2]</sup> |
| [`prefer-const`] | 🌟 | [`prefer-const`][prefer-const] |
| [`prefer-readonly`] | 🛑 | N/A |
| [`prefer-readonly`] | | [`@typescript-eslint/prefer-readonly`] |
| [`trailing-comma`] | 🌓 | [`comma-dangle`][comma-dangle] or [Prettier] |

<sup>[1]</sup> Only warns when importing deprecated symbols<br>
Expand Down Expand Up @@ -611,6 +611,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint-
[`@typescript-eslint/prefer-interface`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-interface.md
[`@typescript-eslint/no-array-constructor`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-array-constructor.md
[`@typescript-eslint/prefer-function-type`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-function-type.md
[`@typescript-eslint/prefer-readonly`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/prefer-readonly.md
[`@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
[`@typescript-eslint/no-unnecessary-qualifier`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-qualifier.md
[`@typescript-eslint/semi`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/semi.md
Expand Down
81 changes: 81 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-readonly.md
@@ -0,0 +1,81 @@
# require never-modified private members be marked as `readonly`

This rule enforces that private members are marked as `readonly` if they're never modified outside of the constructor.

## Rule Details

Member variables with the privacy `private` are never permitted to be modified outside of their declaring class.
If that class never modifies their value, they may safely be marked as `readonly`.

Examples of **incorrect** code for this rule:

```ts
class Container {
// These member variables could be marked as readonly
private neverModifiedMember = true;
private onlyModifiedInConstructor: number;

public constructor(
onlyModifiedInConstructor: number,
// Private parameter properties can also be marked as reaodnly
private neverModifiedParameter: string,
) {
this.onlyModifiedInConstructor = onlyModifiedInConstructor;
}
}
```

Examples of **correct** code for this rule:

```ts
class Container {
// Public members might be modified externally
public publicMember: boolean;

// Protected members might be modified by child classes
protected protectedMember: number;

// This is modified later on by the class
private modifiedLater = 'unchanged';

public mutate() {
this.modifiedLater = 'mutated';
}
}
```

## Options

This rule, in its default state, does not require any argument.

### onlyInlineLambdas

You may pass `"onlyInlineLambdas": true` as a rule option within an object to restrict checking only to members immediately assigned a lambda value.

```cjson
{
"@typescript-eslint/prefer-readonly": ["error", { "onlyInlineLambdas": true }]
}
```

Example of **correct** code for the `{ "onlyInlineLambdas": true }` options:

```ts
class Container {
private neverModifiedPrivate = 'unchanged';
}
```

Example of **incorrect** code for the `{ "onlyInlineLambdas": true }` options:

```ts
class Container {
private onClick = () => {
/* ... */
};
}
```

## Related to

- TSLint: ['prefer-readonly'](https://palantir.github.io/tslint/rules/prefer-readonly)
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -57,6 +57,7 @@
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-readonly": "error",
"@typescript-eslint/prefer-regexp-exec": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
Expand Down
Expand Up @@ -9,13 +9,13 @@ import { TokenInfo } from './TokenInfo';
* A class to store information on desired offsets of tokens from each other
*/
export class OffsetStorage {
private tokenInfo: TokenInfo;
private indentSize: number;
private indentType: string;
private tree: BinarySearchTree;
private lockedFirstTokens: WeakMap<TokenOrComment, TokenOrComment>;
private desiredIndentCache: WeakMap<TokenOrComment, string>;
private ignoredTokens: WeakSet<TokenOrComment>;
private readonly tokenInfo: TokenInfo;
private readonly indentSize: number;
private readonly indentType: string;
private readonly tree: BinarySearchTree;
private readonly lockedFirstTokens: WeakMap<TokenOrComment, TokenOrComment>;
private readonly desiredIndentCache: WeakMap<TokenOrComment, string>;
private readonly ignoredTokens: WeakSet<TokenOrComment>;
/**
* @param tokenInfo a TokenInfo instance
* @param indentSize The desired size of each indentation level
Expand Down
Expand Up @@ -8,7 +8,7 @@ import { TokenOrComment } from './BinarySearchTree';
* A helper class to get token-based info related to indentation
*/
export class TokenInfo {
private sourceCode: TSESLint.SourceCode;
private readonly sourceCode: TSESLint.SourceCode;
public firstTokensByLineNumber: Map<number, TSESTree.Token>;

constructor(sourceCode: TSESLint.SourceCode) {
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -46,6 +46,7 @@ import preferFunctionType from './prefer-function-type';
import preferIncludes from './prefer-includes';
import preferInterface from './prefer-interface';
import preferNamespaceKeyword from './prefer-namespace-keyword';
import preferReadonly from './prefer-readonly';
import preferRegexpExec from './prefer-regexp-exec';
import preferStringStartsEndsWith from './prefer-string-starts-ends-with';
import promiseFunctionAsync from './promise-function-async';
Expand Down Expand Up @@ -105,6 +106,7 @@ export default {
'prefer-includes': preferIncludes,
'prefer-interface': preferInterface,
'prefer-namespace-keyword': preferNamespaceKeyword,
'prefer-readonly': preferReadonly,
'prefer-regexp-exec': preferRegexpExec,
'prefer-string-starts-ends-with': preferStringStartsEndsWith,
'promise-function-async': promiseFunctionAsync,
Expand Down