UNPKG

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