diff --git a/lib/rules/no-unused-styles.js b/lib/rules/no-unused-styles.js index f91d501..ac82f2f 100644 --- a/lib/rules/no-unused-styles.js +++ b/lib/rules/no-unused-styles.js @@ -20,12 +20,7 @@ module.exports = Components.detect((context, components) => { if ({}.hasOwnProperty.call(unusedStyles, key)) { const styles = unusedStyles[key]; styles.forEach((node) => { - const message = [ - 'Unused style detected: ', - key, - '.', - node.key.name, - ].join(''); + const message = ['Unused style detected: ', key, '.', node.key.name].join(''); context.report(node, message); }); @@ -41,6 +36,11 @@ module.exports = Components.detect((context, components) => { } }, + VariableDeclarator: function (node) { + const styleRefs = astHelpers.getPotentialDestructuredStyleReferences(node); + styleRefs.forEach(styleReferences.add, styleReferences); + }, + CallExpression: function (node) { if (astHelpers.isStyleSheetDeclaration(node, context.settings)) { const styleSheetName = astHelpers.getStyleSheetName(node); @@ -53,9 +53,7 @@ module.exports = Components.detect((context, components) => { 'Program:exit': function () { const list = components.all(); if (Object.keys(list).length > 0) { - styleReferences.forEach((reference) => { - styleSheets.markAsUsed(reference); - }); + styleReferences.forEach(styleSheets.markAsUsed, styleSheets); reportUnusedStyles(styleSheets.getUnusedReferences()); } }, diff --git a/lib/util/stylesheet.js b/lib/util/stylesheet.js index ff8da41..11c0409 100644 --- a/lib/util/stylesheet.js +++ b/lib/util/stylesheet.js @@ -1,4 +1,3 @@ - 'use strict'; /** @@ -32,9 +31,9 @@ StyleSheets.prototype.markAsUsed = function (fullyQualifiedName) { const styleSheetProperty = nameSplit[1]; if (this.styleSheets[styleSheetName]) { - this.styleSheets[styleSheetName] = this - .styleSheets[styleSheetName] - .filter((property) => property.key.name !== styleSheetProperty); + this.styleSheets[styleSheetName] = this.styleSheets[styleSheetName].filter( + (property) => property.key.name !== styleSheetProperty + ); } }; @@ -86,11 +85,8 @@ StyleSheets.prototype.getObjectExpressions = function () { return this.objectExpressions; }; - let currentContent; -const getSourceCode = (node) => currentContent - .getSourceCode(node) - .getText(node); +const getSourceCode = (node) => currentContent.getSourceCode(node).getText(node); const getStyleSheetObjectNames = (settings) => settings['react-native/style-sheet-object-names'] || ['StyleSheet']; @@ -98,20 +94,17 @@ const astHelpers = { containsStyleSheetObject: function (node, objectNames) { return Boolean( node - && node.type === 'CallExpression' - && node.callee - && node.callee.object - && node.callee.object.name - && objectNames.includes(node.callee.object.name) + && node.type === 'CallExpression' + && node.callee + && node.callee.object + && node.callee.object.name + && objectNames.includes(node.callee.object.name) ); }, containsCreateCall: function (node) { return Boolean( - node - && node.callee - && node.callee.property - && node.callee.property.name === 'create' + node && node.callee && node.callee.property && node.callee.property.name === 'create' ); }, @@ -119,11 +112,18 @@ const astHelpers = { const objectNames = getStyleSheetObjectNames(settings); return Boolean( - astHelpers.containsStyleSheetObject(node, objectNames) - && astHelpers.containsCreateCall(node) + astHelpers.containsStyleSheetObject(node, objectNames) && astHelpers.containsCreateCall(node) ); }, + getDestructuringAssignmentParts: function (node) { + if (node && node.id && node.id.type === 'ObjectPattern' && node.id.properties && node.init) { + return [node.init.name, node.id.properties]; + } + + return [null, null]; + }, + getStyleSheetName: function (node) { if (node && node.parent && node.parent.id) { return node.parent.id.name; @@ -200,10 +200,12 @@ const astHelpers = { case 'Literal': return node.value; case 'TemplateLiteral': - return node.quasis.reduce((result, quasi, index) => result - + quasi.value.cooked - + astHelpers.getExpressionIdentifier(node.expressions[index]), - ''); + return node.quasis.reduce( + (result, quasi, index) => result + + quasi.value.cooked + + astHelpers.getExpressionIdentifier(node.expressions[index]), + '' + ); default: return ''; } @@ -213,10 +215,7 @@ const astHelpers = { }, getStylePropertyIdentifier: function (node) { - if ( - node - && node.key - ) { + if (node && node.key) { return astHelpers.getExpressionIdentifier(node.key); } }, @@ -224,23 +223,20 @@ const astHelpers = { isStyleAttribute: function (node) { return Boolean( node.type === 'JSXAttribute' - && node.name - && node.name.name - && node.name.name.toLowerCase().includes('style') + && node.name + && node.name.name + && node.name.name.toLowerCase().includes('style') ); }, collectStyleObjectExpressions: function (node, context) { currentContent = context; if (astHelpers.hasArrayOfStyleReferences(node)) { - const styleReferenceContainers = node - .expression - .elements; + const styleReferenceContainers = node.expression.elements; - return astHelpers.collectStyleObjectExpressionFromContainers( - styleReferenceContainers - ); - } if (node && node.expression) { + return astHelpers.collectStyleObjectExpressionFromContainers(styleReferenceContainers); + } + if (node && node.expression) { return astHelpers.getStyleObjectExpressionFromNode(node.expression); } @@ -254,13 +250,9 @@ const astHelpers = { currentContent = context; if (astHelpers.hasArrayOfStyleReferences(node)) { - const styleReferenceContainers = node - .expression - .elements; + const styleReferenceContainers = node.expression.elements; - return astHelpers.collectColorLiteralsFromContainers( - styleReferenceContainers - ); + return astHelpers.collectColorLiteralsFromContainers(styleReferenceContainers); } if (node.type === 'ObjectExpression') { @@ -273,8 +265,9 @@ const astHelpers = { collectStyleObjectExpressionFromContainers: function (nodes) { let objectExpressions = []; nodes.forEach((node) => { - objectExpressions = objectExpressions - .concat(astHelpers.getStyleObjectExpressionFromNode(node)); + objectExpressions = objectExpressions.concat( + astHelpers.getStyleObjectExpressionFromNode(node) + ); }); return objectExpressions; @@ -283,8 +276,7 @@ const astHelpers = { collectColorLiteralsFromContainers: function (nodes) { let colorLiterals = []; nodes.forEach((node) => { - colorLiterals = colorLiterals - .concat(astHelpers.getColorLiteralsFromNode(node)); + colorLiterals = colorLiterals.concat(astHelpers.getColorLiteralsFromNode(node)); }); return colorLiterals; @@ -369,10 +361,13 @@ const astHelpers = { }, hasArrayOfStyleReferences: function (node) { - return node && Boolean( - node.type === 'JSXExpressionContainer' - && node.expression - && node.expression.type === 'ArrayExpression' + return ( + node + && Boolean( + node.type === 'JSXExpressionContainer' + && node.expression + && node.expression.type === 'ArrayExpression' + ) ); }, @@ -408,10 +403,18 @@ const astHelpers = { invalid = true; obj[p.key.name] = getSourceCode(innerNode); } - } else if (p.value.type === 'UnaryExpression' && p.value.operator === '-' && p.value.argument.type === 'Literal') { + } else if ( + p.value.type === 'UnaryExpression' + && p.value.operator === '-' + && p.value.argument.type === 'Literal' + ) { invalid = true; obj[p.key.name] = -1 * p.value.argument.value; - } else if (p.value.type === 'UnaryExpression' && p.value.operator === '+' && p.value.argument.type === 'Literal') { + } else if ( + p.value.type === 'UnaryExpression' + && p.value.operator === '+' + && p.value.argument.type === 'Literal' + ) { invalid = true; obj[p.key.name] = p.value.argument.value; } @@ -443,21 +446,13 @@ const astHelpers = { }, getObjectName: function (node) { - if ( - node - && node.object - && node.object.name - ) { + if (node && node.object && node.object.name) { return node.object.name; } }, getPropertyName: function (node) { - if ( - node - && node.property - && node.property.name - ) { + if (node && node.property && node.property.name) { return node.property.name; } }, @@ -477,11 +472,22 @@ const astHelpers = { } }, + getPotentialDestructuredStyleReferences: function (node) { + const [styleSheetName, properties] = this.getDestructuringAssignmentParts(node); + + return styleSheetName && properties + ? properties.flatMap((property) => (property.key && property.key.type === 'Identifier' && property.key.name + ? `${styleSheetName}.${property.key.name}` + : [])) + : []; + }, + isEitherShortHand: function (property1, property2) { const shorthands = ['margin', 'padding', 'border', 'flex']; if (shorthands.includes(property1)) { return property2.startsWith(property1); - } if (shorthands.includes(property2)) { + } + if (shorthands.includes(property2)) { return property1.startsWith(property2); } return false; diff --git a/tests/lib/rules/no-color-literals.js b/tests/lib/rules/no-color-literals.js index 987c4bf..185b652 100644 --- a/tests/lib/rules/no-color-literals.js +++ b/tests/lib/rules/no-color-literals.js @@ -37,7 +37,7 @@ const tests = { export default class MyComponent extends Component { render() { const isDanger = true; - return ; } @@ -57,11 +57,11 @@ const tests = { export default class MyComponent extends Component { render() { const trueColor = '#fff'; - const falseColor = '#000' - return ; } } @@ -79,9 +79,11 @@ const tests = { } }); `, - errors: [{ - message: 'Color literal: { backgroundColor: \'#FFFFFF\' }', - }], + errors: [ + { + message: "Color literal: { backgroundColor: '#FFFFFF' }", + }, + ], }, { code: ` @@ -93,9 +95,11 @@ const tests = { } }); `, - errors: [{ - message: 'Color literal: { backgroundColor: \'#FFFFFF\' }', - }], + errors: [ + { + message: "Color literal: { backgroundColor: '#FFFFFF' }", + }, + ], }, { code: ` @@ -110,9 +114,11 @@ const tests = { } }); `, - errors: [{ - message: 'Color literal: { fontColor: \'#000\' }', - }], + errors: [ + { + message: "Color literal: { fontColor: '#000' }", + }, + ], }, { code: ` @@ -124,24 +130,28 @@ const tests = { } }); `, - errors: [{ - message: 'Color literal: { backgroundColor: \'#FFFFFF\' }', - }], + errors: [ + { + message: "Color literal: { backgroundColor: '#FFFFFF' }", + }, + ], }, { code: ` const Hello = React.createClass({ render: function() { - const someBoolean = false; + const someBoolean = false; return Hello {this.props.name} ; } }); `, - errors: [{ - message: 'Color literal: { backgroundColor: \'#FFFFFF\' }', - }], + errors: [ + { + message: "Color literal: { backgroundColor: '#FFFFFF' }", + }, + ], }, { code: ` @@ -156,23 +166,23 @@ const tests = { }); export default class MyComponent extends Component { render() { - return ; } } `, errors: [ { - message: 'Color literal: { color: \'red\' }', + message: "Color literal: { color: 'red' }", }, { - message: 'Color literal: { borderBottomColor: \'blue\' }', + message: "Color literal: { borderBottomColor: 'blue' }", }, { - message: 'Color literal: { backgroundColor: \'someBoolean ? \\\'#fff\\\' : \\\'#000\\\'\' }', //eslint-disable-line + message: `Color literal: { backgroundColor: "someBoolean ? '#fff' : '#000'" }`, //eslint-disable-line }, ], }, diff --git a/tests/lib/rules/no-inline-styles.js b/tests/lib/rules/no-inline-styles.js index 0259ab8..d61e2f5 100644 --- a/tests/lib/rules/no-inline-styles.js +++ b/tests/lib/rules/no-inline-styles.js @@ -56,11 +56,11 @@ const tests = { }); export default class MyComponent extends Component { render() { - const trueColor = '#fff'; const falseColor = '#000' - return ; } } @@ -154,7 +154,7 @@ const tests = { code: ` const Hello = React.createClass({ render: function() { - const someBoolean = false; + const someBoolean = false; return Hello {this.props.name} ; @@ -177,16 +177,16 @@ const tests = { }); export default class MyComponent extends Component { render() { - return ; } } `, errors: [{ - message: 'Inline style: { backgroundColor: \'someBoolean ? \\\'#fff\\\' : \\\'#000\\\'\' }', //eslint-disable-line + message: `Inline style: { backgroundColor: "someBoolean ? '#fff' : '#000'" }`, //eslint-disable-line }], }, ], diff --git a/tests/lib/rules/no-unused-styles.js b/tests/lib/rules/no-unused-styles.js index 9681b33..a9b468d 100644 --- a/tests/lib/rules/no-unused-styles.js +++ b/tests/lib/rules/no-unused-styles.js @@ -20,8 +20,9 @@ require('babel-eslint'); const ruleTester = new RuleTester(); const tests = { - valid: [{ - code: ` + valid: [ + { + code: ` const styles = StyleSheet.create({ name: {} }); @@ -31,8 +32,22 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + name: {} + }); + const Hello = React.createClass({ + render: function() { + const { name } = styles; + return Hello {this.props.name}; + } + }); + `, + }, + { + code: ` const Hello = React.createClass({ render: function() { return Hello {this.props.name}; @@ -41,37 +56,68 @@ const tests = { const styles = StyleSheet.create({ name: {} }); + `, + }, + { + code: ` + const Hello = React.createClass({ + render: function() { + const { name } = styles; + return Hello {this.props.name}; + } + }); + const styles = StyleSheet.create({ + name: {} + }); `, - }, { - code: ` + }, + { + code: ` const styles = StyleSheet.create({ name: {} + }) + `, + }, + { + code: ` + const styles = StyleSheet.create({ + name: {}, + welcome: {} }); const Hello = React.createClass({ render: function() { return Hello {this.props.name}; } }); + const Welcome = React.createClass({ + render: function() { + return Welcome; + } + }); `, - }, { - code: ` + }, + { + code: ` const styles = StyleSheet.create({ name: {}, welcome: {} }); const Hello = React.createClass({ render: function() { - return Hello {this.props.name}; + const { name } = styles; + return Hello {this.props.name}; } }); const Welcome = React.createClass({ render: function() { - return Welcome; + const { welcome } = styles; + return Welcome; } }); `, - }, { - code: ` + }, + { + code: ` const styles = StyleSheet.create({ text: {} }) @@ -84,8 +130,25 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + text: {} + }) + const Hello = React.createClass({ + propTypes: { + textStyle: Text.propTypes.style, + }, + render: function() { + const { text } = styles; + return Hello {this.props.name}; + } + }); + `, + }, + { + code: ` const styles = StyleSheet.create({ text: {} }) @@ -105,14 +168,39 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + text: {} + }) + const styles2 = StyleSheet.create({ + text: {} + }) + const Hello = React.createClass({ + propTypes: { + textStyle: Text.propTypes.style, + }, + render: function() { + const { text } = styles; + const { text: text2 } = styles2; + return ( + + Hello {this.props.name} + + ); + } + }); + `, + }, + { + code: ` const styles = StyleSheet.create({ text: {} }); const Hello = React.createClass({ getInitialState: function() { - return { condition: true, condition2: true }; + return { condition: true, condition2: true }; }, render: function() { return ( @@ -127,15 +215,40 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + text: {} + }); + const Hello = React.createClass({ + getInitialState: function() { + return { condition: true, condition2: true }; + }, + render: function() { + const { text } = styles; + return ( + + Hello {this.props.name} + + ); + } + }); + `, + }, + { + code: ` const styles = StyleSheet.create({ text: {}, text2: {}, }); const Hello = React.createClass({ getInitialState: function() { - return { condition: true }; + return { condition: true }; }, render: function() { return ( @@ -146,8 +259,30 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + text: {}, + text2: {}, + }); + const Hello = React.createClass({ + getInitialState: function() { + return { condition: true }; + }, + render: function() { + const { text, text2 } = styles; + return ( + + Hello {this.props.name} + + ); + } + }); + `, + }, + { + code: ` const styles = StyleSheet.create({ style1: { color: 'red', @@ -165,17 +300,40 @@ const tests = { } } `, - }, { - code: ` + }, + { + code: ` + const styles = StyleSheet.create({ + style1: { + color: 'red', + }, + style2: { + color: 'blue', + } + }); + export default class MyComponent extends Component { + static propTypes = { + isDanger: PropTypes.bool + }; + render() { + const { style1, style2 } = styles; + return ; + } + } + `, + }, + { + code: ` const styles = StyleSheet.create({ text: {} }) `, - }, { - code: ` + }, + { + code: ` const Hello = React.createClass({ getInitialState: function() { - return { condition: true }; + return { condition: true }; }, render: function() { const myStyle = this.state.condition ? styles.text : styles.text2; @@ -191,8 +349,31 @@ const tests = { text2: {}, }); `, - }, { - code: ` + }, + { + code: ` + const Hello = React.createClass({ + getInitialState: function() { + return { condition: true }; + }, + render: function() { + const { text, text2 } = styles; + const myStyle = this.state.condition ? text : text2; + return ( + + Hello {this.props.name} + + ); + } + }); + const styles = StyleSheet.create({ + text: {}, + text2: {}, + }); + `, + }, + { + code: ` const additionalStyles = {}; const styles = StyleSheet.create({ name: {}, @@ -204,8 +385,24 @@ const tests = { } }); `, - }, { - code: ` + }, + { + code: ` + const additionalStyles = {}; + const styles = StyleSheet.create({ + name: {}, + ...additionalStyles + }); + const Hello = React.createClass({ + render: function() { + const { name } = styles; + return Hello {this.props.name}; + } + }); + `, + }, + { + code: ` const styles = OtherStyleSheet.create({ name: {}, }); @@ -215,10 +412,25 @@ const tests = { } }); `, - }], + }, + { + code: ` + const styles = OtherStyleSheet.create({ + name: {}, + }); + const Hello = React.createClass({ + render: function() { + const { name } = styles; + return Hello {this.props.name}; + } + }); + `, + }, + ], - invalid: [{ - code: ` + invalid: [ + { + code: ` const styles = StyleSheet.create({ text: {} }) @@ -228,11 +440,32 @@ const tests = { } }); `, - errors: [{ - message: 'Unused style detected: styles.text', - }], - }, { - code: ` + errors: [ + { + message: 'Unused style detected: styles.text', + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + text: {} + }) + const Hello = React.createClass({ + render: function() { + const { b } = styles; + return Hello {this.props.name}; + } + }); + `, + errors: [ + { + message: 'Unused style detected: styles.text', + }, + ], + }, + { + code: ` const styles = StyleSheet.create({ foo: {}, bar: {}, @@ -243,11 +476,33 @@ const tests = { } } `, - errors: [{ - message: 'Unused style detected: styles.bar', - }], - }, { - code: ` + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + foo: {}, + bar: {}, + }) + class Foo extends React.Component { + render() { + const { foo } = styles; + return ; + } + } + `, + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` const styles = StyleSheet.create({ foo: {}, bar: {}, @@ -258,11 +513,33 @@ const tests = { } } `, - errors: [{ - message: 'Unused style detected: styles.bar', - }], - }, { - code: ` + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + foo: {}, + bar: {}, + }) + class Foo extends React.PureComponent { + render() { + const { foo } = styles; + return ; + } + } + `, + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` const styles = OtherStyleSheet.create({ foo: {}, bar: {}, @@ -273,20 +550,61 @@ const tests = { } } `, - errors: [{ - message: 'Unused style detected: styles.bar', - }], - }, { - code: ` + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` + const styles = OtherStyleSheet.create({ + foo: {}, + bar: {}, + }) + class Foo extends React.PureComponent { + render() { + const { foo } = styles; + return ; + } + } + `, + errors: [ + { + message: 'Unused style detected: styles.bar', + }, + ], + }, + { + code: ` const styles = StyleSheet.create({ text: {} }) const Hello = () => (<>Hello); `, - errors: [{ - message: 'Unused style detected: styles.text', - }], - }], + errors: [ + { + message: 'Unused style detected: styles.text', + }, + ], + }, + { + code: ` + const styles = StyleSheet.create({ + text: {} + }) + const Hello = () => { + const { b } = styles; + return <>Hello; + } + `, + errors: [ + { + message: 'Unused style detected: styles.text', + }, + ], + }, + ], }; const config = {