prefer-exact-props.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /**
  2. * @fileoverview Prefer exact proptype definitions
  3. */
  4. 'use strict';
  5. const Components = require('../util/Components');
  6. const docsUrl = require('../util/docsUrl');
  7. const astUtil = require('../util/ast');
  8. const propsUtil = require('../util/props');
  9. const propWrapperUtil = require('../util/propWrapper');
  10. const variableUtil = require('../util/variable');
  11. const report = require('../util/report');
  12. const getText = require('../util/eslint').getText;
  13. // -----------------------------------------------------------------------------
  14. // Rule Definition
  15. // -----------------------------------------------------------------------------
  16. const messages = {
  17. propTypes: 'Component propTypes should be exact by using {{exactPropWrappers}}.',
  18. flow: 'Component flow props should be set with exact objects.',
  19. };
  20. /** @type {import('eslint').Rule.RuleModule} */
  21. module.exports = {
  22. meta: {
  23. docs: {
  24. description: 'Prefer exact proptype definitions',
  25. category: 'Possible Errors',
  26. recommended: false,
  27. url: docsUrl('prefer-exact-props'),
  28. },
  29. messages,
  30. schema: [],
  31. },
  32. create: Components.detect((context, components, utils) => {
  33. const typeAliases = {};
  34. const exactWrappers = propWrapperUtil.getExactPropWrapperFunctions(context);
  35. function getPropTypesErrorMessage() {
  36. const formattedWrappers = propWrapperUtil.formatPropWrapperFunctions(exactWrappers);
  37. const message = exactWrappers.size > 1 ? `one of ${formattedWrappers}` : formattedWrappers;
  38. return { exactPropWrappers: message };
  39. }
  40. function isNonExactObjectTypeAnnotation(node) {
  41. return (
  42. node
  43. && node.type === 'ObjectTypeAnnotation'
  44. && node.properties.length > 0
  45. && !node.exact
  46. );
  47. }
  48. function hasNonExactObjectTypeAnnotation(node) {
  49. const typeAnnotation = node.typeAnnotation;
  50. return (
  51. typeAnnotation
  52. && typeAnnotation.typeAnnotation
  53. && isNonExactObjectTypeAnnotation(typeAnnotation.typeAnnotation)
  54. );
  55. }
  56. function hasGenericTypeAnnotation(node) {
  57. const typeAnnotation = node.typeAnnotation;
  58. return (
  59. typeAnnotation
  60. && typeAnnotation.typeAnnotation
  61. && typeAnnotation.typeAnnotation.type === 'GenericTypeAnnotation'
  62. );
  63. }
  64. function isNonEmptyObjectExpression(node) {
  65. return (
  66. node
  67. && node.type === 'ObjectExpression'
  68. && node.properties.length > 0
  69. );
  70. }
  71. function isNonExactPropWrapperFunction(node) {
  72. return (
  73. astUtil.isCallExpression(node)
  74. && !propWrapperUtil.isExactPropWrapperFunction(context, getText(context, node.callee))
  75. );
  76. }
  77. function reportPropTypesError(node) {
  78. report(context, messages.propTypes, 'propTypes', {
  79. node,
  80. data: getPropTypesErrorMessage(),
  81. });
  82. }
  83. function reportFlowError(node) {
  84. report(context, messages.flow, 'flow', {
  85. node,
  86. });
  87. }
  88. return {
  89. TypeAlias(node) {
  90. // working around an issue with eslint@3 and babel-eslint not finding the TypeAlias in scope
  91. typeAliases[node.id.name] = node;
  92. },
  93. 'ClassProperty, PropertyDefinition'(node) {
  94. if (!propsUtil.isPropTypesDeclaration(node)) {
  95. return;
  96. }
  97. if (hasNonExactObjectTypeAnnotation(node)) {
  98. reportFlowError(node);
  99. } else if (exactWrappers.size > 0 && isNonEmptyObjectExpression(node.value)) {
  100. reportPropTypesError(node);
  101. } else if (exactWrappers.size > 0 && isNonExactPropWrapperFunction(node.value)) {
  102. reportPropTypesError(node);
  103. }
  104. },
  105. Identifier(node) {
  106. if (!utils.getStatelessComponent(node.parent)) {
  107. return;
  108. }
  109. if (hasNonExactObjectTypeAnnotation(node)) {
  110. reportFlowError(node);
  111. } else if (hasGenericTypeAnnotation(node)) {
  112. const identifier = node.typeAnnotation.typeAnnotation.id.name;
  113. const typeAlias = typeAliases[identifier];
  114. const propsDefinition = typeAlias ? typeAlias.right : null;
  115. if (isNonExactObjectTypeAnnotation(propsDefinition)) {
  116. reportFlowError(node);
  117. }
  118. }
  119. },
  120. MemberExpression(node) {
  121. if (!propsUtil.isPropTypesDeclaration(node) || exactWrappers.size === 0) {
  122. return;
  123. }
  124. const right = node.parent.right;
  125. if (isNonEmptyObjectExpression(right)) {
  126. reportPropTypesError(node);
  127. } else if (isNonExactPropWrapperFunction(right)) {
  128. reportPropTypesError(node);
  129. } else if (right.type === 'Identifier') {
  130. const identifier = right.name;
  131. const propsDefinition = variableUtil.findVariableByName(context, node, identifier);
  132. if (isNonEmptyObjectExpression(propsDefinition)) {
  133. reportPropTypesError(node);
  134. } else if (isNonExactPropWrapperFunction(propsDefinition)) {
  135. reportPropTypesError(node);
  136. }
  137. }
  138. },
  139. };
  140. }),
  141. };