From 60115057a99cf467d0d6f13890c58bb959ad1ccc Mon Sep 17 00:00:00 2001 From: Emmanuel Roussel Date: Tue, 1 Oct 2019 21:57:31 -0400 Subject: [PATCH] [new] `jsx-sort-default-props`: make rule fixable --- docs/rules/jsx-sort-default-props.md | 2 + lib/rules/jsx-sort-default-props.js | 10 +- lib/rules/sort-prop-types.js | 89 +-------- lib/util/propTypesSort.js | 164 ++++++++++++++++ tests/lib/rules/jsx-sort-default-props.js | 216 ++++++++++++++++++++-- 5 files changed, 389 insertions(+), 92 deletions(-) create mode 100644 lib/util/propTypesSort.js diff --git a/docs/rules/jsx-sort-default-props.md b/docs/rules/jsx-sort-default-props.md index d4b48f8181..251726b9c2 100644 --- a/docs/rules/jsx-sort-default-props.md +++ b/docs/rules/jsx-sort-default-props.md @@ -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. diff --git a/lib/rules/jsx-sort-default-props.js b/lib/rules/jsx-sort-default-props.js index 7ba0447f90..76b881c161 100644 --- a/lib/rules/jsx-sort-default-props.js +++ b/lib/rules/jsx-sort-default-props.js @@ -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 @@ -22,6 +23,8 @@ module.exports = { url: docsUrl('jsx-sort-default-props') }, + fixable: 'code', + schema: [{ type: 'object', properties: { @@ -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]; @@ -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; diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index 8589d317fc..6e0d3c99c9 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -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 @@ -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. @@ -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) => { diff --git a/lib/util/propTypesSort.js b/lib/util/propTypesSort.js new file mode 100644 index 0000000000..badcf79896 --- /dev/null +++ b/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 +}; diff --git a/tests/lib/rules/jsx-sort-default-props.js b/tests/lib/rules/jsx-sort-default-props.js index 2b168c62ab..4f3918fb25 100644 --- a/tests/lib/rules/jsx-sort-default-props.js +++ b/tests/lib/rules/jsx-sort-default-props.js @@ -374,7 +374,24 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 10, column: 5, type: 'Property' - }] + }], + output: [ + 'class Component extends React.Component {', + ' static propTypes = {', + ' a: PropTypes.any,', + ' b: PropTypes.any,', + ' c: PropTypes.any', + ' };', + ' static defaultProps = {', + ' a: "a",', + ' b: "b",', + ' c: "c"', + ' };', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n') }, { code: [ 'class Component extends React.Component {', @@ -394,7 +411,24 @@ ruleTester.run('jsx-sort-default-props', rule, { '}' ].join('\n'), parser: parsers.BABEL_ESLINT, - errors: 2 + errors: 2, + output: [ + 'class Component extends React.Component {', + ' static propTypes = {', + ' a: PropTypes.any,', + ' b: PropTypes.any,', + ' c: PropTypes.any', + ' };', + ' static defaultProps = {', + ' a: "a",', + ' b: "b",', + ' c: "c"', + ' };', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n') }, { code: [ 'class Component extends React.Component {', @@ -420,7 +454,22 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 8, column: 5, type: 'Property' - }] + }], + output: [ + 'class Component extends React.Component {', + ' static propTypes = {', + ' a: PropTypes.any,', + ' b: PropTypes.any', + ' };', + ' static defaultProps = {', + ' a: "a",', + ' Z: "Z",', + ' };', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n') }, { code: [ 'class Component extends React.Component {', @@ -443,7 +492,22 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 8, column: 5, type: 'Property' - }] + }], + output: [ + 'class Component extends React.Component {', + ' static propTypes = {', + ' a: PropTypes.any,', + ' z: PropTypes.any', + ' };', + ' static defaultProps = {', + ' Z: "Z",', + ' a: "a",', + ' };', + ' render() {', + ' return
;', + ' }', + '}' + ].join('\n') }, { code: [ 'class Hello extends React.Component {', @@ -466,7 +530,22 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 12, column: 3, type: 'Property' - }] + }], + output: [ + 'class Hello extends React.Component {', + ' render() {', + ' return
Hello
;', + ' }', + '}', + 'Hello.propTypes = {', + ' "a": PropTypes.string,', + ' "b": PropTypes.string', + '};', + 'Hello.defaultProps = {', + ' "a": "a",', + ' "b": "b"', + '};' + ].join('\n') }, { code: [ 'class Hello extends React.Component {', @@ -486,7 +565,24 @@ ruleTester.run('jsx-sort-default-props', rule, { '};' ].join('\n'), parser: parsers.BABEL_ESLINT, - errors: 2 + errors: 2, + output: [ + 'class Hello extends React.Component {', + ' render() {', + ' return
Hello
;', + ' }', + '}', + 'Hello.propTypes = {', + ' "a": PropTypes.string,', + ' "b": PropTypes.string,', + ' "c": PropTypes.string', + '};', + 'Hello.defaultProps = {', + ' "a": "a",', + ' "b": "b",', + ' "c": "c"', + '};' + ].join('\n') }, { code: [ 'class Hello extends React.Component {', @@ -509,7 +605,22 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 12, column: 3, type: 'Property' - }] + }], + output: [ + 'class Hello extends React.Component {', + ' render() {', + ' return
Hello
;', + ' }', + '}', + 'Hello.propTypes = {', + ' "a": PropTypes.string,', + ' "B": PropTypes.string,', + '};', + 'Hello.defaultProps = {', + ' "B": "B",', + ' "a": "a",', + '};' + ].join('\n') }, { code: [ 'class Hello extends React.Component {', @@ -535,7 +646,22 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 12, column: 3, type: 'Property' - }] + }], + output: [ + 'class Hello extends React.Component {', + ' render() {', + ' return
Hello
;', + ' }', + '}', + 'Hello.propTypes = {', + ' "a": PropTypes.string,', + ' "B": PropTypes.string,', + '};', + 'Hello.defaultProps = {', + ' "a": "a",', + ' "B": "B",', + '};' + ].join('\n') }, { code: [ 'const First = (props) =>
;', @@ -555,7 +681,20 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 8, column: 3, type: 'Property' - }] + }], + output: [ + 'const First = (props) =>
;', + 'const propTypes = {', + ' z: PropTypes.string,', + ' a: PropTypes.any,', + '};', + 'const defaultProps = {', + ' a: "a",', + ' z: "z",', + '};', + 'First.propTypes = propTypes;', + 'First.defaultProps = defaultProps;' + ].join('\n') }, { code: [ 'export default class ClassWithSpreadInPropTypes extends BaseClass {', @@ -577,7 +716,21 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 9, column: 5, type: 'Property' - }] + }], + output: [ + 'export default class ClassWithSpreadInPropTypes extends BaseClass {', + ' static propTypes = {', + ' b: PropTypes.string,', + ' ...c.propTypes,', + ' a: PropTypes.string', + ' }', + ' static defaultProps = {', + ' a: "a",', + ' b: "b",', + ' ...c.defaultProps', + ' }', + '}' + ].join('\n') }, { code: [ 'export default class ClassWithSpreadInPropTypes extends BaseClass {', @@ -600,7 +753,27 @@ ruleTester.run('jsx-sort-default-props', rule, { '}' ].join('\n'), parser: parsers.BABEL_ESLINT, - errors: 2 + errors: 2, + output: [ + 'export default class ClassWithSpreadInPropTypes extends BaseClass {', + ' static propTypes = {', + ' a: PropTypes.string,', + ' b: PropTypes.string,', + ' c: PropTypes.string,', + ' d: PropTypes.string,', + ' e: PropTypes.string,', + ' f: PropTypes.string', + ' }', + ' static defaultProps = {', + ' a: "a",', + ' b: "b",', + ' ...c.defaultProps,', + ' e: "e",', + ' f: "f",', + ' ...d.defaultProps', + ' }', + '}' + ].join('\n') }, { code: [ 'const defaults = {', @@ -627,6 +800,25 @@ ruleTester.run('jsx-sort-default-props', rule, { line: 15, column: 3, type: 'Property' - }] + }], + output: [ + 'const defaults = {', + ' b: "b"', + '};', + 'const types = {', + ' a: PropTypes.string,', + ' b: PropTypes.string,', + ' c: PropTypes.string', + '};', + 'function StatelessComponentWithSpreadInPropTypes({ a, b, c }) {', + ' return
{a}{b}{c}
;', + '}', + 'StatelessComponentWithSpreadInPropTypes.propTypes = types;', + 'StatelessComponentWithSpreadInPropTypes.defaultProps = {', + ' a: "a",', + ' c: "c",', + ' ...defaults,', + '};' + ].join('\n') }] });