Skip to content

Commit

Permalink
feat(eslint-plugin): added new rule prefer-readonly (#555)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Josh Goldberg authored and bradzacher committed Jun 24, 2019
1 parent a53fc71 commit 76b89a5
Show file tree
Hide file tree
Showing 12 changed files with 1,010 additions and 13 deletions.
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

0 comments on commit 76b89a5

Please sign in to comment.