/** * @fileoverview Enforce default props alphabetical sorting * @author Vladimir Kattsov */ 'use strict'; const variableUtil = require('../util/variable'); const docsUrl = require('../util/docsUrl'); const propWrapperUtil = require('../util/propWrapper'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: 'Enforce default props alphabetical sorting', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-sort-default-props') }, schema: [{ type: 'object', properties: { ignoreCase: { type: 'boolean' } }, additionalProperties: false }] }, create(context) { const configuration = context.options[0] || {}; const ignoreCase = configuration.ignoreCase || false; /** * Get properties name * @param {Object} node - Property. * @returns {String} Property name. */ function getPropertyName(node) { if (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) { return node.key.name; } if (node.type === 'MemberExpression') { return node.property.name; // Special case for class properties // (babel-eslint@5 does not expose property name so we have to rely on tokens) } if (node.type === 'ClassProperty') { const tokens = context.getFirstTokens(node, 2); return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; } return ''; } /** * Checks if the Identifier node passed in looks like a defaultProps declaration. * @param {ASTNode} node The node to check. Must be an Identifier node. * @returns {Boolean} `true` if the node is a defaultProps declaration, `false` if not */ function isDefaultPropsDeclaration(node) { const propName = getPropertyName(node); return (propName === 'defaultProps' || propName === 'getDefaultProps'); } function getKey(node) { return context.getSourceCode().getText(node.key || node.argument); } /** * Find a variable by name in the current scope. * @param {string} name Name of the variable to look for. * @returns {ASTNode|null} Return null if the variable could not be found, ASTNode otherwise. */ function findVariableByName(name) { const variable = variableUtil.variablesInScope(context).find(item => item.name === name); if (!variable || !variable.defs[0] || !variable.defs[0].node) { return null; } if (variable.defs[0].node.type === 'TypeAlias') { return variable.defs[0].node.right; } return variable.defs[0].node.init; } /** * Checks if defaultProps declarations are sorted * @param {Array} declarations The array of AST nodes being checked. * @returns {void} */ function checkSorted(declarations) { declarations.reduce((prev, curr, idx, decls) => { if (/Spread(?:Property|Element)$/.test(curr.type)) { return decls[idx + 1]; } let prevPropName = getKey(prev); let currentPropName = getKey(curr); if (ignoreCase) { prevPropName = prevPropName.toLowerCase(); currentPropName = currentPropName.toLowerCase(); } if (currentPropName < prevPropName) { context.report({ node: curr, message: 'Default prop types declarations should be sorted alphabetically' }); return prev; } return curr; }, declarations[0]); } function checkNode(node) { switch (node && node.type) { case 'ObjectExpression': checkSorted(node.properties); break; case 'Identifier': { const propTypesObject = findVariableByName(node.name); if (propTypesObject && propTypesObject.properties) { checkSorted(propTypesObject.properties); } break; } case 'CallExpression': { const innerNode = node.arguments && node.arguments[0]; if (propWrapperUtil.isPropWrapperFunction(context, node.callee.name) && innerNode) { checkNode(innerNode); } break; } default: break; } } // -------------------------------------------------------------------------- // Public API // -------------------------------------------------------------------------- return { ClassProperty(node) { if (!isDefaultPropsDeclaration(node)) { return; } checkNode(node.value); }, MemberExpression(node) { if (!isDefaultPropsDeclaration(node)) { return; } checkNode(node.parent.right); } }; } };