static-property-placement.js 6.4 KB

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