166 lines
5.8 KiB
JavaScript
166 lines
5.8 KiB
JavaScript
/**
|
|
* @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);
|
|
}
|
|
}
|
|
};
|
|
})
|
|
};
|