Skip to content

Commit 7cabcda

Browse files
authored
fix: support union type in React react-only props rule (#477)
This change fixes the following (simplified) example from our application: ``` type Props = | { +children: React$Node, +title: React$Node, +withHiddenTitle?: false, } | { +children: React$Node, +title?: React$Node, +withHiddenTitle: true, }; const props: Props = {children:"", withHiddenTitle: true} props.title = ""; // correct Flow error ``` Flow correctly throws the following error: ``` 14: props.title = ""; ^ Cannot assign empty string to `props.title` because property `title` is not writable. [cannot-write] ``` However, `flowtype/require-readonly-react-props` Eslint rule throws the following (incorrect) error: ``` Props must be $ReadOnly ``` For simplification, all of the objects in the union must be readonly even though it's probably not a strict requirement in Flow. This can be improved later when needed.
1 parent 0488fcb commit 7cabcda

File tree

4 files changed

+61
-3
lines changed

4 files changed

+61
-3
lines changed

.README/rules/require-readonly-react-props.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### `require-readonly-react-props`
22

3-
This rule validates that React props are marked as $ReadOnly. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as $ReadOnly avoids these issues.
3+
This rule validates that React props are marked as `$ReadOnly`. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as `$ReadOnly` avoids these issues.
44

55
The rule tries its best to work with both class and functional components. For class components, it does a fuzzy check for one of "Component", "PureComponent", "React.Component" and "React.PureComponent". It doesn't actually infer that those identifiers resolve to a proper `React.Component` object.
66

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3451,7 +3451,7 @@ const f: fn = (a, b) => {}
34513451
<a name="eslint-plugin-flowtype-rules-require-readonly-react-props"></a>
34523452
### <code>require-readonly-react-props</code>
34533453
3454-
This rule validates that React props are marked as $ReadOnly. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as $ReadOnly avoids these issues.
3454+
This rule validates that React props are marked as `$ReadOnly`. React props are immutable and modifying them could lead to unexpected results. Marking prop shapes as `$ReadOnly` avoids these issues.
34553455
34563456
The rule tries its best to work with both class and functional components. For class components, it does a fuzzy check for one of "Component", "PureComponent", "React.Component" and "React.PureComponent". It doesn't actually infer that those identifiers resolve to a proper `React.Component` object.
34573457
@@ -3574,6 +3574,13 @@ export type Props = {}; class Foo extends Component<Props> { }
35743574
type Props = {| foo: string |}; class Foo extends Component<Props> { }
35753575
// Message: Props must be $ReadOnly
35763576

3577+
type Props = {| foo: string |} | {| bar: number |}; class Foo extends Component<Props> { }
3578+
// Message: Props must be $ReadOnly
3579+
3580+
// Options: [{"useImplicitExactTypes":true}]
3581+
type Props = { foo: string } | { bar: number }; class Foo extends Component<Props> { }
3582+
// Message: Props must be $ReadOnly
3583+
35773584
type Props = {| +foo: string, ...bar |}; class Foo extends Component<Props> { }
35783585
// Message: Props must be $ReadOnly
35793586

@@ -3622,6 +3629,11 @@ type Props = {| +foo: string |}; class Foo extends Component<Props> { }
36223629

36233630
type Props = {| +foo: string, +bar: number |}; class Foo extends Component<Props> { }
36243631

3632+
type Props = {| +foo: string |} | {| +bar: number |}; class Foo extends Component<Props> { }
3633+
3634+
// Options: [{"useImplicitExactTypes":true}]
3635+
type Props = { +foo: string } | { +bar: number }; class Foo extends Component<Props> { }
3636+
36253637
type Props = $FlowFixMe; class Foo extends Component<Props> { }
36263638

36273639
type Props = {||}; class Foo extends Component<Props> { }

src/rules/requireReadonlyReactProps.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const isReactComponent = (node) => {
3333
);
3434
};
3535

36+
// type Props = {| +foo: string |}
3637
const isReadOnlyObjectType = (node, {useImplicitExactTypes}) => {
3738
if (!node || node.type !== 'ObjectTypeAnnotation') {
3839
return false;
@@ -55,8 +56,21 @@ const isReadOnlyObjectType = (node, {useImplicitExactTypes}) => {
5556
});
5657
};
5758

59+
// type Props = {| +foo: string |} | {| +bar: number |}
60+
const isReadOnlyObjectUnionType = (node, options) => {
61+
if (!node || node.type !== 'UnionTypeAnnotation') {
62+
return false;
63+
}
64+
65+
return node.types.every((type) => {
66+
return isReadOnlyObjectType(type, options);
67+
});
68+
};
69+
5870
const isReadOnlyType = (node, options) => {
59-
return node.right.id && reReadOnly.test(node.right.id.name) || isReadOnlyObjectType(node.right, options);
71+
return node.right.id && reReadOnly.test(node.right.id.name) ||
72+
isReadOnlyObjectType(node.right, options) ||
73+
isReadOnlyObjectUnionType(node.right, options);
6074
};
6175

6276
const create = (context) => {

tests/rules/assertions/requireReadonlyReactProps.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,27 @@ export default {
6666
},
6767
],
6868
},
69+
{
70+
code: 'type Props = {| foo: string |} | {| bar: number |}; class Foo extends Component<Props> { }',
71+
errors: [
72+
{
73+
message: 'Props must be $ReadOnly',
74+
},
75+
],
76+
},
77+
{
78+
code: 'type Props = { foo: string } | { bar: number }; class Foo extends Component<Props> { }',
79+
errors: [
80+
{
81+
message: 'Props must be $ReadOnly',
82+
},
83+
],
84+
options: [
85+
{
86+
useImplicitExactTypes: true,
87+
},
88+
],
89+
},
6990
{
7091
code: 'type Props = {| +foo: string, ...bar |}; class Foo extends Component<Props> { }',
7192
errors: [
@@ -164,6 +185,17 @@ export default {
164185
{
165186
code: 'type Props = {| +foo: string, +bar: number |}; class Foo extends Component<Props> { }',
166187
},
188+
{
189+
code: 'type Props = {| +foo: string |} | {| +bar: number |}; class Foo extends Component<Props> { }',
190+
},
191+
{
192+
code: 'type Props = { +foo: string } | { +bar: number }; class Foo extends Component<Props> { }',
193+
options: [
194+
{
195+
useImplicitExactTypes: true,
196+
},
197+
],
198+
},
167199
{
168200
code: 'type Props = $FlowFixMe; class Foo extends Component<Props> { }',
169201
},

0 commit comments

Comments
 (0)