static-property-placement.js 6.1 KB

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