You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

182 lines
6.2 KiB

2 years ago
/**
* @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 componentUtil = require('../util/componentUtil');
const propsUtil = require('../util/props');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// 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
// ------------------------------------------------------------------------------
const 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.',
};
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,
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) {
const messageId = ERROR_MESSAGES[config[name]];
report(context, messages[messageId], messageId, {
node,
data: { name },
});
}
}
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
return {
'ClassProperty, PropertyDefinition'(node) {
if (!componentUtil.getParentES6Component(context)) {
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 || !componentUtil.isES6Component(relatedComponent.node, context)) {
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 (componentUtil.getParentES6Component(context) && node.static && node.kind === 'get') {
// Report error if needed
reportNodeIncorrectlyPositioned(node, STATIC_GETTER);
}
},
};
}),
};