Skip to content

Create only-extend-defined rule #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

const onlySpreadCSS = require('./rules/only-spread-css');
const noUnusedStyles = require('./rules/no-unused-styles');
const onlyExtendDefined = require('./rules/only-extend-defined');

module.exports = {
rules: {
'only-spread-css': onlySpreadCSS,
'no-unused-styles': noUnusedStyles,
'only-extend-defined': onlyExtendDefined,
},

configs: {
recommended: {
rules: {
'react-with-styles/only-spread-css': 'error',
'react-with-styles/no-unused-styles': 'error',
'react-with-styles/only-extend-defined': 'error',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, adding this to the recommended list makes this a breaking change. Sometimes we add rules at first and then when we are ready to make a breaking change, we lump all of the modifications to the recommended config into one breaking change.

However, given how rarely we work on this particular plugin, it might make sense to do this now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might still be worth it to ship it as minor first (without this line) and then do a followup as a major that enables it.

'no-restricted-imports': ['error', {
paths: [{
name: 'react-with-styles',
Expand Down
121 changes: 121 additions & 0 deletions lib/rules/only-extend-defined.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use strict';

// Traverse the style object and gather all possible paths
const gatherDefinedStylePaths = (acc, styleObject, currentPath = '') => {
styleObject.properties.forEach((property) => {
if (property.computed) {
// Skip over computed properties for now.
// e.g. `{ [foo]: { ... } }`
// TODO handle this better?
return;
}

if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
// Skip over object spread for now.
// e.g. `{ ...foo }`
// TODO handle this better?
return;
}

const propertyName = property.key.value || property.key.name;
const path = currentPath ? `${currentPath}.${propertyName}` : propertyName;

// Save the path
acc[path] = true;

// Continue traversing
if (property.value.type === 'ObjectExpression') {
gatherDefinedStylePaths(acc, property.value, path);
}
});
};

// Traverse extendable styles and validate that all possible paths exist in the defined styles
const checkExtendedStyles = (context, definedStylePaths, extendedStyle, currentPath = '') => {
extendedStyle.properties.forEach((property) => {
if (property.computed) {
// Skip over computed properties for now.
// e.g. `{ [foo]: { ... } }`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this raises an interesting question: should there be a rule to disallow computed properties for extendable styles (or for styles in general)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen a lot of usages of constants for things around responsiveness (e.g. [responsive.mediumAndAbove]). I think we would begin to limit ourselves a lot if we disallowed computed properties.

I wonder if we can ensure that the same variable is used in both places... I can take a look tonight.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right! Can we add some valid and invalid test cases for nested styles? Media queries, @supports queries, pseudo-elements, pseudo-selectors, etc.

// TODO handle this better?
return;
}

if (property.type === 'ExperimentalSpreadProperty' || property.type === 'SpreadElement') {
// Skip over object spread for now.
// e.g. `{ ...foo }`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, should spreading in extendable styles be disallowed?

// TODO handle this better?
return;
}

const propertyName = property.key.value || property.key.name;
const path = currentPath ? `${currentPath}.${propertyName}` : propertyName;

// Fire the lint rule if the path is not found
if (!definedStylePaths[path]) {
context.report(
property,
`\`${propertyName}\` is not extendable`
);
return;
}

// Continue traversing
if (property.value.type === 'ObjectExpression') {
checkExtendedStyles(context, definedStylePaths, property.value, path);
}
});
};

module.exports = {
meta: {
docs: {
description: 'Require that all styles that are extendable are defined on the component.',
recommended: true,
},

schema: [],
},

create: function rule(context) {
// Paths that exist in the defined styles
const definedStyles = {};

return {
CallExpression(node) {
if (node.callee.name === 'withStyles') {
const styles = node.arguments[0];
if (styles && styles.type === 'ArrowFunctionExpression') {
const body = styles.body;

let stylesObj;
if (body.type === 'ObjectExpression') {
stylesObj = body;
} else if (body.type === 'BlockStatement') {
body.body.forEach((bodyNode) => {
if (
bodyNode.type === 'ReturnStatement'
&& bodyNode.argument.type === 'ObjectExpression'
) {
stylesObj = bodyNode.argument;
}
});
}

if (stylesObj) {
gatherDefinedStylePaths(definedStyles, stylesObj);
}
}

const options = node.arguments[1];
if (options && options.type === 'ObjectExpression') {
options.properties.forEach((optionProperty) => {
if (optionProperty.key.name === 'extendableStyles' && optionProperty.value.type === 'ObjectExpression') {
checkExtendedStyles(context, definedStyles, optionProperty.value);
}
});
}
}
},
};
},
};
Loading