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.
181 lines
6.2 KiB
181 lines
6.2 KiB
/** |
|
* @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); |
|
} |
|
}, |
|
}; |
|
}), |
|
};
|
|
|