diff --git a/lib/rules/sort-styles.js b/lib/rules/sort-styles.js index 3f2da77..44d8279 100644 --- a/lib/rules/sort-styles.js +++ b/lib/rules/sort-styles.js @@ -12,9 +12,11 @@ const { astHelpers } = require('../util/stylesheet'); const { - getStyleDeclarations, + getStyleDeclarationsChunks, + getPropertiesChunks, getStylePropertyIdentifier, isStyleSheetDeclaration, + isEitherShortHand, } = astHelpers; //------------------------------------------------------------------------------ @@ -28,13 +30,54 @@ module.exports = (context) => { const ignoreStyleProperties = options.ignoreStyleProperties; const isValidOrder = order === 'asc' ? (a, b) => a <= b : (a, b) => a >= b; - function report(type, node, prev, current) { + const sourceCode = context.getSourceCode(); + + function sort(array) { + return [].concat(array).sort((a, b) => { + const identifierA = getStylePropertyIdentifier(a); + const identifierB = getStylePropertyIdentifier(b); + + let sortOrder = 0; + if (isEitherShortHand(identifierA, identifierB)) { + return a.range[0] - b.range[0]; + } else if (identifierA < identifierB) { + sortOrder = -1; + } else if (identifierA > identifierB) { + sortOrder = 1; + } + return sortOrder * (order === 'asc' ? 1 : -1); + }); + } + + function report(array, type, node, prev, current) { const currentName = getStylePropertyIdentifier(current); const prevName = getStylePropertyIdentifier(prev); + const hasComments = array + .map(prop => sourceCode.getComments(prop)) + .reduce( + (hasComment, comment) => + hasComment || comment.leading.length > 0 || comment.trailing > 0, + false + ); + context.report({ node, message: `Expected ${type} to be in ${order}ending order. '${currentName}' should be before '${prevName}'.`, loc: current.key.loc, + fix: hasComments ? undefined : (fixer) => { + const sortedArray = sort(array); + return array + .map((item, i) => { + if (item !== sortedArray[i]) { + return fixer.replaceText( + item, + sourceCode.getText(sortedArray[i]) + ); + } + return null; + }) + .filter(Boolean); + }, }); } @@ -50,8 +93,15 @@ module.exports = (context) => { const prevName = getStylePropertyIdentifier(previous); const currentName = getStylePropertyIdentifier(current); + if ( + arrayName === 'style properties' && + isEitherShortHand(prevName, currentName) + ) { + return; + } + if (!isValidOrder(prevName, currentName)) { - return report(arrayName, node, previous, current); + return report(array, arrayName, node, previous, current); } } } @@ -62,26 +112,33 @@ module.exports = (context) => { return; } - const classDefinitions = getStyleDeclarations(node); + const classDefinitionsChunks = getStyleDeclarationsChunks(node); if (!ignoreClassNames) { - checkIsSorted(classDefinitions, 'class names', node); + classDefinitionsChunks.forEach((classDefinitions) => { + checkIsSorted(classDefinitions, 'class names', node); + }); } if (ignoreStyleProperties) return; - classDefinitions.forEach((classDefinition) => { - const styleProperties = classDefinition.value.properties; - if (!styleProperties || styleProperties.length < 2) { - return; - } - - checkIsSorted(styleProperties, 'style properties', node); + classDefinitionsChunks.forEach((classDefinitions) => { + classDefinitions.forEach((classDefinition) => { + const styleProperties = classDefinition.value.properties; + if (!styleProperties || styleProperties.length < 2) { + return; + } + const stylePropertyChunks = getPropertiesChunks(styleProperties); + stylePropertyChunks.forEach((stylePropertyChunk) => { + checkIsSorted(stylePropertyChunk, 'style properties', node); + }); + }); }); }, }; }; +module.exports.fixable = 'code'; module.exports.schema = [ { enum: ['asc', 'desc'], diff --git a/lib/util/stylesheet.js b/lib/util/stylesheet.js index 21bf520..c9b2b14 100644 --- a/lib/util/stylesheet.js +++ b/lib/util/stylesheet.js @@ -127,15 +127,26 @@ const astHelpers = { }, getStyleSheetName: function (node) { + if (node && node.id) { + return node.id.name; + } + }, + + getStyleDeclarations: function (node) { if ( node && - node.id + node.init && + node.init.arguments && + node.init.arguments[0] && + node.init.arguments[0].properties ) { - return node.id.name; + return node.init.arguments[0].properties.filter(property => property.type === 'Property'); } + + return []; }, - getStyleDeclarations: function (node) { + getStyleDeclarationsChunks: function (node) { if ( node && node.init && @@ -143,16 +154,45 @@ const astHelpers = { node.init.arguments[0] && node.init.arguments[0].properties ) { - return node - .init - .arguments[0] - .properties - .filter(property => property.type === 'Property'); + const properties = node.init.arguments[0].properties; + const result = []; + let chunk = []; + for (let i = 0; i < properties.length; i += 1) { + const property = properties[i]; + if (property.type === 'Property') { + chunk.push(property); + } else if (chunk.length) { + result.push(chunk); + chunk = []; + } + } + if (chunk.length) { + result.push(chunk); + } + return result; } return []; }, + getPropertiesChunks: function (properties) { + const result = []; + let chunk = []; + for (let i = 0; i < properties.length; i += 1) { + const property = properties[i]; + if (property.type === 'Property') { + chunk.push(property); + } else if (chunk.length) { + result.push(chunk); + chunk = []; + } + } + if (chunk.length) { + result.push(chunk); + } + return result; + }, + getExpressionIdentifier: function (node) { if (node) { switch (node.type) { @@ -437,6 +477,16 @@ const astHelpers = { return [node.object.name, node.property.name].join('.'); } }, + + isEitherShortHand: function (property1, property2) { + const shorthands = ['margin', 'padding', 'border', 'flex']; + if (shorthands.includes(property1)) { + return property2.startsWith(property1); + } else if (shorthands.includes(property2)) { + return property1.startsWith(property2); + } + return false; + }, }; module.exports.astHelpers = astHelpers; diff --git a/tests/lib/rules/sort-styles.js b/tests/lib/rules/sort-styles.js index 22b1e27..e687600 100644 --- a/tests/lib/rules/sort-styles.js +++ b/tests/lib/rules/sort-styles.js @@ -121,73 +121,481 @@ const tests = { `, options: ['asc', { ignoreClassNames: true }], }, - ], - invalid: [ { code: ` const styles = StyleSheet.create({ - myClass: { - y: 2, + a: { x: 1, - z: 3, + y: 2, + ...c, + a: 1, + c: 2, + ...g, + b: 5, + }, + c: {}, + ...g, + b: { + a: 1, + b: 2, }, }) + `, + }, + { + code: ` + const styles = StyleSheet.create({ + a: { + margin: 1, + marginLeft: 1, + }, + b: { + border: 1, + borderLeft: 1, + }, + c: { + padding: 1, + paddingLeft: 1, + }, + d: { + flex: 1, + flexGrow: 1, + } + }) + `, + options: ['asc'], + }, + { + code: ` + const styles = StyleSheet.create({ + d: { + marginLeft: 1, + margin: 1, + }, + c: { + border: 1, + borderLeft: 1, + }, + b: { + padding: 1, + paddingLeft: 1, + }, + a: { + flex: 1, + flexGrow: 1, + } + }) + `, + options: ['desc'], + }, + ], + invalid: [ + { + code: ` + const styles = StyleSheet.create({ + myClass: { + y: 2, + x: 1, + z: 3, + }, + }) + `, + output: ` + const styles = StyleSheet.create({ + myClass: { + x: 1, + y: 2, + z: 3, + }, + }) + `, + errors: [ + { + message: + "Expected style properties to be in ascending order. 'x' should be before 'y'.", + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + b: { + x: 1, + y: 2, + }, + a: { + a: 1, + b: 2, + }, + }) + `, + output: ` + const styles = StyleSheet.create({ + a: { + a: 1, + b: 2, + }, + b: { + x: 1, + y: 2, + }, + }) + `, + errors: [ + { + message: + "Expected class names to be in ascending order. 'a' should be before 'b'.", + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + 'd': {}, + 'c': {}, + 'a': {}, + 'e': {}, + 'b': {}, + }) + `, + + output: ` + const styles = StyleSheet.create({ + 'a': {}, + 'b': {}, + 'c': {}, + 'd': {}, + 'e': {}, + }) + `, + errors: [ + { + message: + "Expected class names to be in ascending order. 'c' should be before 'd'.", + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + ['b']: {}, + [\`a\`]: {}, + }) + `, + output: ` + const styles = StyleSheet.create({ + [\`a\`]: {}, + ['b']: {}, + }) + `, + errors: [ + { + message: + "Expected class names to be in ascending order. 'a' should be before 'b'.", + }, + ], + }, + { + code: ` + const a = 'a'; + const b = 'b'; + const styles = StyleSheet.create({ + [\`\${a}-\${b}-b\`]: {}, + [\`a-\${b}-a\`]: {}, + }) + `, + output: ` + const a = 'a'; + const b = 'b'; + const styles = StyleSheet.create({ + [\`a-\${b}-a\`]: {}, + [\`\${a}-\${b}-b\`]: {}, + }) `, - errors: [{ - message: 'Expected style properties to be in ascending order. \'x\' should be before \'y\'.', - }], + errors: [ + { + message: + "Expected class names to be in ascending order. 'a-b-a' should be before 'a-b-b'.", + }, + ], }, { code: ` const styles = StyleSheet.create({ + a: { + y: 2, + x: 1, + ...c, + d: 3, + c: 2, + a: 1, + ...g, + b: 5, + }, + d: {}, + c: {}, + ...g, b: { + a: 1, + b: 2, + }, + }) + `, + output: ` + const styles = StyleSheet.create({ + a: { x: 1, y: 2, + ...c, + a: 1, + c: 2, + d: 3, + ...g, + b: 5, }, - a: { + c: {}, + d: {}, + ...g, + b: { a: 1, b: 2, }, }) `, - errors: [{ - message: 'Expected class names to be in ascending order. \'a\' should be before \'b\'.', - }], + errors: [ + { + message: + "Expected style properties to be in ascending order. 'x' should be before 'y'.", + }, + { + message: + "Expected style properties to be in ascending order. 'c' should be before 'd'.", + }, + { + message: + "Expected class names to be in ascending order. 'c' should be before 'd'.", + }, + ], }, { code: ` const styles = StyleSheet.create({ - 'b': {}, - 'a': {}, + a: { + d: 4, + // comments 1 + c: 3, + a: 1, + b: 2, + }, + d: {}, + c: {}, + // comments 2 + b: { + a: 1, + b: 2, + }, + // comments 3 + }) + `, + output: ` + const styles = StyleSheet.create({ + a: { + d: 4, + // comments 1 + c: 3, + a: 1, + b: 2, + }, + d: {}, + c: {}, + // comments 2 + b: { + a: 1, + b: 2, + }, + // comments 3 }) `, - errors: [{ - message: 'Expected class names to be in ascending order. \'a\' should be before \'b\'.', - }], + errors: [ + { + message: + "Expected style properties to be in ascending order. 'c' should be before 'd'.", + }, + { + message: + "Expected class names to be in ascending order. 'c' should be before 'd'.", + }, + ], }, { code: ` - const styles = StyleSheet.create({ - ['b']: {}, - [\`a\`]: {}, - }) + const styles = StyleSheet.create({ + a: { + z: 1, + margin: 1, + b: 1, + marginLeft: 1, + a: 1, + }, + b: { + z: 1, + b: 1, + border: 1, + borderLeft: 1, + a: 1, + }, + c: { + z: 1, + padding: 1, + paddingLeft: 1, + b: 1, + a: 1, + }, + d: { + flex: 1, + z: 1, + b: 1, + flexGrow: 1, + a: 1, + } + }) + `, + output: ` + const styles = StyleSheet.create({ + a: { + a: 1, + b: 1, + margin: 1, + marginLeft: 1, + z: 1, + }, + b: { + a: 1, + b: 1, + border: 1, + borderLeft: 1, + z: 1, + }, + c: { + a: 1, + b: 1, + padding: 1, + paddingLeft: 1, + z: 1, + }, + d: { + a: 1, + b: 1, + flex: 1, + flexGrow: 1, + z: 1, + } + }) `, - errors: [{ - message: 'Expected class names to be in ascending order. \'a\' should be before \'b\'.', - }], + options: ['asc'], + errors: [ + { + message: + "Expected style properties to be in ascending order. 'margin' should be before 'z'.", + }, + { + message: + "Expected style properties to be in ascending order. 'b' should be before 'z'.", + }, + { + message: + "Expected style properties to be in ascending order. 'padding' should be before 'z'.", + }, + { + message: + "Expected style properties to be in ascending order. 'b' should be before 'z'.", + }, + ], }, { code: ` - const a = 'a'; - const b = 'b'; - const styles = StyleSheet.create({ - [\`\${a}-\${b}-b\`]: {}, - [\`a-\${b}-a\`]: {}, - }) + const styles = StyleSheet.create({ + d: { + a: 1, + marginLeft: 1, + b: 1, + margin: 1, + z: 1, + }, + c: { + a: 1, + b: 1, + border: 1, + borderLeft: 1, + z: 1, + }, + b: { + a: 1, + padding: 1, + paddingLeft: 1, + b: 1, + z: 1, + }, + a: { + flex: 1, + a: 1, + b: 1, + flexGrow: 1, + z: 1, + } + }) `, - errors: [{ - message: 'Expected class names to be in ascending order. \'a-b-a\' should be before \'a-b-b\'.', - }], + output: ` + const styles = StyleSheet.create({ + d: { + z: 1, + marginLeft: 1, + margin: 1, + b: 1, + a: 1, + }, + c: { + z: 1, + border: 1, + borderLeft: 1, + b: 1, + a: 1, + }, + b: { + z: 1, + padding: 1, + paddingLeft: 1, + b: 1, + a: 1, + }, + a: { + z: 1, + flex: 1, + flexGrow: 1, + b: 1, + a: 1, + } + }) + `, + options: ['desc'], + errors: [ + { + message: + "Expected style properties to be in descending order. 'marginLeft' should be before 'a'.", + }, + { + message: + "Expected style properties to be in descending order. 'b' should be before 'a'.", + }, + { + message: + "Expected style properties to be in descending order. 'padding' should be before 'a'.", + }, + { + message: + "Expected style properties to be in descending order. 'b' should be before 'a'.", + }, + ], }, ], };