| /** |
| * @fileoverview Defines where React component static properties should be positioned. |
| * @author Daniel Mason |
| */ |
| |
| 'use strict'; |
| |
| const fromEntries = require('object.fromentries'); |
| const Components = require('../util/Components'); |
| const docsUrl = require('../util/docsUrl'); |
| const astUtil = require('../util/ast'); |
| const propsUtil = require('../util/props'); |
| |
| // ------------------------------------------------------------------------------ |
| // Positioning Options |
| // ------------------------------------------------------------------------------ |
| const STATIC_PUBLIC_FIELD = 'static public field'; |
| const STATIC_GETTER = 'static getter'; |
| const PROPERTY_ASSIGNMENT = 'property assignment'; |
| const POSITION_SETTINGS = [STATIC_PUBLIC_FIELD, STATIC_GETTER, PROPERTY_ASSIGNMENT]; |
| |
| // ------------------------------------------------------------------------------ |
| // Rule messages |
| // ------------------------------------------------------------------------------ |
| const ERROR_MESSAGES = { |
| [STATIC_PUBLIC_FIELD]: '\'{{name}}\' should be declared as a static class property.', |
| [STATIC_GETTER]: '\'{{name}}\' should be declared as a static getter class function.', |
| [PROPERTY_ASSIGNMENT]: '\'{{name}}\' should be declared outside the class body.' |
| }; |
| |
| // ------------------------------------------------------------------------------ |
| // Properties to check |
| // ------------------------------------------------------------------------------ |
| const propertiesToCheck = { |
| propTypes: propsUtil.isPropTypesDeclaration, |
| defaultProps: propsUtil.isDefaultPropsDeclaration, |
| childContextTypes: propsUtil.isChildContextTypesDeclaration, |
| contextTypes: propsUtil.isContextTypesDeclaration, |
| contextType: propsUtil.isContextTypeDeclaration, |
| displayName: node => propsUtil.isDisplayNameDeclaration(astUtil.getPropertyNameNode(node)) |
| }; |
| |
| const classProperties = Object.keys(propertiesToCheck); |
| const schemaProperties = fromEntries(classProperties.map(property => [property, {enum: POSITION_SETTINGS}])); |
| |
| // ------------------------------------------------------------------------------ |
| // Rule Definition |
| // ------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| docs: { |
| description: 'Defines where React component static properties should be positioned.', |
| category: 'Stylistic Issues', |
| recommended: false, |
| url: docsUrl('static-property-placement') |
| }, |
| fixable: null, // or 'code' or 'whitespace' |
| schema: [ |
| {enum: POSITION_SETTINGS}, |
| { |
| type: 'object', |
| properties: schemaProperties, |
| additionalProperties: false |
| } |
| ] |
| }, |
| |
| create: Components.detect((context, components, utils) => { |
| // variables should be defined here |
| const options = context.options; |
| const defaultCheckType = options[0] || STATIC_PUBLIC_FIELD; |
| const hasAdditionalConfig = options.length > 1; |
| const additionalConfig = hasAdditionalConfig ? options[1] : {}; |
| |
| // Set config |
| const config = fromEntries(classProperties.map(property => [ |
| property, |
| additionalConfig[property] || defaultCheckType |
| ])); |
| |
| // ---------------------------------------------------------------------- |
| // Helpers |
| // ---------------------------------------------------------------------- |
| |
| /** |
| * Checks if we are declaring context in class |
| * @returns {Boolean} True if we are declaring context in class, false if not. |
| */ |
| function isContextInClass() { |
| let blockNode; |
| let scope = context.getScope(); |
| while (scope) { |
| blockNode = scope.block; |
| if (blockNode && blockNode.type === 'ClassDeclaration') { |
| return true; |
| } |
| scope = scope.upper; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Check if we should report this property node |
| * @param {ASTNode} node |
| * @param {string} expectedRule |
| */ |
| function reportNodeIncorrectlyPositioned(node, expectedRule) { |
| // Detect if this node is an expected property declaration adn return the property name |
| const name = classProperties.find((propertyName) => { |
| if (propertiesToCheck[propertyName](node)) { |
| return !!propertyName; |
| } |
| |
| return false; |
| }); |
| |
| // If name is set but the configured rule does not match expected then report error |
| if (name && config[name] !== expectedRule) { |
| // Report the error |
| context.report({ |
| node, |
| message: ERROR_MESSAGES[config[name]], |
| data: {name} |
| }); |
| } |
| } |
| |
| // ---------------------------------------------------------------------- |
| // Public |
| // ---------------------------------------------------------------------- |
| return { |
| ClassProperty: node => reportNodeIncorrectlyPositioned(node, STATIC_PUBLIC_FIELD), |
| |
| MemberExpression: (node) => { |
| // If definition type is undefined then it must not be a defining expression or if the definition is inside a |
| // class body then skip this node. |
| const right = node.parent.right; |
| if (!right || right.type === 'undefined' || isContextInClass()) { |
| return; |
| } |
| |
| // Get the related component |
| const relatedComponent = utils.getRelatedComponent(node); |
| |
| // If the related component is not an ES6 component then skip this node |
| if (!relatedComponent || !utils.isES6Component(relatedComponent.node)) { |
| return; |
| } |
| |
| // Report if needed |
| reportNodeIncorrectlyPositioned(node, PROPERTY_ASSIGNMENT); |
| }, |
| |
| MethodDefinition: (node) => { |
| // If the function is inside a class and is static getter then check if correctly positioned |
| if (isContextInClass() && node.static && node.kind === 'get') { |
| // Report error if needed |
| reportNodeIncorrectlyPositioned(node, STATIC_GETTER); |
| } |
| } |
| }; |
| }) |
| }; |