From 712d840e0e72396a12a3e44021227f555306b75c Mon Sep 17 00:00:00 2001 From: Juriy Zaytsev Date: Thu, 23 May 2019 02:59:33 -0400 Subject: [PATCH] fix: requireReadOnlyReactProps (#406) * docs: add GitSpo mentions badge * docs: generate docs * fix: update GitSpo badge URL * docs: generate docs * Make be treated as * Add tests * Fix remaining tests * Fix whitespace --- README.md | 8 ++--- src/rules/requireReadonlyReactProps.js | 35 ++++++++++++------- .../assertions/requireReadonlyReactProps.js | 15 ++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5de10f32..b06b4503 100644 --- a/README.md +++ b/README.md @@ -1149,7 +1149,7 @@ import Foo from './foo'; // Message: Expected newline after flow annotation // Options: ["always-windows"] -// @flow +// @flow import Foo from './foo'; // Message: Expected newline after flow annotation @@ -1169,8 +1169,8 @@ The following patterns are not considered problems: import Foo from './foo'; // Options: ["always-windows"] -// @flow - +// @flow + import Foo from './foo'; // Options: ["never"] @@ -3701,7 +3701,7 @@ The following patterns are not considered problems: { a: string, b: number }) => {} // Options: ["always",{"allowLineBreak":true}] -(foo: +(foo: { a: string, b: number }) => {} // Options: ["never"] diff --git a/src/rules/requireReadonlyReactProps.js b/src/rules/requireReadonlyReactProps.js index c40bdfd0..f52ff92c 100644 --- a/src/rules/requireReadonlyReactProps.js +++ b/src/rules/requireReadonlyReactProps.js @@ -1,6 +1,7 @@ const schema = []; const reComponentName = /^(Pure)?Component$/; +const reReadOnly = /^\$(ReadOnly|FlowFixMe)$/; const isReactComponent = (node) => { if (!node.superClass) { @@ -25,23 +26,30 @@ const create = (context) => { const reportedFunctionalComponents = []; const isReadOnlyClassProp = (node) => { - return node.superTypeParameters.params[0].id && - node.superTypeParameters.params[0].id.name !== '$ReadOnly' && - !readOnlyTypes.includes(node.superTypeParameters.params[0].id.name); + const id = node.superTypeParameters.params[0].id; + + return id && !reReadOnly.test(id.name) && !readOnlyTypes.includes(id.name); }; const isReadOnlyObjectType = (node) => { - return node.type === 'TypeAlias' && - node.right && - node.right.type === 'ObjectTypeAnnotation' && - node.right.properties.length > 0 && - node.right.properties.every((prop) => { + 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; + } + + // { +foo: ..., +bar: ..., ... } + return node.properties.length > 0 && + node.properties.every((prop) => { return prop.variance && prop.variance.kind === 'plus'; }); }; const isReadOnlyType = (node) => { - return node.type === 'TypeAlias' && node.right.id && node.right.id.name === '$ReadOnly' || isReadOnlyObjectType(node); + return node.type === 'TypeAlias' && node.right.id && reReadOnly.test(node.right.id.name) || isReadOnlyObjectType(node.right); }; for (const node of context.getSourceCode().ast.body) { @@ -65,7 +73,9 @@ const create = (context) => { message: node.superTypeParameters.params[0].id.name + ' must be $ReadOnly', node }); - } else if (node.superTypeParameters && node.superTypeParameters.params[0].type === 'ObjectTypeAnnotation') { + } else if (node.superTypeParameters && + node.superTypeParameters.params[0].type === 'ObjectTypeAnnotation' && + !isReadOnlyObjectType(node.superTypeParameters.params[0])) { context.report({ message: node.id.name + ' class props must be $ReadOnly', node @@ -92,7 +102,7 @@ const create = (context) => { (typeAnnotation = currentNode.params[0].typeAnnotation)) { if ((identifier = typeAnnotation.typeAnnotation.id) && !readOnlyTypes.includes(identifier.name) && - identifier.name !== '$ReadOnly') { + !reReadOnly.test(identifier.name)) { if (reportedFunctionalComponents.includes(identifier)) { return; } @@ -107,7 +117,8 @@ const create = (context) => { return; } - if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation') { + if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation' && + !isReadOnlyObjectType(typeAnnotation.typeAnnotation)) { 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 9cd62284..d3685a46 100644 --- a/tests/rules/assertions/requireReadonlyReactProps.js +++ b/tests/rules/assertions/requireReadonlyReactProps.js @@ -175,6 +175,15 @@ export default { { code: 'type Props = {| +foo: string, +bar: number |}; class Foo extends Component { }' }, + { + code: 'type Props = $FlowFixMe; class Foo extends Component { }' + }, + { + code: 'type Props = {||}; class Foo extends Component { }' + }, + { + code: 'class Foo extends Component<{||}> { }' + }, // functional components { @@ -188,6 +197,12 @@ export default { }, { code: 'function Foo() { return

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

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

}' } ] };