Skip to content

Commit

Permalink
feat: add support for implicit exact Flow types (#471)
Browse files Browse the repository at this point in the history
This commit adds a support for implicit exact Flow types. This is especially useful when using `exact_by_default=true` Flow option and migrating towards `{ }` and `{ ... }` syntax (Flow standard is now to use `{ }` for strict objects and `{ key: value, ... }` for open objects).

Closes: #467
  • Loading branch information
mrtnzlml committed Apr 5, 2021
1 parent 8fd60d6 commit 6d5362b
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 10 deletions.
17 changes: 17 additions & 0 deletions .README/rules/require-readonly-react-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,21 @@ class Bar extends React.Component<Props> { }
```


Optionally, you can enable support for [implicit exact Flow types](https://medium.com/flow-type/on-the-roadmap-exact-objects-by-default-16b72933c5cf) (useful when using `exact_by_default=true` Flow option):


```js
{
"rules": {
"flowtype/require-readonly-react-props": [
2,
{
"useImplicitExactTypes": true
}
]
}
}
```


<!-- assertions requireReadonlyReactProps -->
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3461,6 +3461,23 @@ class Bar extends React.Component<Props> { }
```
Optionally, you can enable support for [implicit exact Flow types](https://medium.com/flow-type/on-the-roadmap-exact-objects-by-default-16b72933c5cf) (useful when using `exact_by_default=true` Flow option):
```js
{
"rules": {
"flowtype/require-readonly-react-props": [
2,
{
"useImplicitExactTypes": true
}
]
}
}
```
The following patterns are considered problems:
```js
Expand Down Expand Up @@ -3540,8 +3557,20 @@ type Props = $FlowFixMe; class Foo extends Component<Props> { }

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

// Options: [{"useImplicitExactTypes":true}]
type Props = {||}; class Foo extends Component<Props> { }

// Options: [{"useImplicitExactTypes":true}]
type Props = {}; class Foo extends Component<Props> { }

class Foo extends Component<{||}> { }

// Options: [{"useImplicitExactTypes":true}]
class Foo extends Component<{||}> { }

// Options: [{"useImplicitExactTypes":true}]
class Foo extends Component<{}> { }

class Foo extends React.Component<UnknownProps> { }

import { type Props } from "file"; class Foo extends React.Component<Props> { }
Expand All @@ -3557,6 +3586,12 @@ function Foo() { return <p /> }
function Foo(props: $FlowFixMe) { return <p /> }

function Foo(props: {||}) { return <p /> }

// Options: [{"useImplicitExactTypes":true}]
function Foo(props: {||}) { return <p /> }

// Options: [{"useImplicitExactTypes":true}]
function Foo(props: {}) { return <p /> }
```
Expand Down
40 changes: 30 additions & 10 deletions src/rules/requireReadonlyReactProps.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
const schema = [];
import _ from 'lodash';

const schema = [
{
additionalProperties: false,
properties: {
useImplicitExactTypes: {
type: 'boolean',
},
},
type: 'object',
},
];

const reComponentName = /^(Pure)?Component$/;
const reReadOnly = /^\$(ReadOnly|FlowFixMe)$/;
Expand All @@ -21,14 +33,19 @@ const isReactComponent = (node) => {
);
};

const isReadOnlyObjectType = (node) => {
const isReadOnlyObjectType = (node, {useImplicitExactTypes}) => {
if (!node || node.type !== 'ObjectTypeAnnotation') {
return false;
}

// we consider `{||}` to be ReadOnly since it's exact AND has no props
if (node.exact && node.properties.length === 0) {
return true;
if (node.properties.length === 0) {
// we consider `{}` to be ReadOnly since it's exact AND has no props (when `implicitExactTypes=true`)
// we consider `{||}` to be ReadOnly since it's exact AND has no props (when `implicitExactTypes=false`)
if (useImplicitExactTypes === true && node.exact === false) {
return true;
} else if (node.exact === true) {
return true;
}
}

// { +foo: ..., +bar: ..., ... }
Expand All @@ -38,11 +55,14 @@ const isReadOnlyObjectType = (node) => {
});
};

const isReadOnlyType = (node) => {
return node.right.id && reReadOnly.test(node.right.id.name) || isReadOnlyObjectType(node.right);
const isReadOnlyType = (node, options) => {
return node.right.id && reReadOnly.test(node.right.id.name) || isReadOnlyObjectType(node.right, options);
};

const create = (context) => {
const useImplicitExactTypes = _.get(context, ['options', 0, 'useImplicitExactTypes'], false);
const options = {useImplicitExactTypes};

const readOnlyTypes = [];
const foundTypes = [];
const reportedFunctionalComponents = [];
Expand Down Expand Up @@ -72,7 +92,7 @@ const create = (context) => {

if (idName) {
foundTypes.push(idName);
if (isReadOnlyType(typeNode)) {
if (isReadOnlyType(typeNode, options)) {
readOnlyTypes.push(idName);
}
}
Expand All @@ -89,7 +109,7 @@ const create = (context) => {
});
} else if (node.superTypeParameters &&
node.superTypeParameters.params[0].type === 'ObjectTypeAnnotation' &&
!isReadOnlyObjectType(node.superTypeParameters.params[0])) {
!isReadOnlyObjectType(node.superTypeParameters.params[0], options)) {
context.report({
message: node.id.name + ' class props must be $ReadOnly',
node,
Expand Down Expand Up @@ -133,7 +153,7 @@ const create = (context) => {
}

if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation' &&
!isReadOnlyObjectType(typeAnnotation.typeAnnotation)) {
!isReadOnlyObjectType(typeAnnotation.typeAnnotation, options)) {
context.report({
message: currentNode.id.name + ' component props must be $ReadOnly',
node,
Expand Down
48 changes: 48 additions & 0 deletions tests/rules/assertions/requireReadonlyReactProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,40 @@ export default {
{
code: 'type Props = {||}; class Foo extends Component<Props> { }',
},
{
code: 'type Props = {||}; class Foo extends Component<Props> { }',
options: [
{
useImplicitExactTypes: true,
},
],
},
{
code: 'type Props = {}; class Foo extends Component<Props> { }',
options: [
{
useImplicitExactTypes: true,
},
],
},
{
code: 'class Foo extends Component<{||}> { }',
},
{
code: 'class Foo extends Component<{||}> { }',
options: [
{
useImplicitExactTypes: true,
},
],
},
{
code: 'class Foo extends Component<{}> { }',
options: [
{
useImplicitExactTypes: true,
},
],
},
{
code: 'class Foo extends React.Component<UnknownProps> { }',
Expand Down Expand Up @@ -194,5 +226,21 @@ export default {
{
code: 'function Foo(props: {||}) { return <p /> }',
},
{
code: 'function Foo(props: {||}) { return <p /> }',
options: [
{
useImplicitExactTypes: true,
},
],
},
{
code: 'function Foo(props: {}) { return <p /> }',
options: [
{
useImplicitExactTypes: true,
},
],
},
],
};

0 comments on commit 6d5362b

Please sign in to comment.