Skip to content

Commit 998eb5a

Browse files
authored
feat: migrate a new rule 'use-read-only-spread' (#472)
This commit adds a new rule 'use-read-only-spread' originally developed and used here: https://github.com/adeira/universe/blob/91ba39d4f59ac0376121b67e443f474a6ade4feb/src/eslint-plugin-adeira/src/rules/flow-use-readonly-spread.js It prevents users from accidentally creating an object which is no longer read-only because of Flow spread operator.
1 parent 6d5362b commit 998eb5a

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

.README/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,5 @@ When `true`, only checks files with a [`@flow` annotation](http://flowtype.org/d
194194
{"gitdown": "include", "file": "./rules/type-import-style.md"}
195195
{"gitdown": "include", "file": "./rules/union-intersection-spacing.md"}
196196
{"gitdown": "include", "file": "./rules/use-flow-type.md"}
197+
{"gitdown": "include", "file": "./rules/use-read-only-spread.md"}
197198
{"gitdown": "include", "file": "./rules/valid-syntax.md"}

.README/rules/use-read-only-spread.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
### `use-read-only-spread`
2+
3+
Warns against accidentally creating an object which is no longer read-only because of how spread operator works in Flow. Imagine the following code:
4+
5+
```flow js
6+
type INode = {|
7+
+type: string,
8+
|};
9+
10+
type Identifier = {|
11+
...INode,
12+
+name: string,
13+
|};
14+
```
15+
16+
You might expect the identifier name to be read-only, however, that's not true ([flow.org/try](https://flow.org/try/#0C4TwDgpgBAkgcgewCbQLxQN4B8BQUoDUokAXFAM7ABOAlgHYDmANDlgL4DcOOx0MKdYDQBmNCFSjpseKADp58ZBBb4CdAIYBbCGUq1GLdlxwBjBHUpQAHmX4RBIsRKlQN2sgHIPTKL08eoTm4rWV5JKA8AZQALBABXABskVwRgKAAjaAB3WmB1dISIAEIPLhC3NAiY+KSUtMyoHJo8guLSnCA)):
17+
18+
```flow js
19+
const x: Identifier = { name: '', type: '' };
20+
21+
x.type = 'Should not be writable!'; // No Flow error
22+
x.name = 'Should not be writable!'; // No Flow error
23+
```
24+
25+
This rule suggests to use `$ReadOnly<…>` to prevent accidental loss of readonly-ness:
26+
27+
```flow js
28+
type Identifier = $ReadOnly<{|
29+
...INode,
30+
+name: string,
31+
|}>;
32+
33+
const x: Identifier = { name: '', type: '' };
34+
35+
x.type = 'Should not be writable!'; // $FlowExpectedError[cannot-write]
36+
x.name = 'Should not be writable!'; // $FlowExpectedError[cannot-write]
37+
```
38+
39+
<!-- assertions useReadOnlySpread -->

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
* [`type-import-style`](#eslint-plugin-flowtype-rules-type-import-style)
5757
* [`union-intersection-spacing`](#eslint-plugin-flowtype-rules-union-intersection-spacing)
5858
* [`use-flow-type`](#eslint-plugin-flowtype-rules-use-flow-type)
59+
* [`use-read-only-spread`](#eslint-plugin-flowtype-rules-use-read-only-spread)
5960
* [`valid-syntax`](#eslint-plugin-flowtype-rules-valid-syntax)
6061

6162

@@ -6430,6 +6431,105 @@ import type A from "a"; type X<B = A<string>> = { b: B }; let x: X; console.log(
64306431

64316432

64326433

6434+
<a name="eslint-plugin-flowtype-rules-use-read-only-spread"></a>
6435+
### <code>use-read-only-spread</code>
6436+
6437+
Warns against accidentally creating an object which is no longer read-only because of how spread operator works in Flow. Imagine the following code:
6438+
6439+
```flow js
6440+
type INode = {|
6441+
+type: string,
6442+
|};
6443+
6444+
type Identifier = {|
6445+
...INode,
6446+
+name: string,
6447+
|};
6448+
```
6449+
6450+
You might expect the identifier name to be read-only, however, that's not true ([flow.org/try](https://flow.org/try/#0C4TwDgpgBAkgcgewCbQLxQN4B8BQUoDUokAXFAM7ABOAlgHYDmANDlgL4DcOOx0MKdYDQBmNCFSjpseKADp58ZBBb4CdAIYBbCGUq1GLdlxwBjBHUpQAHmX4RBIsRKlQN2sgHIPTKL08eoTm4rWV5JKA8AZQALBABXABskVwRgKAAjaAB3WmB1dISIAEIPLhC3NAiY+KSUtMyoHJo8guLSnCA)):
6451+
6452+
```flow js
6453+
const x: Identifier = { name: '', type: '' };
6454+
6455+
x.type = 'Should not be writable!'; // No Flow error
6456+
x.name = 'Should not be writable!'; // No Flow error
6457+
```
6458+
6459+
This rule suggests to use `$ReadOnly<…>` to prevent accidental loss of readonly-ness:
6460+
6461+
```flow js
6462+
type Identifier = $ReadOnly<{|
6463+
...INode,
6464+
+name: string,
6465+
|}>;
6466+
6467+
const x: Identifier = { name: '', type: '' };
6468+
6469+
x.type = 'Should not be writable!'; // $FlowExpectedError[cannot-write]
6470+
x.name = 'Should not be writable!'; // $FlowExpectedError[cannot-write]
6471+
```
6472+
6473+
The following patterns are considered problems:
6474+
6475+
```js
6476+
type INode = {||};
6477+
type Identifier = {|
6478+
...INode,
6479+
+aaa: string,
6480+
|};
6481+
// Message: Flow type with spread property and all readonly properties should be wrapped in '$ReadOnly<>' to prevent accidental loss of readonly-ness.
6482+
6483+
type INode = {||};
6484+
type Identifier = {|
6485+
...INode,
6486+
+aaa: string,
6487+
+bbb: string,
6488+
|};
6489+
// Message: Flow type with spread property and all readonly properties should be wrapped in '$ReadOnly<>' to prevent accidental loss of readonly-ness.
6490+
```
6491+
6492+
The following patterns are not considered problems:
6493+
6494+
```js
6495+
type INode = {||};
6496+
type Identifier = {|
6497+
...INode,
6498+
name: string,
6499+
|};
6500+
6501+
type INode = {||};
6502+
type Identifier = {|
6503+
...INode,
6504+
name: string, // writable on purpose
6505+
+surname: string,
6506+
|};
6507+
6508+
type Identifier = {|
6509+
+name: string,
6510+
|};
6511+
6512+
type INode = {||};
6513+
type Identifier = $ReadOnly<{|
6514+
...INode,
6515+
+name: string,
6516+
|}>;
6517+
6518+
type INode = {||};
6519+
type Identifier = $ReadOnly<{|
6520+
...INode,
6521+
name: string, // writable on purpose
6522+
|}>;
6523+
6524+
type INode = {||};
6525+
type Identifier = $ReadOnly<{|
6526+
...INode,
6527+
-name: string,
6528+
|}>;
6529+
```
6530+
6531+
6532+
64336533
<a name="eslint-plugin-flowtype-rules-valid-syntax"></a>
64346534
### <code>valid-syntax</code>
64356535

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import typeIdMatch from './rules/typeIdMatch';
3939
import typeImportStyle from './rules/typeImportStyle';
4040
import unionIntersectionSpacing from './rules/unionIntersectionSpacing';
4141
import useFlowType from './rules/useFlowType';
42+
import useReadOnlySpread from './rules/useReadOnlySpread';
4243
import validSyntax from './rules/validSyntax';
4344
import spreadExactType from './rules/spreadExactType';
4445
import arrowParens from './rules/arrowParens';
@@ -85,6 +86,7 @@ const rules = {
8586
'type-import-style': typeImportStyle,
8687
'union-intersection-spacing': unionIntersectionSpacing,
8788
'use-flow-type': useFlowType,
89+
'use-read-only-spread': useReadOnlySpread,
8890
'valid-syntax': validSyntax,
8991
};
9092

src/rules/useReadOnlySpread.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const meta = {
2+
messages: {
3+
readonlySpread:
4+
'Flow type with spread property and all readonly properties should be ' +
5+
'wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.',
6+
},
7+
};
8+
9+
const create = (context) => {
10+
return {
11+
TypeAlias (node) {
12+
if (node.right.type === 'GenericTypeAnnotation' && node.right.id.name === '$ReadOnly') {
13+
// it's already $ReadOnly<…>, nothing to do
14+
} else if (node.right.type === 'ObjectTypeAnnotation') {
15+
// let's iterate all props and if everything is readonly then throw
16+
let shouldThrow = false;
17+
let hasSpread = false;
18+
for (const property of node.right.properties) {
19+
if (property.type === 'ObjectTypeProperty') {
20+
if (property.variance && property.variance.kind === 'plus') {
21+
shouldThrow = true;
22+
} else {
23+
shouldThrow = false;
24+
break;
25+
}
26+
} else if (property.type === 'ObjectTypeSpreadProperty') {
27+
hasSpread = true;
28+
}
29+
}
30+
if (hasSpread === true && shouldThrow === true) {
31+
context.report({
32+
messageId: 'readonlySpread',
33+
node: node.right,
34+
});
35+
}
36+
}
37+
},
38+
};
39+
};
40+
41+
export default {
42+
create,
43+
meta,
44+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: `type INode = {||};
5+
type Identifier = {|
6+
...INode,
7+
+aaa: string,
8+
|};`,
9+
errors: [{
10+
message: 'Flow type with spread property and all readonly properties should be wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.',
11+
}],
12+
},
13+
{
14+
code: `type INode = {||};
15+
type Identifier = {|
16+
...INode,
17+
+aaa: string,
18+
+bbb: string,
19+
|};`,
20+
errors: [{
21+
message: 'Flow type with spread property and all readonly properties should be wrapped in \'$ReadOnly<…>\' to prevent accidental loss of readonly-ness.',
22+
}],
23+
},
24+
],
25+
26+
valid: [
27+
// Object with spread operator:
28+
{
29+
code: `type INode = {||};
30+
type Identifier = {|
31+
...INode,
32+
name: string,
33+
|};`,
34+
},
35+
{
36+
code: `type INode = {||};
37+
type Identifier = {|
38+
...INode,
39+
name: string, // writable on purpose
40+
+surname: string,
41+
|};`,
42+
},
43+
44+
// Object without spread operator:
45+
{
46+
code: `type Identifier = {|
47+
+name: string,
48+
|};`,
49+
},
50+
51+
// Read-only object with spread:
52+
{
53+
code: `type INode = {||};
54+
type Identifier = $ReadOnly<{|
55+
...INode,
56+
+name: string,
57+
|}>;`,
58+
},
59+
{
60+
code: `type INode = {||};
61+
type Identifier = $ReadOnly<{|
62+
...INode,
63+
name: string, // writable on purpose
64+
|}>;`,
65+
},
66+
67+
// Write-only object with spread:
68+
{
69+
code: `type INode = {||};
70+
type Identifier = $ReadOnly<{|
71+
...INode,
72+
-name: string,
73+
|}>;`,
74+
},
75+
],
76+
};

tests/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const reportingRules = [
5151
'type-import-style',
5252
'union-intersection-spacing',
5353
'use-flow-type',
54+
'use-read-only-spread',
5455
'valid-syntax',
5556
];
5657

0 commit comments

Comments
 (0)