blob: 8589d317fc208366d50db632327fb57abacdc14a [file] [log] [blame]
/**
* @fileoverview Enforce propTypes declarations alphabetical sorting
*/
'use strict';
const variableUtil = require('../util/variable');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Enforce propTypes declarations alphabetical sorting',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('sort-prop-types')
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
requiredFirst: {
type: 'boolean'
},
callbacksLast: {
type: 'boolean'
},
ignoreCase: {
type: 'boolean'
},
// Whether alphabetical sorting should be enforced
noSortAlphabetically: {
type: 'boolean'
},
sortShapeProp: {
type: 'boolean'
}
},
additionalProperties: false
}]
},
create(context) {
const configuration = context.options[0] || {};
const requiredFirst = configuration.requiredFirst || false;
const callbacksLast = configuration.callbacksLast || false;
const ignoreCase = configuration.ignoreCase || false;
const noSortAlphabetically = configuration.noSortAlphabetically || false;
const sortShapeProp = configuration.sortShapeProp || false;
function getKey(node) {
if (node.key && node.key.value) {
return node.key.value;
}
return context.getSourceCode().getText(node.key || node.argument);
}
function getValueName(node) {
return node.type === 'Property' && node.value.property && node.value.property.name;
}
function isCallbackPropName(propName) {
return /^on[A-Z]/.test(propName);
}
function isRequiredProp(node) {
return getValueName(node) === 'isRequired';
}
function isShapeProp(node) {
return Boolean(
node && node.callee && node.callee.property && node.callee.property.name === 'shape'
);
}
function getShapeProperties(node) {
return node.arguments && node.arguments[0] && node.arguments[0].properties;
}
function toLowerCase(item) {
return String(item).toLowerCase();
}
function sorter(a, b) {
let aKey = getKey(a);
let bKey = getKey(b);
if (requiredFirst) {
if (isRequiredProp(a) && !isRequiredProp(b)) {
return -1;
}
if (!isRequiredProp(a) && isRequiredProp(b)) {
return 1;
}
}
if (callbacksLast) {
if (isCallbackPropName(aKey) && !isCallbackPropName(bKey)) {
return 1;
}
if (!isCallbackPropName(aKey) && isCallbackPropName(bKey)) {
return -1;
}
}
if (ignoreCase) {
aKey = toLowerCase(aKey);
bKey = toLowerCase(bKey);
}
if (aKey < bKey) {
return -1;
}
if (aKey > bKey) {
return 1;
}
return 0;
}
/**
* Checks if propTypes declarations are sorted
* @param {Array} declarations The array of AST nodes being checked.
* @returns {void}
*/
function checkSorted(declarations) {
// Declarations will be `undefined` if the `shape` is not a literal. For
// example, if it is a propType imported from another file.
if (!declarations) {
return;
}
function fix(fixer) {
function sortInSource(allNodes, source) {
const originalSource = source;
const nodeGroups = allNodes.reduce((acc, curr) => {
if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
acc.push([]);
} else {
acc[acc.length - 1].push(curr);
}
return acc;
}, [[]]);
nodeGroups.forEach((nodes) => {
const sortedAttributes = nodes.slice().sort(sorter);
for (let i = nodes.length - 1; i >= 0; i--) {
const sortedAttr = sortedAttributes[i];
const attr = nodes[i];
let sortedAttrText = context.getSourceCode().getText(sortedAttr);
if (sortShapeProp && isShapeProp(sortedAttr.value)) {
const shape = getShapeProperties(sortedAttr.value);
if (shape) {
const attrSource = sortInSource(
shape,
originalSource
);
sortedAttrText = attrSource.slice(sortedAttr.range[0], sortedAttr.range[1]);
}
}
source = `${source.slice(0, attr.range[0])}${sortedAttrText}${source.slice(attr.range[1])}`;
}
});
return source;
}
const source = sortInSource(declarations, context.getSourceCode().getText());
const rangeStart = declarations[0].range[0];
const rangeEnd = declarations[declarations.length - 1].range[1];
return fixer.replaceTextRange([rangeStart, rangeEnd], source.slice(rangeStart, rangeEnd));
}
declarations.reduce((prev, curr, idx, decls) => {
if (curr.type === 'ExperimentalSpreadProperty' || curr.type === 'SpreadElement') {
return decls[idx + 1];
}
let prevPropName = getKey(prev);
let currentPropName = getKey(curr);
const previousIsRequired = isRequiredProp(prev);
const currentIsRequired = isRequiredProp(curr);
const previousIsCallback = isCallbackPropName(prevPropName);
const currentIsCallback = isCallbackPropName(currentPropName);
if (ignoreCase) {
prevPropName = toLowerCase(prevPropName);
currentPropName = toLowerCase(currentPropName);
}
if (requiredFirst) {
if (previousIsRequired && !currentIsRequired) {
// Transition between required and non-required. Don't compare for alphabetical.
return curr;
}
if (!previousIsRequired && currentIsRequired) {
// Encountered a non-required prop after a required prop
context.report({
node: curr,
message: 'Required prop types must be listed before all other prop types',
fix
});
return curr;
}
}
if (callbacksLast) {
if (!previousIsCallback && currentIsCallback) {
// Entering the callback prop section
return curr;
}
if (previousIsCallback && !currentIsCallback) {
// Encountered a non-callback prop after a callback prop
context.report({
node: prev,
message: 'Callback prop types must be listed after all other prop types',
fix
});
return prev;
}
}
if (!noSortAlphabetically && currentPropName < prevPropName) {
context.report({
node: curr,
message: 'Prop types declarations should be sorted alphabetically',
fix
});
return prev;
}
return curr;
}, declarations[0]);
}
function checkNode(node) {
switch (node && node.type) {
case 'ObjectExpression':
checkSorted(node.properties);
break;
case 'Identifier': {
const propTypesObject = variableUtil.findVariableByName(context, 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;
}
}
return {
CallExpression(node) {
if (!sortShapeProp || !isShapeProp(node) || !(node.arguments && node.arguments[0])) {
return;
}
const firstArg = node.arguments[0];
if (firstArg.properties) {
checkSorted(firstArg.properties);
} else if (firstArg.type === 'Identifier') {
const variable = variableUtil.findVariableByName(context, firstArg.name);
if (variable && variable.properties) {
checkSorted(variable.properties);
}
}
},
ClassProperty(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
return;
}
checkNode(node.value);
},
MemberExpression(node) {
if (!propsUtil.isPropTypesDeclaration(node)) {
return;
}
checkNode(node.parent.right);
},
ObjectExpression(node) {
node.properties.forEach((property) => {
if (!property.key) {
return;
}
if (!propsUtil.isPropTypesDeclaration(property)) {
return;
}
if (property.value.type === 'ObjectExpression') {
checkSorted(property.value.properties);
}
});
}
};
}
};