Skip to content

ReadonlySet and ReadonlyMap #30

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

Merged
merged 7 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
// Our rules.
"ts-immutable/immutable-data": "error",
"ts-immutable/no-let": "error",
"ts-immutable/readonly-array": ["error", { "ignoreReturnType": true }],
"ts-immutable/readonly-keyword": "error",
"ts-immutable/prefer-readonly-types": [
"error",
{ "ignoreReturnType": true }
],
"ts-immutable/no-method-signature": "error",
"ts-immutable/no-this": "error",
"ts-immutable/no-class": "error",
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,12 @@ In addition to immutable rules this project also contains a few rules for enforc

### Immutability rules

| 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: |
| ------------------------------------------------------------ | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: |
| [`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: |
| [`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: |
| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
| [`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: |
| 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: |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- | :--------------------------------------------: | :-------------------------------------------------: | :---------------------------------------------: | :------: | :---------------: |
| [`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: |
| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | |
| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: |
| [`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: |

### Functional style rules

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Enforce readonly modifiers are used where possible (readonly-keyword)
# Prefer readonly types over mutable types (prefer-readonly-types)

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.
This rule enforces use of the readonly modifier and readonly types.

## Rule Details

Below is some information about the `readonly` modifier and the benefits of using it:
This rule enforces use of `readonly T[]` (`ReadonlyArray<T>`) over `T[]` (`Array<T>`).

The readonly modifier must appear on property signatures in interfaces, property declarations in classes, and index signatures.

### Benefits of using the `readonly` modifier

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`.

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

### Benefits of using `readonly T[]`

Even if an array is declared with `const` it is still possible to mutate the contents of the array.

```typescript
interface Point {
readonly x: number;
readonly y: number;
}
const points: Array<Point> = [{ x: 23, y: 44 }];
points.push({ x: 1, y: 2 }); // This is legal
```

Using the `ReadonlyArray<T>` type or `readonly T[]` will stop this mutation:

```typescript
interface Point {
readonly x: number;
readonly y: number;
}

const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }];
// const points: readonly Point[] = [{ x: 23, y: 44 }]; // This is the alternative syntax for the line above

points.push({ x: 1, y: 2 }); // Unresolved method push()
```

## Options

The rule accepts an options object with the following properties:

```typescript
type Options = {
readonly checkImplicit: boolean
readonly ignoreClass?: boolean;
readonly ignoreInterface?: boolean;
readonly ignoreLocal?: boolean;
readonly ignorePattern?: string | Array<string>;
readonly ignoreReturnType?: boolean;
};

const defaults = {
checkImplicit: false,
ignoreClass: false,
ignoreInterface: false,
ignoreLocal: false
ignoreLocal: false,
ignoreReturnType: false
};
```

### `checkImplicit`

By default, this function only checks explicit types. Enabling this option will make the rule also check implicit types.

Note: Checking implicit types is more expensive (slow).

### `ignoreReturnType`

Doesn't check the return type of functions.

### `ignoreClass`

A boolean to specify if checking for `readonly` should apply to classes. `false` by default.
Expand Down
69 changes: 0 additions & 69 deletions docs/rules/readonly-array.md

This file was deleted.

3 changes: 1 addition & 2 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ const config = {
rules: {
"ts-immutable/no-method-signature": "error",
"ts-immutable/no-mixed-interface": "error",
"ts-immutable/readonly-array": "error",
"ts-immutable/readonly-keyword": "error"
"ts-immutable/prefer-readonly-types": "error"
}
}
]
Expand Down
3 changes: 1 addition & 2 deletions src/configs/immutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ const config = deepMerge([
files: ["*.ts", "*.tsx"],
rules: {
"ts-immutable/no-method-signature": "warn",
"ts-immutable/readonly-array": "error",
"ts-immutable/readonly-keyword": "error"
"ts-immutable/prefer-readonly-types": "error"
}
}
]
Expand Down
13 changes: 4 additions & 9 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ import { name as noThisRuleName, rule as noThisRule } from "./no-this";
import { name as noThrowRuleName, rule as noThrowRule } from "./no-throw";
import { name as noTryRuleName, rule as noTryRule } from "./no-try";
import {
name as readonlyArrayRuleName,
rule as readonlyArrayRule
} from "./readonly-array";
import {
name as readonlyKeywordRuleName,
rule as readonlyKeywordRule
} from "./readonly-keyword";
name as preferReadonlyTypesRuleName,
rule as preferReadonlyTypesRule
} from "./prefer-readonly-types";

/**
* All of the custom rules.
Expand All @@ -63,6 +59,5 @@ export const rules = {
[noThisRuleName]: noThisRule,
[noThrowRuleName]: noThrowRule,
[noTryRuleName]: noTryRule,
[readonlyArrayRuleName]: readonlyArrayRule,
[readonlyKeywordRuleName]: readonlyKeywordRule
[preferReadonlyTypesRuleName]: preferReadonlyTypesRule
};
93 changes: 75 additions & 18 deletions src/rules/readonly-array.ts → src/rules/prefer-readonly-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ import {
isFunctionLike,
isIdentifier,
isTSArrayType,
isTSIndexSignature,
isTSParameterProperty,
isTSTupleType,
isTSTypeOperator
} from "../util/typeguard";

// The name of this rule.
export const name = "readonly-array" as const;
export const name = "prefer-readonly-types" as const;

// The options this rule can take.
type Options = ignore.IgnoreLocalOption &
ignore.IgnorePatternOption &
ignore.IgnoreClassOption &
ignore.IgnoreInterfaceOption &
ignore.IgnoreReturnTypeOption & {
readonly checkImplicit: boolean;
};
Expand All @@ -39,6 +44,8 @@ const schema: JSONSchema4 = [
deepMerge([
ignore.ignoreLocalOptionSchema,
ignore.ignorePatternOptionSchema,
ignore.ignoreClassOptionSchema,
ignore.ignoreInterfaceOptionSchema,
ignore.ignoreReturnTypeOptionSchema,
{
type: "object",
Expand All @@ -54,15 +61,20 @@ const schema: JSONSchema4 = [

// The default options for the rule.
const defaultOptions: Options = {
checkImplicit: false,
ignoreClass: false,
ignoreInterface: false,
ignoreLocal: false,
ignoreReturnType: false,
checkImplicit: false
ignoreReturnType: false
};

// The possible error messages.
const errorMessages = {
generic: "Only readonly arrays allowed.",
implicit: "Implicitly a mutable array. Only readonly arrays allowed."
array: "Only readonly arrays allowed.",
implicit: "Implicitly a mutable array. Only readonly arrays allowed.",
property: "A readonly modifier is required.",
tuple: "Only readonly tuples allowed.",
type: "Only readonly types allowed."
} as const;

// The meta data for this rule.
Expand All @@ -78,6 +90,11 @@ const meta: RuleMetaData<keyof typeof errorMessages> = {
schema
};

const mutableToImmutableTypes: ReadonlyMap<string, string> = new Map<
string,
string
>([["Array", "ReadonlyArray"], ["Map", "ReadonlyMap"], ["Set", "ReadonlySet"]]);

/**
* Check if the given ArrayType or TupleType violates this rule.
*/
Expand All @@ -96,7 +113,7 @@ function checkArrayOrTupleType(
? [
{
node,
messageId: "generic",
messageId: isTSTupleType(node) ? "tuple" : "array",
fix:
node.parent && isTSArrayType(node.parent)
? fixer => [
Expand All @@ -117,21 +134,56 @@ function checkTypeReference(
node: TSESTree.TSTypeReference,
context: RuleContext<keyof typeof errorMessages, Options>,
options: Options
): RuleResult<keyof typeof errorMessages, Options> {
if (isIdentifier(node.typeName)) {
const immutableType = mutableToImmutableTypes.get(node.typeName.name);
return {
context,
descriptors:
immutableType && (!options.ignoreReturnType || !isInReturnType(node))
? [
{
node,
messageId: "type",
fix: fixer => fixer.replaceText(node.typeName, immutableType)
}
]
: []
};
} else {
return {
context,
descriptors: []
};
}
}

/**
* Check if the given property/signature node violates this rule.
*/
function checkProperty(
node:
| TSESTree.ClassProperty
| TSESTree.TSIndexSignature
| TSESTree.TSParameterProperty
| TSESTree.TSPropertySignature,
context: RuleContext<keyof typeof errorMessages, Options>
): RuleResult<keyof typeof errorMessages, Options> {
return {
context,
descriptors:
isIdentifier(node.typeName) &&
node.typeName.name === "Array" &&
(!options.ignoreReturnType || !isInReturnType(node))
? [
{
node,
messageId: "generic",
fix: fixer => fixer.insertTextBefore(node, "Readonly")
}
]
: []
descriptors: node.readonly
? []
: [
{
node,
messageId: "property",
fix: isTSIndexSignature(node)
? fixer => fixer.insertTextBefore(node, "readonly ")
: isTSParameterProperty(node)
? fixer => fixer.insertTextBefore(node.parameter, "readonly ")
: fixer => fixer.insertTextBefore(node.key, "readonly ")
}
]
};
}

Expand Down Expand Up @@ -203,12 +255,17 @@ export const rule = createRule<keyof typeof errorMessages, Options>(
options
);
const _checkTypeReference = checkNode(checkTypeReference, context, options);
const _checkProperty = checkNode(checkProperty, context, options);
const _checkImplicitType = checkNode(checkImplicitType, context, options);

return {
TSArrayType: _checkArrayOrTupleType,
TSTupleType: _checkArrayOrTupleType,
TSTypeReference: _checkTypeReference,
ClassProperty: _checkProperty,
TSIndexSignature: _checkProperty,
TSParameterProperty: _checkProperty,
TSPropertySignature: _checkProperty,
...(options.checkImplicit
? {
VariableDeclaration: _checkImplicitType,
Expand Down
Loading