|
|
/** * @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]: 'notStaticClassProp', [STATIC_GETTER]: 'notGetterClassFunc', [PROPERTY_ASSIGNMENT]: 'declareOutsideClass' };
// ------------------------------------------------------------------------------
// 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'
messages: { notStaticClassProp: '\'{{name}}\' should be declared as a static class property.', notGetterClassFunc: '\'{{name}}\' should be declared as a static getter class function.', declareOutsideClass: '\'{{name}}\' should be declared outside the class body.' },
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, messageId: ERROR_MESSAGES[config[name]], data: {name} }); } }
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return { ClassProperty: (node) => { if (!utils.getParentES6Component()) { return; }
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 (utils.getParentES6Component() && node.static && node.kind === 'get') { // Report error if needed
reportNodeIncorrectlyPositioned(node, STATIC_GETTER); } } }; }) };
|