123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- /**
- * @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')
- },
- messages: {
- unsortedProps: '{{propA}} should be placed {{position}} {{propB}}'
- },
- 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 methodsOrder = getMethodsOrder(context.options[0]);
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
- const regExpRegExp = /\/(.*)\/([gimsuy]*)/;
- /**
- * 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-variables') {
- if (method.staticVariable) {
- methodGroupIndexes.push(groupIndex);
- }
- } else if (currentGroup === 'static-methods') {
- if (method.staticMethod) {
- 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,
- messageId: 'unsortedProps',
- 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',
- staticVariable: node.static
- && node.type === 'ClassProperty'
- && (!node.value || !astUtil.isFunctionLikeExpression(node.value)),
- staticMethod: node.static
- && (node.type === 'ClassProperty' || node.type === 'MethodDefinition')
- && node.value
- && (astUtil.isFunctionLikeExpression(node.value)),
- 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'() {
- const list = components.list();
- Object.keys(list).forEach((component) => {
- const properties = astUtil.getComponentProperties(list[component].node);
- checkPropsOrder(properties);
- });
- reportErrors();
- }
- };
- }),
- defaultConfig
- };
|