| /** |
| * @fileoverview Enforce component methods order |
| * @author Yannick Croissant |
| */ |
| |
| 'use strict'; |
| |
| const has = require('has'); |
| const entries = require('object.entries'); |
| const arrayIncludes = require('array-includes'); |
| |
| const Components = require('../util/Components'); |
| const astUtil = require('../util/ast'); |
| const docsUrl = require('../util/docsUrl'); |
| |
| const defaultConfig = { |
| order: [ |
| 'static-methods', |
| 'lifecycle', |
| 'everything-else', |
| 'render' |
| ], |
| groups: { |
| lifecycle: [ |
| 'displayName', |
| 'propTypes', |
| 'contextTypes', |
| 'childContextTypes', |
| 'mixins', |
| 'statics', |
| 'defaultProps', |
| 'constructor', |
| 'getDefaultProps', |
| 'state', |
| 'getInitialState', |
| 'getChildContext', |
| 'getDerivedStateFromProps', |
| 'componentWillMount', |
| 'UNSAFE_componentWillMount', |
| 'componentDidMount', |
| 'componentWillReceiveProps', |
| 'UNSAFE_componentWillReceiveProps', |
| 'shouldComponentUpdate', |
| 'componentWillUpdate', |
| 'UNSAFE_componentWillUpdate', |
| 'getSnapshotBeforeUpdate', |
| 'componentDidUpdate', |
| 'componentDidCatch', |
| 'componentWillUnmount' |
| ] |
| } |
| }; |
| |
| /** |
| * Get the methods order from the default config and the user config |
| * @param {Object} userConfig The user configuration. |
| * @returns {Array} Methods order |
| */ |
| function getMethodsOrder(userConfig) { |
| userConfig = userConfig || {}; |
| |
| const groups = Object.assign({}, defaultConfig.groups, userConfig.groups); |
| const order = userConfig.order || defaultConfig.order; |
| |
| let config = []; |
| let entry; |
| for (let i = 0, j = order.length; i < j; i++) { |
| entry = order[i]; |
| if (has(groups, entry)) { |
| config = config.concat(groups[entry]); |
| } else { |
| config.push(entry); |
| } |
| } |
| |
| return config; |
| } |
| |
| // ------------------------------------------------------------------------------ |
| // Rule Definition |
| // ------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| docs: { |
| description: 'Enforce component methods order', |
| category: 'Stylistic Issues', |
| recommended: false, |
| url: docsUrl('sort-comp') |
| }, |
| |
| schema: [{ |
| type: 'object', |
| properties: { |
| order: { |
| type: 'array', |
| items: { |
| type: 'string' |
| } |
| }, |
| groups: { |
| type: 'object', |
| patternProperties: { |
| '^.*$': { |
| type: 'array', |
| items: { |
| type: 'string' |
| } |
| } |
| } |
| } |
| }, |
| additionalProperties: false |
| }] |
| }, |
| |
| create: Components.detect((context, components) => { |
| const errors = {}; |
| |
| const MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}'; |
| |
| const methodsOrder = getMethodsOrder(context.options[0]); |
| |
| // -------------------------------------------------------------------------- |
| // Public |
| // -------------------------------------------------------------------------- |
| |
| const regExpRegExp = /\/(.*)\/([g|y|i|m]*)/; |
| |
| /** |
| * Get indexes of the matching patterns in methods order configuration |
| * @param {Object} method - Method metadata. |
| * @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match. |
| */ |
| function getRefPropIndexes(method) { |
| const methodGroupIndexes = []; |
| |
| methodsOrder.forEach((currentGroup, groupIndex) => { |
| if (currentGroup === 'getters') { |
| if (method.getter) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === 'setters') { |
| if (method.setter) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === 'type-annotations') { |
| if (method.typeAnnotation) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === 'static-methods') { |
| if (method.static) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === 'instance-variables') { |
| if (method.instanceVariable) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === 'instance-methods') { |
| if (method.instanceMethod) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (arrayIncludes([ |
| 'displayName', |
| 'propTypes', |
| 'contextTypes', |
| 'childContextTypes', |
| 'mixins', |
| 'statics', |
| 'defaultProps', |
| 'constructor', |
| 'getDefaultProps', |
| 'state', |
| 'getInitialState', |
| 'getChildContext', |
| 'getDerivedStateFromProps', |
| 'componentWillMount', |
| 'UNSAFE_componentWillMount', |
| 'componentDidMount', |
| 'componentWillReceiveProps', |
| 'UNSAFE_componentWillReceiveProps', |
| 'shouldComponentUpdate', |
| 'componentWillUpdate', |
| 'UNSAFE_componentWillUpdate', |
| 'getSnapshotBeforeUpdate', |
| 'componentDidUpdate', |
| 'componentDidCatch', |
| 'componentWillUnmount', |
| 'render' |
| ], currentGroup)) { |
| if (currentGroup === method.name) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else { |
| // Is the group a regex? |
| const isRegExp = currentGroup.match(regExpRegExp); |
| if (isRegExp) { |
| const isMatching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name); |
| if (isMatching) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } else if (currentGroup === method.name) { |
| methodGroupIndexes.push(groupIndex); |
| } |
| } |
| }); |
| |
| // No matching pattern, return 'everything-else' index |
| if (methodGroupIndexes.length === 0) { |
| const everythingElseIndex = methodsOrder.indexOf('everything-else'); |
| |
| if (everythingElseIndex !== -1) { |
| methodGroupIndexes.push(everythingElseIndex); |
| } else { |
| // No matching pattern and no 'everything-else' group |
| methodGroupIndexes.push(Infinity); |
| } |
| } |
| |
| return methodGroupIndexes; |
| } |
| |
| /** |
| * Get properties name |
| * @param {Object} node - Property. |
| * @returns {String} Property name. |
| */ |
| function getPropertyName(node) { |
| if (node.kind === 'get') { |
| return 'getter functions'; |
| } |
| |
| if (node.kind === 'set') { |
| return 'setter functions'; |
| } |
| |
| return astUtil.getPropertyName(node); |
| } |
| |
| /** |
| * Store a new error in the error list |
| * @param {Object} propA - Mispositioned property. |
| * @param {Object} propB - Reference property. |
| */ |
| function storeError(propA, propB) { |
| // Initialize the error object if needed |
| if (!errors[propA.index]) { |
| errors[propA.index] = { |
| node: propA.node, |
| score: 0, |
| closest: { |
| distance: Infinity, |
| ref: { |
| node: null, |
| index: 0 |
| } |
| } |
| }; |
| } |
| // Increment the prop score |
| errors[propA.index].score++; |
| // Stop here if we already have pushed another node at this position |
| if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) { |
| return; |
| } |
| // Stop here if we already have a closer reference |
| if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) { |
| return; |
| } |
| // Update the closest reference |
| errors[propA.index].closest.distance = Math.abs(propA.index - propB.index); |
| errors[propA.index].closest.ref.node = propB.node; |
| errors[propA.index].closest.ref.index = propB.index; |
| } |
| |
| /** |
| * Dedupe errors, only keep the ones with the highest score and delete the others |
| */ |
| function dedupeErrors() { |
| for (const i in errors) { |
| if (has(errors, i)) { |
| const index = errors[i].closest.ref.index; |
| if (errors[index]) { |
| if (errors[i].score > errors[index].score) { |
| delete errors[index]; |
| } else { |
| delete errors[i]; |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Report errors |
| */ |
| function reportErrors() { |
| dedupeErrors(); |
| |
| entries(errors).forEach((entry) => { |
| const nodeA = entry[1].node; |
| const nodeB = entry[1].closest.ref.node; |
| const indexA = entry[0]; |
| const indexB = entry[1].closest.ref.index; |
| |
| context.report({ |
| node: nodeA, |
| message: MISPOSITION_MESSAGE, |
| data: { |
| propA: getPropertyName(nodeA), |
| propB: getPropertyName(nodeB), |
| position: indexA < indexB ? 'before' : 'after' |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Compare two properties and find out if they are in the right order |
| * @param {Array} propertiesInfos Array containing all the properties metadata. |
| * @param {Object} propA First property name and metadata |
| * @param {Object} propB Second property name. |
| * @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties. |
| */ |
| function comparePropsOrder(propertiesInfos, propA, propB) { |
| let i; |
| let j; |
| let k; |
| let l; |
| let refIndexA; |
| let refIndexB; |
| |
| // Get references indexes (the correct position) for given properties |
| const refIndexesA = getRefPropIndexes(propA); |
| const refIndexesB = getRefPropIndexes(propB); |
| |
| // Get current indexes for given properties |
| const classIndexA = propertiesInfos.indexOf(propA); |
| const classIndexB = propertiesInfos.indexOf(propB); |
| |
| // Loop around the references indexes for the 1st property |
| for (i = 0, j = refIndexesA.length; i < j; i++) { |
| refIndexA = refIndexesA[i]; |
| |
| // Loop around the properties for the 2nd property (for comparison) |
| for (k = 0, l = refIndexesB.length; k < l; k++) { |
| refIndexB = refIndexesB[k]; |
| |
| if ( |
| // Comparing the same properties |
| refIndexA === refIndexB || |
| // 1st property is placed before the 2nd one in reference and in current component |
| refIndexA < refIndexB && classIndexA < classIndexB || |
| // 1st property is placed after the 2nd one in reference and in current component |
| refIndexA > refIndexB && classIndexA > classIndexB |
| ) { |
| return { |
| correct: true, |
| indexA: classIndexA, |
| indexB: classIndexB |
| }; |
| } |
| } |
| } |
| |
| // We did not find any correct match between reference and current component |
| return { |
| correct: false, |
| indexA: refIndexA, |
| indexB: refIndexB |
| }; |
| } |
| |
| /** |
| * Check properties order from a properties list and store the eventual errors |
| * @param {Array} properties Array containing all the properties. |
| */ |
| function checkPropsOrder(properties) { |
| const propertiesInfos = properties.map(node => ({ |
| name: getPropertyName(node), |
| getter: node.kind === 'get', |
| setter: node.kind === 'set', |
| static: node.static, |
| instanceVariable: !node.static && |
| node.type === 'ClassProperty' && |
| (!node.value || !astUtil.isFunctionLikeExpression(node.value)), |
| instanceMethod: !node.static && |
| node.type === 'ClassProperty' && |
| node.value && |
| (astUtil.isFunctionLikeExpression(node.value)), |
| typeAnnotation: !!node.typeAnnotation && node.value === null |
| })); |
| |
| // Loop around the properties |
| propertiesInfos.forEach((propA, i) => { |
| // Loop around the properties a second time (for comparison) |
| propertiesInfos.forEach((propB, k) => { |
| if (i === k) { |
| return; |
| } |
| |
| // Compare the properties order |
| const order = comparePropsOrder(propertiesInfos, propA, propB); |
| |
| if (!order.correct) { |
| // Store an error if the order is incorrect |
| storeError({ |
| node: properties[i], |
| index: order.indexA |
| }, { |
| node: properties[k], |
| index: order.indexB |
| }); |
| } |
| }); |
| }); |
| } |
| |
| return { |
| 'Program:exit': function () { |
| const list = components.list(); |
| Object.keys(list).forEach((component) => { |
| const properties = astUtil.getComponentProperties(list[component].node); |
| checkPropsOrder(properties); |
| }); |
| |
| reportErrors(); |
| } |
| }; |
| }), |
| |
| defaultConfig |
| }; |