Skip to content

Commit

Permalink
[new] jsx-sort-default-props: make rule fixable
Browse files Browse the repository at this point in the history
  • Loading branch information
emroussel authored and ljharb committed Oct 2, 2019
1 parent 6f14e16 commit 6011505
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 92 deletions.
2 changes: 2 additions & 0 deletions docs/rules/jsx-sort-default-props.md
Expand Up @@ -2,6 +2,8 @@

Some developers prefer to sort `defaultProps` declarations alphabetically to be able to find necessary declarations easier at a later time. Others feel that it adds complexity and becomes a burden to maintain.

**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.

## Rule Details

This rule checks all components and verifies that all `defaultProps` declarations are sorted alphabetically. A spread attribute resets the verification. The default configuration of the rule is case-sensitive.
Expand Down
10 changes: 9 additions & 1 deletion lib/rules/jsx-sort-default-props.js
Expand Up @@ -8,6 +8,7 @@
const variableUtil = require('../util/variable');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
const propTypesSortUtil = require('../util/propTypesSort');

// ------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -22,6 +23,8 @@ module.exports = {
url: docsUrl('jsx-sort-default-props')
},

fixable: 'code',

schema: [{
type: 'object',
properties: {
Expand Down Expand Up @@ -97,6 +100,10 @@ module.exports = {
* @returns {void}
*/
function checkSorted(declarations) {
function fix(fixer) {
return propTypesSortUtil.fixPropTypesSort(fixer, context, declarations, ignoreCase);
}

declarations.reduce((prev, curr, idx, decls) => {
if (/Spread(?:Property|Element)$/.test(curr.type)) {
return decls[idx + 1];
Expand All @@ -113,7 +120,8 @@ module.exports = {
if (currentPropName < prevPropName) {
context.report({
node: curr,
message: 'Default prop types declarations should be sorted alphabetically'
message: 'Default prop types declarations should be sorted alphabetically',
fix
});

return prev;
Expand Down
89 changes: 10 additions & 79 deletions lib/rules/sort-prop-types.js
Expand Up @@ -8,6 +8,7 @@ const variableUtil = require('../util/variable');
const propsUtil = require('../util/props');
const docsUrl = require('../util/docsUrl');
const propWrapperUtil = require('../util/propWrapper');
const propTypesSortUtil = require('../util/propTypesSort');

// ------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -81,50 +82,10 @@ module.exports = {
);
}

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.
Expand All @@ -138,45 +99,15 @@ module.exports = {
}

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));
return propTypesSortUtil.fixPropTypesSort(
fixer,
context,
declarations,
ignoreCase,
requiredFirst,
callbacksLast,
sortShapeProp
);
}

declarations.reduce((prev, curr, idx, decls) => {
Expand Down
164 changes: 164 additions & 0 deletions lib/util/propTypesSort.js
@@ -0,0 +1,164 @@
/**
* @fileoverview Common propTypes sorting functionality.
*/

'use strict';

const astUtil = require('./ast');

/**
* Returns the value name of a node.
*
* @param {ASTNode} node the node to check.
* @returns {String} The name of the node.
*/
function getValueName(node) {
return node.type === 'Property' && node.value.property && node.value.property.name;
}

/**
* Checks if the prop is required or not.
*
* @param {ASTNode} node the prop to check.
* @returns {Boolean} true if the prop is required.
*/
function isRequiredProp(node) {
return getValueName(node) === 'isRequired';
}

/**
* Checks if the proptype is a callback by checking if it starts with 'on'.
*
* @param {String} propName the name of the proptype to check.
* @returns {Boolean} true if the proptype is a callback.
*/
function isCallbackPropName(propName) {
return /^on[A-Z]/.test(propName);
}

/**
* Checks if the prop is PropTypes.shape.
*
* @param {ASTNode} node the prop to check.
* @returns {Boolean} true if the prop is PropTypes.shape.
*/
function isShapeProp(node) {
return Boolean(
node && node.callee && node.callee.property && node.callee.property.name === 'shape'
);
}

/**
* Returns the properties of a PropTypes.shape.
*
* @param {ASTNode} node the prop to check.
* @returns {Array} the properties of the PropTypes.shape node.
*/
function getShapeProperties(node) {
return node.arguments && node.arguments[0] && node.arguments[0].properties;
}

/**
* Compares two elements.
*
* @param {ASTNode} a the first element to compare.
* @param {ASTNode} b the second element to compare.
* @param {Context} context The context of the two nodes.
* @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements.
* @param {Boolean=} requiredFirst whether or not to sort required elements first.
* @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else.
* @returns {Number} the sort order of the two elements.
*/
function sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast) {
const aKey = String(astUtil.getKeyValue(context, a));
const bKey = String(astUtil.getKeyValue(context, 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) {
return aKey.localeCompare(bKey);
}

if (aKey < bKey) {
return -1;
}
if (aKey > bKey) {
return 1;
}
return 0;
}

/**
* Fixes sort order of prop types.
*
* @param {Fixer} fixer the first element to compare.
* @param {Object} context the second element to compare.
* @param {Array} declarations The context of the two nodes.
* @param {Boolean=} ignoreCase whether or not to ignore case when comparing the two elements.
* @param {Boolean=} requiredFirst whether or not to sort required elements first.
* @param {Boolean=} callbacksLast whether or not to sort callbacks after everyting else.
* @param {Boolean=} sortShapeProp whether or not to sort propTypes defined in PropTypes.shape.
* @returns {Object|*|{range, text}} the sort order of the two elements.
*/
function fixPropTypesSort(fixer, context, declarations, ignoreCase, requiredFirst, callbacksLast, sortShapeProp) {
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((a, b) => sorter(a, b, context, ignoreCase, requiredFirst, callbacksLast));

source = nodes.reduceRight((acc, attr, index) => {
const sortedAttr = sortedAttributes[index];
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]);
}
}
return `${acc.slice(0, attr.range[0])}${sortedAttrText}${acc.slice(attr.range[1])}`;
}, source);
});
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));
}

module.exports = {
fixPropTypesSort
};

0 comments on commit 6011505

Please sign in to comment.