diff --git a/README.md b/README.md index 2dd2cddb5a..6bb0836761 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](# * [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'` * [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX * [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components -* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting +* [react/jsx-sort-props](docs/rules/jsx-sort-props.md): Enforce props alphabetical sorting (fixable) * [react/jsx-space-before-closing](docs/rules/jsx-space-before-closing.md): Validate spacing before closing bracket in JSX (fixable) * [react/jsx-tag-spacing](docs/rules/jsx-tag-spacing.md): Validate whitespace in and around the JSX opening and closing brackets (fixable) * [react/jsx-uses-react](docs/rules/jsx-uses-react.md): Prevent React to be incorrectly marked as unused diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index 3a4c09b4b8..3eee76c654 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -40,6 +40,77 @@ function isReservedPropName(name, list) { return list.indexOf(name) >= 0; } +function alphabeticalCompare(a, b, ignoreCase) { + if (ignoreCase) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + return a.localeCompare(b); +} + +/** + * Create an array of arrays where each subarray is composed of attributes + * that are considered sortable. + * @param {Array} attributes + * @return {Array} + */ +function getGroupsOfSortableAttributes(attributes) { + const sortableAttributeGroups = []; + let groupCount = 0; + for (let i = 0; i < attributes.length; i++) { + const lastAttr = attributes[i - 1]; + // If we have no groups or if the last attribute was JSXSpreadAttribute + // then we start a new group. Append attributes to the group until we + // come across another JSXSpreadAttribute or exhaust the array. + if ( + !lastAttr || + (lastAttr.type === 'JSXSpreadAttribute' && + attributes[i].type !== 'JSXSpreadAttribute') + ) { + groupCount++; + sortableAttributeGroups[groupCount - 1] = []; + } + if (attributes[i].type !== 'JSXSpreadAttribute') { + sortableAttributeGroups[groupCount - 1].push(attributes[i]); + } + } + return sortableAttributeGroups; +} + +const generateFixerFunction = (node, context) => { + const sourceCode = context.getSourceCode(); + const attributes = node.attributes.slice(0); + const configuration = context.options[0] || {}; + const ignoreCase = configuration.ignoreCase || false; + + // Sort props according to the context. Only supports ignoreCase. + // Since we cannot safely move JSXSpreadAttribute (due to potential variable overrides), + // we only consider groups of sortable attributes. + const sortableAttributeGroups = getGroupsOfSortableAttributes(attributes); + const sortedAttributeGroups = sortableAttributeGroups.slice(0).map(group => + group.slice(0).sort((a, b) => + alphabeticalCompare(propName(a), propName(b), ignoreCase) + ) + ); + + return function(fixer) { + const fixers = []; + + // Replace each unsorted attribute with the sorted one. + sortableAttributeGroups.forEach((sortableGroup, ii) => { + sortableGroup.forEach((attr, jj) => { + const sortedAttr = sortedAttributeGroups[ii][jj]; + const sortedAttrText = sourceCode.getText(sortedAttr); + fixers.push( + fixer.replaceTextRange([attr.start, attr.end], sortedAttrText) + ); + }); + }); + + return fixers; + }; +}; + /** * Checks if the `reservedFirst` option is valid * @param {Object} context The context of the rule @@ -88,7 +159,7 @@ module.exports = { category: 'Stylistic Issues', recommended: false }, - + fixable: 'code', schema: [{ type: 'object', properties: { @@ -168,7 +239,8 @@ module.exports = { if (!noSortAlphabetically && currentPropName < previousPropName) { context.report({ node: decl, - message: 'Props should be sorted alphabetically' + message: 'Props should be sorted alphabetically', + fix: generateFixerFunction(node, context) }); return memo; } @@ -228,7 +300,8 @@ module.exports = { if (!noSortAlphabetically && currentPropName < previousPropName) { context.report({ node: decl, - message: 'Props should be sorted alphabetically' + message: 'Props should be sorted alphabetically', + fix: generateFixerFunction(node, context) }); return memo; } diff --git a/tests/lib/rules/jsx-sort-props.js b/tests/lib/rules/jsx-sort-props.js index 08628da9dc..4ec08dd5d9 100644 --- a/tests/lib/rules/jsx-sort-props.js +++ b/tests/lib/rules/jsx-sort-props.js @@ -153,15 +153,82 @@ ruleTester.run('jsx-sort-props', rule, { } ], invalid: [ - {code: ';', errors: [expectedError]}, - {code: ';', errors: [expectedError]}, - {code: ';', errors: [expectedError]}, - {code: ';', errors: [expectedError]}, - {code: ';', options: ignoreCaseArgs, errors: [expectedError]}, - {code: ';', options: ignoreCaseArgs, errors: [expectedError]}, - {code: ';', errors: 2}, - {code: ';', errors: 2}, - {code: ';', errors: 2}, + { + code: ';', + errors: [expectedError], + output: ';' + }, + { + code: ';', + errors: [expectedError], + output: ';' + }, + { + code: ';', + errors: [expectedError], + output: ';' + }, + { + code: ';', + errors: [expectedError], + output: ';' + }, + { + code: ';', + options: ignoreCaseArgs, + errors: [expectedError], + output: ';' + }, + { + code: ';', + options: ignoreCaseArgs, + errors: [expectedError], + output: ';' + }, + { + code: ';', + output: ';', + errors: 2 + }, + { + code: ';', + output: ';', + errors: 2 + }, + { + code: ';', + output: ';', + errors: 2 + }, + { + code: [ + '', + ' {test}', + '' + ].join('\n'), + output: [ + '', + ' {test}', + '' + ].join('\n'), + errors: 3 + }, { code: ';', errors: [expectedError],