static-property-placement.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /**
  2. * @fileoverview Defines where React component static properties should be positioned.
  3. * @author Daniel Mason
  4. */
  5. 'use strict';
  6. const fromEntries = require('object.fromentries');
  7. const Components = require('../util/Components');
  8. const docsUrl = require('../util/docsUrl');
  9. const astUtil = require('../util/ast');
  10. const componentUtil = require('../util/componentUtil');
  11. const propsUtil = require('../util/props');
  12. const report = require('../util/report');
  13. const getScope = require('../util/eslint').getScope;
  14. // ------------------------------------------------------------------------------
  15. // Positioning Options
  16. // ------------------------------------------------------------------------------
  17. const STATIC_PUBLIC_FIELD = 'static public field';
  18. const STATIC_GETTER = 'static getter';
  19. const PROPERTY_ASSIGNMENT = 'property assignment';
  20. const POSITION_SETTINGS = [STATIC_PUBLIC_FIELD, STATIC_GETTER, PROPERTY_ASSIGNMENT];
  21. // ------------------------------------------------------------------------------
  22. // Rule messages
  23. // ------------------------------------------------------------------------------
  24. const ERROR_MESSAGES = {
  25. [STATIC_PUBLIC_FIELD]: 'notStaticClassProp',
  26. [STATIC_GETTER]: 'notGetterClassFunc',
  27. [PROPERTY_ASSIGNMENT]: 'declareOutsideClass',
  28. };
  29. // ------------------------------------------------------------------------------
  30. // Properties to check
  31. // ------------------------------------------------------------------------------
  32. const propertiesToCheck = {
  33. propTypes: propsUtil.isPropTypesDeclaration,
  34. defaultProps: propsUtil.isDefaultPropsDeclaration,
  35. childContextTypes: propsUtil.isChildContextTypesDeclaration,
  36. contextTypes: propsUtil.isContextTypesDeclaration,
  37. contextType: propsUtil.isContextTypeDeclaration,
  38. displayName: (node) => propsUtil.isDisplayNameDeclaration(astUtil.getPropertyNameNode(node)),
  39. };
  40. const classProperties = Object.keys(propertiesToCheck);
  41. const schemaProperties = fromEntries(classProperties.map((property) => [property, { enum: POSITION_SETTINGS }]));
  42. // ------------------------------------------------------------------------------
  43. // Rule Definition
  44. // ------------------------------------------------------------------------------
  45. const messages = {
  46. notStaticClassProp: '\'{{name}}\' should be declared as a static class property.',
  47. notGetterClassFunc: '\'{{name}}\' should be declared as a static getter class function.',
  48. declareOutsideClass: '\'{{name}}\' should be declared outside the class body.',
  49. };
  50. /** @type {import('eslint').Rule.RuleModule} */
  51. module.exports = {
  52. meta: {
  53. docs: {
  54. description: 'Enforces where React component static properties should be positioned.',
  55. category: 'Stylistic Issues',
  56. recommended: false,
  57. url: docsUrl('static-property-placement'),
  58. },
  59. fixable: null, // or 'code' or 'whitespace'
  60. messages,
  61. schema: [
  62. { enum: POSITION_SETTINGS },
  63. {
  64. type: 'object',
  65. properties: schemaProperties,
  66. additionalProperties: false,
  67. },
  68. ],
  69. },
  70. create: Components.detect((context, components, utils) => {
  71. // variables should be defined here
  72. const options = context.options;
  73. const defaultCheckType = options[0] || STATIC_PUBLIC_FIELD;
  74. const hasAdditionalConfig = options.length > 1;
  75. const additionalConfig = hasAdditionalConfig ? options[1] : {};
  76. // Set config
  77. const config = fromEntries(classProperties.map((property) => [
  78. property,
  79. additionalConfig[property] || defaultCheckType,
  80. ]));
  81. // ----------------------------------------------------------------------
  82. // Helpers
  83. // ----------------------------------------------------------------------
  84. /**
  85. * Checks if we are declaring context in class
  86. * @param {ASTNode} node
  87. * @returns {boolean} True if we are declaring context in class, false if not.
  88. */
  89. function isContextInClass(node) {
  90. let blockNode;
  91. let scope = getScope(context, node);
  92. while (scope) {
  93. blockNode = scope.block;
  94. if (blockNode && blockNode.type === 'ClassDeclaration') {
  95. return true;
  96. }
  97. scope = scope.upper;
  98. }
  99. return false;
  100. }
  101. /**
  102. * Check if we should report this property node
  103. * @param {ASTNode} node
  104. * @param {string} expectedRule
  105. */
  106. function reportNodeIncorrectlyPositioned(node, expectedRule) {
  107. // Detect if this node is an expected property declaration adn return the property name
  108. const name = classProperties.find((propertyName) => {
  109. if (propertiesToCheck[propertyName](node)) {
  110. return !!propertyName;
  111. }
  112. return false;
  113. });
  114. // If name is set but the configured rule does not match expected then report error
  115. if (
  116. name
  117. && (
  118. config[name] !== expectedRule
  119. || (!node.static && (config[name] === STATIC_PUBLIC_FIELD || config[name] === STATIC_GETTER))
  120. )
  121. ) {
  122. const messageId = ERROR_MESSAGES[config[name]];
  123. report(context, messages[messageId], messageId, {
  124. node,
  125. data: { name },
  126. });
  127. }
  128. }
  129. // ----------------------------------------------------------------------
  130. // Public
  131. // ----------------------------------------------------------------------
  132. return {
  133. 'ClassProperty, PropertyDefinition'(node) {
  134. if (!componentUtil.getParentES6Component(context, node)) {
  135. return;
  136. }
  137. reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD);
  138. },
  139. MemberExpression(node) {
  140. // If definition type is undefined then it must not be a defining expression or if the definition is inside a
  141. // class body then skip this node.
  142. const right = node.parent.right;
  143. if (!right || right.type === 'undefined' || isContextInClass(node)) {
  144. return;
  145. }
  146. // Get the related component
  147. const relatedComponent = utils.getRelatedComponent(node);
  148. // If the related component is not an ES6 component then skip this node
  149. if (!relatedComponent || !componentUtil.isES6Component(relatedComponent.node, context)) {
  150. return;
  151. }
  152. // Report if needed
  153. reportNodeIncorrectlyPositioned(node, PROPERTY_ASSIGNMENT);
  154. },
  155. MethodDefinition(node) {
  156. // If the function is inside a class and is static getter then check if correctly positioned
  157. if (
  158. componentUtil.getParentES6Component(context, node)
  159. && node.static
  160. && node.kind === 'get'
  161. ) {
  162. // Report error if needed
  163. reportNodeIncorrectlyPositioned(node, STATIC_GETTER);
  164. }
  165. },
  166. };
  167. }),
  168. };