Skip to content

Commit

Permalink
biggest last change
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertCarreras committed Aug 19, 2021
1 parent 1f3c058 commit dfca97e
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 192 deletions.
4 changes: 2 additions & 2 deletions docs/src/Eslint Plugin.doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ card(
`}
/>
<MainSection.Subsection
title="gestalt/prefer-box-no-classname"
description={`Prevent \`<div>\` tags that don't contain a \`className\` attribute. Instead, Use Gestalt Box.
title="gestalt/prefer-box-no-disallowed"
description={`Prevent \`<div>\` tags that don't contain disallowed attributes: className and onClick. Use Gestalt Box, instead. Other attributes are disallowed as well so this Eslint rule doesn't conflict with [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y).
[Read more about Box](/Box).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box } from 'gestalt';
export default function TestElement() {
const props = { onBlur: () => {} };
return (
<Box>
<Box ref={undefined} />
Expand All @@ -10,6 +11,7 @@ export default function TestElement() {
<div ref={undefined} onMouseOver={() => {}} />
<div ref={undefined} accessKey="test" />
<div ref={undefined} autoFocus />
<div ref={undefined} {...props} />
</Box>
);
}
23 changes: 18 additions & 5 deletions packages/eslint-plugin-gestalt/src/eslintASTFixers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// @flow strict
import { getNamedImportsComponents, getTextNodeFromSourceCode } from './eslintASTHelpers.js';
import {
getLocalComponentImportName,
getNamedImportsComponents,
getTextNodeFromSourceCode,
} from './eslintASTHelpers.js';

// $FlowFixMe[unclear-type]
type GenericNode = {| [string]: any |};
Expand Down Expand Up @@ -102,22 +106,25 @@ export const renameTagFixer: RenameTagFixerType = ({
};

type RenameTagWithPropsFixerType = ({|
additionalPropsString: string,
fixedPropsString: string,
context: GenericNode,
elementNode: GenericNode,
fixer: GenericNode,
gestaltImportNode: GenericNode,
newComponentName: string,
tagName: string,
propsToRemove?: $ReadOnlyArray<string>,
|}) => $ReadOnlyArray<GenericNode>;

/** This function is a more complex version of renameTagFixer. It has the same tag replacement functionality, but it also rebuild the props in the opening tag to include a new prop. The new prop must be formatted as a string, p.e. `as="article"`
Examples 1:
"\<div\>\<\/div\>" if tagName="div", newComponentName="Box", and completePropsString=`as="article"` returns "\<Box as="article"\>\<\/Box\>"
*/
export const renameTagWithPropsFixer: RenameTagWithPropsFixerType = ({
additionalPropsString,
fixedPropsString,
context,
elementNode,
gestaltImportNode,
fixer,
newComponentName,
tagName,
Expand All @@ -127,16 +134,22 @@ export const renameTagWithPropsFixer: RenameTagWithPropsFixerType = ({
// $FlowFixMe[incompatible-type] Flow is not detecting the method filter(Boolean)
if (!node) return false;

const completeOpeningNode = `<${newComponentName} ${additionalPropsString}${
const finalNewComponentName = getLocalComponentImportName({
importNode: gestaltImportNode,
componentName: newComponentName,
});

const completeOpeningNode = `<${finalNewComponentName} ${fixedPropsString}${
elementNode.closingElement ? '' : ' /'
}>`;

return index === 0
? fixer.replaceText(node, completeOpeningNode)
: fixer.replaceText(
node,
getTextNodeFromSourceCode({ context, elementNode: node }).replace(
tagName,
newComponentName,
finalNewComponentName,
),
);
})
Expand Down
129 changes: 100 additions & 29 deletions packages/eslint-plugin-gestalt/src/eslintASTHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ type GenericNode = {| [string]: any |};
/** ================= HELPERS =================
*/

type GetNodeFromPropNameType = ({|
elementNode: GenericNode,
propName: string,
|}) => GenericNode;

/** This function returns the attribute node within a component node (elementNode) if names (propName) match.
*/
const getNodeFromPropName: GetNodeFromPropNameType = ({ elementNode, propName }) =>
elementNode.openingElement.attributes.find((prop) => prop.name.name === propName);

type GetTextNodeFromSourceCodeType = ({|
context: GenericNode,
elementNode: GenericNode,
|}) => string;

/** This function returns the text from a node as it's shown in the code source.
*/
export const getTextNodeFromSourceCode: GetTextNodeFromSourceCodeType = ({
context,
elementNode,
}) => context.getSourceCode().getText(elementNode);
const getTextNodeFromSourceCode: GetTextNodeFromSourceCodeType = ({ context, elementNode }) =>
context.getSourceCode().getText(elementNode);

type GetPropertiesFromVariableType = ({|
variableNode: GenericNode,
Expand All @@ -25,7 +33,7 @@ type GetPropertiesFromVariableType = ({|
/** This function returns the properties of a variable (variableNode).
Examples: const a = { key: "value"} >> returns nodes containing information (key: "value")
*/
export const getPropertiesFromVariable: GetPropertiesFromVariableType = ({ variableNode }) =>
const getPropertiesFromVariable: GetPropertiesFromVariableType = ({ variableNode }) =>
variableNode?.resolved?.defs[0]?.node?.init?.properties;

type KeyValuesType = {|
Expand All @@ -46,7 +54,7 @@ const a = { width: 20} >> returns [{ key: "width", value: 20, isValueTypeLiteral
Example 3:
const a = { onClick: () => {}} >> returns [{ key: "onClick", value: "() => {}", isValueTypeLiteral: false }]
*/
export const retrieveKeyValuesFromVariable: RetrieveKeyValuesFromVariableType = ({
const retrieveKeyValuesFromVariable: RetrieveKeyValuesFromVariableType = ({
context,
variableNode,
}) => {
Expand All @@ -73,31 +81,44 @@ Example 1:
Example 2:
{ key: "width", value: 20, isValueTypeLiteral: true } >> "{20}"
*/
export const buildLiteralValueString: BuildLiteralValueStringType = ({ value }) =>
const buildLiteralValueString: BuildLiteralValueStringType = ({ value }) =>
typeof value === 'number' ? `{${value}}` : `"${value}"`;

type RetrieveKeyValuesFromPropsType = ({|
context: GenericNode,
elementNode: GenericNode,
newPropsString: string,
propSorting?: boolean,
propsToAdd?: string,
propsToRemove?: $ReadOnlyArray<string>,
|}) => string;

/** This function returns a string of component props
Example:
If the node is \<div width={20} \> and newPropsString="color='red'"", it returns "color='red' width={20}"
*/
export const buildProps: RetrieveKeyValuesFromPropsType = ({
const buildProps: RetrieveKeyValuesFromPropsType = ({
context,
elementNode,
newPropsString,
propsToAdd,
propSorting = true,
propsToRemove,
}) => {
if (elementNode.openingElement.attributes.length === 0) return newPropsString;
const openingElement =
elementNode.type === 'JSXOpeningElement' ? elementNode : elementNode.openingElement;

if (elementNode.openingElement.attributes.length === 0) return propsToAdd ?? '';
const filteredProps = propsToRemove
? openingElement.attributes.filter((prop) => !(propsToRemove ?? []).includes(prop.name.name))
: openingElement.attributes;

const previousProps = elementNode.openingElement.attributes.map(
(prop) => `${prop.name.name}=${context.getSourceCode().getText(prop.value)}`,
const previousProps = filteredProps.map(
(prop) =>
`${prop.name.name}=${getTextNodeFromSourceCode({ context, elementNode: prop.value })}`,
);

return [...previousProps, newPropsString].sort().join(' ');
const propsArray = propsToAdd ? [...previousProps, propsToAdd] : previousProps;

return propSorting ? propsArray.sort().join(' ') : propsArray.join(' ');
};

type BuildPropsFromKeyValuesType = ({|
Expand All @@ -112,7 +133,7 @@ Example 3:
{ key: "onClick", value: "() => {}", isValueTypeLiteral: false } >> onClick={() => {}}
*/

export const buildPropsFromKeyValues: BuildPropsFromKeyValuesType = ({ keyValues }) => {
const buildPropsFromKeyValues: BuildPropsFromKeyValuesType = ({ keyValues }) => {
if (!keyValues[0]) return '';

const newKeyValues = [...keyValues];
Expand Down Expand Up @@ -144,7 +165,7 @@ type BuildPropsFromKeyValuesVariableType = ({|
Example:
[{ key: "color", value: "red", isValueTypeLiteral: true }, { key: "width", value: 20, isValueTypeLiteral: true }] >> 'color="red" width={20}'
*/
export const buildPropsFromKeyValuesVariable: BuildPropsFromKeyValuesVariableType = ({
const buildPropsFromKeyValuesVariable: BuildPropsFromKeyValuesVariableType = ({
context,
variableNode,
}) => {
Expand All @@ -160,7 +181,7 @@ type GetComponentFromAttributeType = ({| nodeAttribute: GenericNode |}) => Gener
Example:
\<div {...props} \/\> returns div node for the spread props attribute
*/
export const getComponenFromAttribute: GetComponentFromAttributeType = ({ nodeAttribute }) =>
const getComponenFromAttribute: GetComponentFromAttributeType = ({ nodeAttribute }) =>
nodeAttribute.parent;

type GetVariableNodeInScopeFromNameType = ({|
Expand All @@ -173,7 +194,7 @@ Example:
\<div {...props} \/\> returns div for the spread props attribute
*/

export const getVariableNodeInScopeFromName: GetVariableNodeInScopeFromNameType = ({
const getVariableNodeInScopeFromName: GetVariableNodeInScopeFromNameType = ({
context,
nodeElement,
name,
Expand All @@ -189,9 +210,8 @@ type GetComponentNameFromAttributeType = ({| nodeAttribute: GenericNode |}) => s
Example:
\<div {...props} \/\> returns div for the spread props attribute
*/
export const getComponentNameFromAttribute: GetComponentNameFromAttributeType = ({
nodeAttribute,
}) => nodeAttribute?.parent?.name?.name;
const getComponentNameFromAttribute: GetComponentNameFromAttributeType = ({ nodeAttribute }) =>
nodeAttribute?.parent?.name?.name;

type HasImportType = ({| importNode: GenericNode, path: string |}) => boolean;
/** This function checks is a given node (importNode) contains a given import path (path), and returns true if so.
Expand All @@ -200,7 +220,7 @@ import { Box } from 'gestalt'; path="gestalt"
Example 2:
import { Box } from 'app/box'; path="app/box"
*/
export const hasImport: HasImportType = ({ importNode, path }) => {
const hasImport: HasImportType = ({ importNode, path }) => {
const importName = importNode.source ? importNode.source.value : null;
return importName === path;
};
Expand All @@ -210,7 +230,7 @@ type GetNamedImportsComponentsType = ({| importNode: GenericNode |}) => ?$ReadOn
>;
/** This function returns an array of arrays containing the named imports ([imported name, local or aliased name]) from a node (importNode).
*/
export const getNamedImportsComponents: GetNamedImportsComponentsType = ({ importNode }) => {
const getNamedImportsComponents: GetNamedImportsComponentsType = ({ importNode }) => {
const namedImports = importNode?.specifiers?.map((node) => [
node.imported.name,
node?.local?.name,
Expand All @@ -225,8 +245,7 @@ Examples:
\<div \/\> returns "div"
\<button \/\> returns "button"
*/
export const getHtmlTag: GetHtmlTagType = ({ elementNode }) =>
elementNode?.openingElement?.name?.name;
const getHtmlTag: GetHtmlTagType = ({ elementNode }) => elementNode?.openingElement?.name?.name;

type IsTagType = ({| elementNode: GenericNode, tagName: string |}) => boolean;
/** This function checks is a given node (elementNode) contains a given tag (tagName), and returns true if so.
Expand All @@ -235,7 +254,15 @@ Example 1:
Example 2:
\<div \/\> >> tagName="button" returns false
*/
export const isTag: IsTagType = ({ elementNode, tagName }) => elementNode?.name?.name === tagName;
const isTag: IsTagType = ({ elementNode, tagName }) => elementNode?.name?.name === tagName;

type HasSpreadAttributesType = ({| elementNode: GenericNode |}) => boolean;
/** This function checks is a given node (elementNode) contains spread attributs
Example 1:
\<div {...props} \/\> >> returns true
*/
const hasSpreadAttributes: HasSpreadAttributesType = ({ elementNode }) =>
elementNode.attributes.some((attributeNode) => attributeNode.type === 'JSXSpreadAttribute');

type HasLonelyAttributeType = ({|
elementNode: GenericNode,
Expand All @@ -249,7 +276,7 @@ Example 1:
Example 2:
\<div ref={} style={} \/\> if attribute="ref" returns false
*/
export const hasLonelyAttribute: HasLonelyAttributeType = ({ elementNode, tagName, attribute }) =>
const hasLonelyAttribute: HasLonelyAttributeType = ({ elementNode, tagName, attribute }) =>
isTag({ elementNode, tagName }) &&
elementNode?.attributes?.length === 1 &&
elementNode.attributes[0]?.name?.name === attribute;
Expand All @@ -264,7 +291,7 @@ type HasAttributesType = ({|
Example 1:
\<div role="button" \/\> if attribute="role" returns true
*/
export const hasAttributes: HasAttributesType = ({ elementNode, tagName, attributes }) =>
const hasAttributes: HasAttributesType = ({ elementNode, tagName, attributes }) =>
isTag({ elementNode, tagName }) &&
elementNode?.attributes.some((nodeAttribute) => attributes.includes(nodeAttribute?.name?.name));

Expand All @@ -277,6 +304,50 @@ type HasAriaAttributesType = ({|
Example 1:
\<div aria-label="test" \/\> returns true
*/
export const hasAriaAttributes: HasAriaAttributesType = ({ elementNode, tagName }) =>
const hasAriaAttributes: HasAriaAttributesType = ({ elementNode, tagName }) =>
isTag({ elementNode, tagName }) &&
elementNode?.attributes.some((nodeAttribute) => nodeAttribute?.name?.name.startsWith('aria-'));

type GetLocalComponentImportNameType = ({|
importNode: GenericNode,
componentName: string,
|}) => string;

/** This function returns the local component name, returning the alias.
Example 1:
import { Box } from 'gestalt ?? returns Box
Example 2:
import { Box as RenamedBox } from 'gestalt ?? returns RenamedBox
*/
const getLocalComponentImportName: GetLocalComponentImportNameType = ({
importNode,
componentName,
}) => {
const namedImportsComponents = getNamedImportsComponents({ importNode }) ?? [];
const componentNameMatch = namedImportsComponents.find((item) => item[0] === componentName);
return (componentNameMatch && componentNameMatch[1]) ?? componentName;
};

// This export acts as an index of all helper functions for quick reference of helpers available
export {
buildLiteralValueString,
buildProps,
buildPropsFromKeyValues,
buildPropsFromKeyValuesVariable,
getComponenFromAttribute,
getComponentNameFromAttribute,
getHtmlTag,
getLocalComponentImportName,
getNamedImportsComponents,
getNodeFromPropName,
getPropertiesFromVariable,
getTextNodeFromSourceCode,
getVariableNodeInScopeFromName,
hasAriaAttributes,
hasAttributes,
hasImport,
hasLonelyAttribute,
hasSpreadAttributes,
isTag,
retrieveKeyValuesFromVariable,
};
7 changes: 4 additions & 3 deletions packages/eslint-plugin-gestalt/src/prefer-box-as-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ const rule: ESLintRule = {
data: { tagName },
fix: (fixer) => {
const tagFixers = renameTagWithPropsFixer({
additionalPropsString: buildProps({
fixedPropsString: buildProps({
context,
elementNode: node,
newPropsString: `as="${tagName}"`,
propsToAdd: `as="${tagName}"`,
}),
context,
fixer,
elementNode: node,
fixer,
gestaltImportNode,
newComponentName: 'Box',
tagName,
});
Expand Down

0 comments on commit dfca97e

Please sign in to comment.