From 6d5362bf2e657066487687ee3d35a8363afcab97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Zl=C3=A1mal?= Date: Mon, 5 Apr 2021 22:37:51 +0200 Subject: [PATCH] feat: add support for implicit exact Flow types (#471) 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: https://github.com/gajus/eslint-plugin-flowtype/issues/467 --- .README/rules/require-readonly-react-props.md | 17 +++++++ README.md | 35 ++++++++++++++ src/rules/requireReadonlyReactProps.js | 40 ++++++++++++---- .../assertions/requireReadonlyReactProps.js | 48 +++++++++++++++++++ 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/.README/rules/require-readonly-react-props.md b/.README/rules/require-readonly-react-props.md index 68220025..2ccf54fe 100644 --- a/.README/rules/require-readonly-react-props.md +++ b/.README/rules/require-readonly-react-props.md @@ -79,4 +79,21 @@ class Bar extends React.Component { } ``` +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 + } + ] + } +} +``` + + diff --git a/README.md b/README.md index 8cdf81a6..9b2f188f 100644 --- a/README.md +++ b/README.md @@ -3461,6 +3461,23 @@ class Bar extends React.Component { } ``` +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 @@ -3540,8 +3557,20 @@ type Props = $FlowFixMe; class Foo extends Component { } type Props = {||}; class Foo extends Component { } +// Options: [{"useImplicitExactTypes":true}] +type Props = {||}; class Foo extends Component { } + +// Options: [{"useImplicitExactTypes":true}] +type Props = {}; class Foo extends Component { } + +class Foo extends Component<{||}> { } + +// Options: [{"useImplicitExactTypes":true}] class Foo extends Component<{||}> { } +// Options: [{"useImplicitExactTypes":true}] +class Foo extends Component<{}> { } + class Foo extends React.Component { } import { type Props } from "file"; class Foo extends React.Component { } @@ -3557,6 +3586,12 @@ function Foo() { return

} function Foo(props: $FlowFixMe) { return

} function Foo(props: {||}) { return

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

} + +// Options: [{"useImplicitExactTypes":true}] +function Foo(props: {}) { return

} ``` diff --git a/src/rules/requireReadonlyReactProps.js b/src/rules/requireReadonlyReactProps.js index 978861b3..ab123b01 100644 --- a/src/rules/requireReadonlyReactProps.js +++ b/src/rules/requireReadonlyReactProps.js @@ -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)$/; @@ -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: ..., ... } @@ -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 = []; @@ -72,7 +92,7 @@ const create = (context) => { if (idName) { foundTypes.push(idName); - if (isReadOnlyType(typeNode)) { + if (isReadOnlyType(typeNode, options)) { readOnlyTypes.push(idName); } } @@ -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, @@ -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, diff --git a/tests/rules/assertions/requireReadonlyReactProps.js b/tests/rules/assertions/requireReadonlyReactProps.js index 5ddf61e3..fec66102 100644 --- a/tests/rules/assertions/requireReadonlyReactProps.js +++ b/tests/rules/assertions/requireReadonlyReactProps.js @@ -165,8 +165,40 @@ export default { { code: 'type Props = {||}; class Foo extends Component { }', }, + { + code: 'type Props = {||}; class Foo extends Component { }', + options: [ + { + useImplicitExactTypes: true, + }, + ], + }, + { + code: 'type Props = {}; class Foo extends Component { }', + 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 { }', @@ -194,5 +226,21 @@ export default { { code: 'function Foo(props: {||}) { return

}', }, + { + code: 'function Foo(props: {||}) { return

}', + options: [ + { + useImplicitExactTypes: true, + }, + ], + }, + { + code: 'function Foo(props: {}) { return

}', + options: [ + { + useImplicitExactTypes: true, + }, + ], + }, ], };