Skip to content

Commit

Permalink
Merge pull request #41 from pawelglosz/fixes-for-hanging-issue
Browse files Browse the repository at this point in the history
Fixes for hanging issue
  • Loading branch information
brendanmorrell committed Jul 29, 2022
2 parents 30451e2 + f4f875d commit 525dd9a
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 36 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This plugin adds the ability to lint styled components according to the rules outlined in eslint-plugin-jsx-a11y.

It handles all 4 methods styled components uses to create components. All of these would show the error
It handles all 5 methods styled components uses to create components. All of these would show the error

```diff
-Visible, non-interactive elements with click handlers must have at least one keyboard listener.`
Expand All @@ -13,6 +13,11 @@ const Div = styled.div``;
<Div onClick={() => null} />;
```

```jsx
const S = { Div: styled.div`` };
<S.Div onClick={() => null} />;
```

```jsx
const Div = styled.div.attrs({ onClick: () => null })``;
<Div />;
Expand Down
41 changes: 27 additions & 14 deletions __tests__/utils/makeStyledTestCases.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
const parserOptionsMapper = require('./parserOptionsMapper');

const makeRuleMaker = (func) => ({
tag = 'div',
attrs = '{}',
props = '',
children = '',
errors,
siblings = '',
} = {}) => {
const args = { tag, attrs, props, children, errors, siblings };
const code = func(args);
return [{ code, errors }].map(parserOptionsMapper)[0];
};
const makeRuleMaker =
(func) =>
({ tag = 'div', attrs = '{}', props = '', children = '', errors, siblings = '' } = {}) => {
const args = { tag, attrs, props, children, errors, siblings };
const code = func(args);
return [{ code, errors }].map(parserOptionsMapper)[0];
};

const regular = ({ tag, props, children, siblings }) =>
`
`
const STYLED = styled.${tag}\`\`;
const Func = () => ${
children ? `<>${siblings}<STYLED ${props}>${children}</STYLED></>` : `<>${siblings}<STYLED ${props} /></>`
};
`;

const regularAsObject = ({ tag, props, children, siblings }) =>
`
const STYLED = {tag: styled.${tag}\`\`};
const Func = () => ${
children
? `<>${siblings}<STYLED.tag ${props}>${children}</STYLED.tag></>`
: `<>${siblings}<STYLED.tag ${props} /></>`
};
`;

const withStyledAttrs = ({ tag, attrs, children, siblings }) =>
`
const STYLED = styled.${tag}.attrs(${attrs})\`\`;
Expand Down Expand Up @@ -60,7 +65,15 @@ const withStyledComponentsAsOtherWithComponentDefinedAfterInstantiation = ({ tag
`;

const makeStyledTestCases = (args) =>
[regular, withStyledAttrs, withStyledComponent, withStyledComponentImmediatelyChained, withStyledComponentAsOther, withStyledComponentsAsOtherWithComponentDefinedAfterInstantiation]
[
regular,
regularAsObject,
withStyledAttrs,
withStyledComponent,
withStyledComponentImmediatelyChained,
withStyledComponentAsOther,
withStyledComponentsAsOtherWithComponentDefinedAfterInstantiation,
]
.map(makeRuleMaker)
.map((x) => x(args));

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-react-hooks": "^2.4.0",
"jest": "^25.1.0",
"jsx-ast-utils": "^2.2.3",
"jsx-ast-utils": "^3.3.2",
"nodemon": "^2.0.2"
}
}
12 changes: 10 additions & 2 deletions src/utils/collectStyledComponentData.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ const { __UNKNOWN_IDENTIFER__ } = require('./constants');

module.exports = (styledComponentsDict, context, name) => ({
TaggedTemplateExpression(node) {
const scName = node.parent.id && node.parent.id.name;
if (!scName) return;
let scName = node.parent.id && node.parent.id.name;

if (!scName) {
if (isPlainSTE(node) && node.parent.key?.name && node.parent.parent?.parent?.id?.name) {
scName = `${node.parent.parent.parent.id.name}.${node.parent.key.name}`;
} else {
return;
}
}

let attrs = [];
let tag = '';
const func = (inspectee) => name.includes('anchor-is-valid') && context.report(node, inspect(inspectee || node));
Expand Down
8 changes: 8 additions & 0 deletions src/utils/mapChainExpressions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = (attributes) =>
attributes.map((attribute) => {
if (attribute.value?.expression?.type === 'ChainExpression') {
attribute.value.expression = attribute.value.expression.expression;
}

return attribute;
});
40 changes: 33 additions & 7 deletions src/utils/nodeParsers/JSXAttribute.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,61 @@
const mergeStyledAttrsWithNodeAttrs = require('../mergeStyledAttrsWithNodeAttrs');
const getAsProp = require('../getAsProp');
const { inspect } = require('util');

module.exports = (context, styledComponents, rule, name) => ({
JSXOpeningElement(node) {
const func = inspectee => name.includes('') && context.report(node, inspect(inspectee || node));
const func = (inspectee) => name.includes('') && context.report(node, inspect(inspectee || node));
try {
const originalName = node.name.name;
const styledComponent = styledComponents[originalName];
let elementName = node.name.name;

if (!elementName && node.name.type === 'JSXMemberExpression') {
elementName = `${node.name.object.name}.${node.name.property.name}`;
}

const styledComponent = styledComponents[elementName];

if (styledComponent) {
const { tag, attrs } = styledComponent;
const originalNodeAttr = node.attributes;
const originalNodeName = node.name;

const allAttrs = mergeStyledAttrsWithNodeAttrs(attrs, originalNodeAttr);
const asProp = getAsProp(allAttrs);

allAttrs.forEach(atr => {
allAttrs.forEach((atr) => {
const originalAtrLoc = atr.loc;
const originalParent = atr.parent;
// need to save the attrs of both the atr parent and the actual node depending on which one we use as the parent so we can reassign them back after
const originalAtrParentAttributes = atr.parent && atr.parent.attributes;
const originalNodeAttributes = node.attributes;
if (!atr.parent) atr.parent = node;
let nodeNameProperties;

try {
if (!atr.parent) {
atr.parent = node;
nodeNameProperties = originalNodeName;
} else {
nodeNameProperties = originalParent.name;
}

atr.loc = node.loc;
atr.parent.name.name = asProp || tag;
// Convert JSXMemberExpression to JSXIdentifier, so it'll be properly handled by eslint-plugin-jsx-a11y plugin
atr.parent.name = {
type: 'JSXIdentifier',
name: asProp || tag,
start: nodeNameProperties.start,
end: nodeNameProperties.end,
loc: nodeNameProperties.loc,
range: nodeNameProperties.range,
parent: nodeNameProperties.parent,
};
atr.parent.attributes = allAttrs;
rule.create(context).JSXAttribute(atr, func);
} finally {
atr.loc = originalAtrLoc;
atr.parent = originalParent;
if (originalAtrParentAttributes) atr.parent.attributes = originalAtrParentAttributes;
node.name.name = originalName;
node.name = originalNodeName;
node.attributes = originalNodeAttributes;
}
});
Expand Down
30 changes: 25 additions & 5 deletions src/utils/nodeParsers/JSXElement.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
const mergeStyledAttrsWithNodeAttrs = require('../mergeStyledAttrsWithNodeAttrs');
const getAsProp = require('../getAsProp');
const { inspect } = require('util');

module.exports = (context, styledComponents, rule, name) => ({
JSXElement(node) {
const func = inspectee => name.includes('scope') && context.report(node, inspect(inspectee || node));
const func = (inspectee) => name.includes('scope') && context.report(node, inspect(inspectee || node));
try {
const originalName = node.openingElement.name.name;
const styledComponent = styledComponents[originalName];
let elementName = node.openingElement.name.name;

if (!elementName && node.openingElement.name.type === 'JSXMemberExpression') {
elementName = `${node.openingElement.name.object.name}.${node.openingElement.name.property.name}`;
}

const styledComponent = styledComponents[elementName];

if (styledComponent) {
const { tag, attrs } = styledComponent;
const originalNodeAttr = node.openingElement.attributes;
const originalNodeName = node.openingElement.name;

try {
const allAttrs = mergeStyledAttrsWithNodeAttrs(attrs, originalNodeAttr);
const asProp = getAsProp(allAttrs);
node.openingElement.attributes = allAttrs;
node.openingElement.name.name = asProp || tag;

// Convert JSXMemberExpression to JSXIdentifier, so it'll be properly handled by eslint-plugin-jsx-a11y plugin
node.openingElement.name = {
type: 'JSXIdentifier',
name: asProp || tag,
start: originalNodeName.start,
end: originalNodeName.end,
loc: originalNodeName.loc,
range: originalNodeName.range,
parent: originalNodeName.parent,
};

rule.create(context).JSXElement(node);
} finally {
node.openingElement.name.name = originalName;
node.openingElement.name = originalNodeName;
node.openingElement.attributes = originalNodeAttr;
}
}
Expand Down
32 changes: 26 additions & 6 deletions src/utils/nodeParsers/JSXOpeningElement.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
const mergeStyledAttrsWithNodeAttrs = require('../mergeStyledAttrsWithNodeAttrs');
const getAsProp = require('../getAsProp');
const mapChainExpressions = require('../mapChainExpressions');
const { inspect } = require('util');

module.exports = (context, styledComponents, rule, name) => ({
JSXOpeningElement(node) {
const func = inspectee => name.includes('scope') && context.report(node, inspect(inspectee || node));
const func = (inspectee) => name.includes('scope') && context.report(node, inspect(inspectee || node));
try {
const originalName = node.name.name;
const styledComponent = styledComponents[originalName];
let elementName = node.name.name;

if (!elementName && node.name.type === 'JSXMemberExpression') {
elementName = `${node.name.object.name}.${node.name.property.name}`;
}

const styledComponent = styledComponents[elementName];

if (styledComponent) {
const { tag, attrs } = styledComponent;
const originalNodeAttr = node.attributes;
const originalNodeName = node.name;

try {
const allAttrs = mergeStyledAttrsWithNodeAttrs(attrs, originalNodeAttr);
const asProp = getAsProp(allAttrs);
node.attributes = allAttrs;
node.name.name = asProp || tag;
node.attributes = mapChainExpressions(allAttrs);

// Convert JSXMemberExpression to JSXIdentifier, so it'll be properly handled by eslint-plugin-jsx-a11y plugin
node.name = {
type: 'JSXIdentifier',
name: asProp || tag,
start: originalNodeName.start,
end: originalNodeName.end,
loc: originalNodeName.loc,
range: originalNodeName.range,
parent: originalNodeName.parent,
};

rule.create(context).JSXOpeningElement(node);
} finally {
node.name.name = originalName;
node.name = originalNodeName;
node.attributes = originalNodeAttr;
}
}
Expand Down

0 comments on commit 525dd9a

Please sign in to comment.