requireReadonlyReactProps.js.flow 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import _ from 'lodash';
  2. const schema = [
  3. {
  4. additionalProperties: false,
  5. properties: {
  6. useImplicitExactTypes: {
  7. type: 'boolean',
  8. },
  9. },
  10. type: 'object',
  11. },
  12. ];
  13. const reComponentName = /^(Pure)?Component$/u;
  14. const reReadOnly = /^\$(ReadOnly|FlowFixMe)$/u;
  15. const isReactComponent = (node) => {
  16. if (!node.superClass) {
  17. return false;
  18. }
  19. return (
  20. // class Foo extends Component { }
  21. // class Foo extends PureComponent { }
  22. (node.superClass.type === 'Identifier' && reComponentName.test(node.superClass.name))
  23. // class Foo extends React.Component { }
  24. // class Foo extends React.PureComponent { }
  25. || (node.superClass.type === 'MemberExpression'
  26. && (node.superClass.object.name === 'React' && reComponentName.test(node.superClass.property.name)))
  27. );
  28. };
  29. // type Props = {| +foo: string |}
  30. const isReadOnlyObjectType = (node, { useImplicitExactTypes }) => {
  31. if (!node || node.type !== 'ObjectTypeAnnotation') {
  32. return false;
  33. }
  34. if (node.properties.length === 0) {
  35. // we consider `{}` to be ReadOnly since it's exact
  36. // AND has no props (when `implicitExactTypes=true`)
  37. // we consider `{||}` to be ReadOnly since it's exact
  38. // AND has no props (when `implicitExactTypes=false`)
  39. if (useImplicitExactTypes === true && node.exact === false) {
  40. return true;
  41. }
  42. if (node.exact === true) {
  43. return true;
  44. }
  45. }
  46. // { +foo: ..., +bar: ..., ... }
  47. return node.properties.length > 0
  48. && node.properties.every((prop) => prop.variance && prop.variance.kind === 'plus');
  49. };
  50. // type Props = {| +foo: string |} | {| +bar: number |}
  51. const isReadOnlyObjectUnionType = (node, options) => {
  52. if (!node || node.type !== 'UnionTypeAnnotation') {
  53. return false;
  54. }
  55. return node.types.every((type) => isReadOnlyObjectType(type, options));
  56. };
  57. const isReadOnlyType = (node, options) => (
  58. (node.right.id && reReadOnly.test(node.right.id.name))
  59. || isReadOnlyObjectType(node.right, options)
  60. || isReadOnlyObjectUnionType(node.right, options)
  61. );
  62. const create = (context) => {
  63. const useImplicitExactTypes = _.get(context, ['options', 0, 'useImplicitExactTypes'], false);
  64. const options = { useImplicitExactTypes };
  65. const readOnlyTypes = [];
  66. const foundTypes = [];
  67. const reportedFunctionalComponents = [];
  68. const isReadOnlyClassProp = (node) => {
  69. const id = node.superTypeParameters && node.superTypeParameters.params[0].id;
  70. return (
  71. id
  72. && !reReadOnly.test(id.name)
  73. && !readOnlyTypes.includes(id.name)
  74. && foundTypes.includes(id.name)
  75. );
  76. };
  77. for (const node of context.getSourceCode().ast.body) {
  78. let idName;
  79. let typeNode;
  80. // type Props = $ReadOnly<{}>
  81. if (node.type === 'TypeAlias') {
  82. idName = node.id.name;
  83. typeNode = node;
  84. // export type Props = $ReadOnly<{}>
  85. } else if (node.type === 'ExportNamedDeclaration'
  86. && node.declaration
  87. && node.declaration.type === 'TypeAlias') {
  88. idName = node.declaration.id.name;
  89. typeNode = node.declaration;
  90. }
  91. if (idName) {
  92. foundTypes.push(idName);
  93. if (isReadOnlyType(typeNode, options)) {
  94. readOnlyTypes.push(idName);
  95. }
  96. }
  97. }
  98. return {
  99. // class components
  100. ClassDeclaration(node) {
  101. if (isReactComponent(node) && isReadOnlyClassProp(node)) {
  102. context.report({
  103. message: `${node.superTypeParameters.params[0].id.name} must be $ReadOnly`,
  104. node,
  105. });
  106. } else if (node.superTypeParameters
  107. && node.superTypeParameters.params[0].type === 'ObjectTypeAnnotation'
  108. && !isReadOnlyObjectType(node.superTypeParameters.params[0], options)) {
  109. context.report({
  110. message: `${node.id.name} class props must be $ReadOnly`,
  111. node,
  112. });
  113. }
  114. },
  115. // functional components
  116. JSXElement(node) {
  117. let currentNode = node;
  118. while (currentNode && currentNode.type !== 'FunctionDeclaration') {
  119. currentNode = currentNode.parent;
  120. }
  121. // functional components can only have 1 param
  122. if (!currentNode || currentNode.params.length !== 1) {
  123. return;
  124. }
  125. const { typeAnnotation } = currentNode.params[0];
  126. if (currentNode.params[0].type === 'Identifier' && typeAnnotation) {
  127. const identifier = typeAnnotation.typeAnnotation.id;
  128. if (identifier
  129. && foundTypes.includes(identifier.name)
  130. && !readOnlyTypes.includes(identifier.name)
  131. && !reReadOnly.test(identifier.name)) {
  132. if (reportedFunctionalComponents.includes(identifier)) {
  133. return;
  134. }
  135. context.report({
  136. message: `${identifier.name} must be $ReadOnly`,
  137. node: identifier,
  138. });
  139. reportedFunctionalComponents.push(identifier);
  140. return;
  141. }
  142. if (typeAnnotation.typeAnnotation.type === 'ObjectTypeAnnotation'
  143. && !isReadOnlyObjectType(typeAnnotation.typeAnnotation, options)) {
  144. context.report({
  145. message: `${currentNode.id.name} component props must be $ReadOnly`,
  146. node,
  147. });
  148. }
  149. }
  150. },
  151. };
  152. };
  153. export default {
  154. create,
  155. schema,
  156. };