eslint-plugin-gestalt
Version:
ESLint rules for Pinterest's design language Gestalt
534 lines (468 loc) • 20.3 kB
text/typescript
import { GenericNode, GenericType, ReducerAccType, ReducerType } from './reducerTypes';
/** ================= HELPERS =================
*/
type GetPropertiesFromVariableType = (arg1: { variableNode: GenericNode }) => GenericType;
/** This function returns the properties from a variable in scope
Example 1:
const a = { color: "red"}
<Box color={a}> >> returns the color property
*/
const getPropertiesFromVariable: GetPropertiesFromVariableType = ({ variableNode }) =>
variableNode?.resolved?.defs?.[0]?.node?.init?.properties;
type GetInlineDefinedStylesType = (arg1: {
attributeNode: GenericNode;
}) => GenericNode | null | undefined;
/** This function returns the inline style defined in Box within dangerouslySetInlineStyle
Example 1: Return the property color from variable a
<Box dangerouslySetInlineStyle={{ __style: { color: red }}}> >> returns the color property
*/
const getInlineDefinedStyles: GetInlineDefinedStylesType = ({ attributeNode }) => {
const propertyNode = attributeNode?.value?.expression?.properties?.[0];
return propertyNode?.key?.name === '__style' ? propertyNode?.value.properties : null;
};
type GetOpeningElementType = (arg1: { elementNode: GenericNode }) => GenericNode;
/** This function returns the opening element independently of the node passed
Sometimes we have the parent JSXElement node, sometimes the child JSXOpeningElement node
*/
const getOpeningElement: GetOpeningElementType = ({ elementNode }) =>
elementNode.type === 'JSXOpeningElement' ? elementNode : elementNode?.openingElement;
type GetClosingElementType = (arg1: { elementNode: GenericNode }) => GenericNode;
/** This function returns the closing element independently of the node passed
Sometimes we have the parent JSXElement node, sometimes the child JSXOpeningElement node
*/
const getClosingElement: GetClosingElementType = ({ elementNode }) =>
elementNode.type === 'JSXElement'
? elementNode.closingElement
: elementNode?.parent?.closingElement;
type GetVariableDefinedStylesType = (arg1: {
variableNode: GenericNode;
}) => GenericNode | null | undefined;
/** This function returns the style defined a varianle and passed to Box's dangerouslySetInlineStyle
Example 1: Return the property color from variable a
const a = { color: "red"}
<Box dangerouslySetInlineStyle={{ __style: a }}> >> returns the color property
*/
const getVariableDefinedStyles: GetVariableDefinedStylesType = ({ variableNode }) => {
const propertyNode = getPropertiesFromVariable({ variableNode })?.[0];
return propertyNode?.key?.name === '__style' ? propertyNode?.value?.properties : null;
};
type GetNodeFromPropNameType = (arg1: {
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 }) =>
// @ts-expect-error - TS7006 - Parameter 'prop' implicitly has an 'any' type.
getOpeningElement({ elementNode })?.attributes?.find((prop) => prop?.name?.name === propName);
type GetTextNodeFromSourceCodeType = (arg1: {
context: GenericNode;
elementNode: GenericNode;
}) => string;
/** This function returns the text from a node as it's shown in the code source.
*/
const getTextNodeFromSourceCode: GetTextNodeFromSourceCodeType = ({ context, elementNode }) =>
context.getSourceCode().getText(elementNode);
type KeyValuesType = {
key: string;
value: string | number;
isValueTypeLiteral: boolean;
};
type RetrieveKeyValuesFromVariableType = (arg1: {
context: GenericNode;
variableNode: GenericNode;
}) => ReadonlyArray<KeyValuesType> | null;
/** This function returns an array of objects containing the data for each key/value in the variable object.
Example 1:
const a = { color: "red"} >> returns [{ key: "color", value: "red", isValueTypeLiteral: true }]
Example 2:
const a = { width: 20} >> returns [{ key: "width", value: 20, isValueTypeLiteral: true }]
Example 3:
const a = { onClick: () => {}} >> returns [{ key: "onClick", value: "() => {}", isValueTypeLiteral: false }]
*/
const retrieveKeyValuesFromVariable: RetrieveKeyValuesFromVariableType = ({
context,
variableNode,
}) => {
const properties = getPropertiesFromVariable({ variableNode });
return properties
? // @ts-expect-error - TS7006 - Parameter 'prop' implicitly has an 'any' type.
properties.map((prop) => {
const isLiteral = prop.value.type === 'Literal';
return {
key: prop.key.name,
value: isLiteral ? prop.value.value : context.getSourceCode().getText(prop.value),
isValueTypeLiteral: isLiteral,
};
})
: null;
};
type BuildLiteralValueStringType = (arg1: { value: string | number }) => string;
/** This function returns a string of literal value formatted as a prop value.
Example 1:
{ key: "color", value: "red", isValueTypeLiteral: true } >> "red"
Example 2:
{ key: "width", value: 20, isValueTypeLiteral: true } >> "{20}"
*/
const buildLiteralValueString: BuildLiteralValueStringType = ({ value }) =>
typeof value === 'number' ? `{${value}}` : `"${value}"`;
type RetrieveKeyValuesFromPropsType = (arg1: {
context: GenericNode;
elementNode: GenericNode;
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}"
*/
const buildProps: RetrieveKeyValuesFromPropsType = ({
context,
elementNode,
propsToAdd,
propSorting = true,
propsToRemove,
}) => {
const openingElement = getOpeningElement({ elementNode });
if (openingElement.attributes.length === 0) {
return propsToAdd ?? '';
}
const filteredProps = propsToRemove
? // @ts-expect-error - TS7006 - Parameter 'prop' implicitly has an 'any' type.
openingElement.attributes.filter((prop) => !(propsToRemove ?? []).includes(prop.name.name))
: openingElement.attributes;
// @ts-expect-error - TS7006 - Parameter 'prop' implicitly has an 'any' type.
const previousProps = filteredProps.map((prop) =>
prop.value
? `${prop.name.name}=${getTextNodeFromSourceCode({
context,
elementNode: prop.value,
})}`
: // catch boolean props
getTextNodeFromSourceCode({
context,
elementNode: prop,
}),
);
const propsArray = propsToAdd ? [...previousProps, propsToAdd] : previousProps;
return propSorting ? propsArray.sort().join(' ') : propsArray.join(' ');
};
type BuildPropsFromKeyValuesType = (arg1: { keyValues: ReadonlyArray<KeyValuesType> }) => string;
/** This function returns key/values formatted as component props.
Example 1:
{ key: "color", value: "red", isValueTypeLiteral: true } >> color="red"
Example 2:
{ key: "width", value: 20, isValueTypeLiteral: true } >> width={20}
Example 3:
{ key: "onClick", value: "() => {}", isValueTypeLiteral: false } >> onClick={() => {}}
*/
const buildPropsFromKeyValues: BuildPropsFromKeyValuesType = ({ keyValues }) => {
if (!keyValues[0]) return '';
const newKeyValues = [...keyValues];
const stringProps = newKeyValues
.sort((a, b) => {
if (a.key < b.key) return -1;
if (a.key > b.key) return 1;
return 0;
})
.map(
(pair) =>
`${pair.key}=${
pair.isValueTypeLiteral
? buildLiteralValueString({ value: pair.value })
: `{${pair.value}}`
}`,
)
.join(' ');
return stringProps;
};
type BuildPropsFromKeyValuesVariableType = (arg1: {
context: GenericNode;
variableNode: GenericNode;
}) => string;
/** This function returns key/values formatted as component props.
Example:
[{ key: "color", value: "red", isValueTypeLiteral: true }, { key: "width", value: 20, isValueTypeLiteral: true }] >> 'color="red" width={20}'
*/
const buildPropsFromKeyValuesVariable: BuildPropsFromKeyValuesVariableType = ({
context,
variableNode,
}) => {
const keyValues = retrieveKeyValuesFromVariable({
context,
variableNode,
});
return keyValues ? buildPropsFromKeyValues({ keyValues }) : '';
};
type GetComponentFromAttributeType = (arg1: { nodeAttribute: GenericNode }) => GenericNode;
/** This function returns the component containing the attribute's node (nodeAttribute).
Example:
\<div {...props} \/\> returns div node for the spread props attribute
*/
const getComponentFromAttribute: GetComponentFromAttributeType = ({ nodeAttribute }) =>
nodeAttribute.parent;
type GetVariableNodeInScopeFromNameType = (arg1: {
context: GenericNode;
nodeElement: GenericNode;
name: string;
}) => GenericNode;
/** This function returns the component's name containing the attribute's node (nodeAttribute).
Example:
\<div {...props} \/\> returns div for the spread props attribute
*/
const getVariableNodeInScopeFromName: GetVariableNodeInScopeFromNameType = ({
context,
nodeElement,
name,
}) => {
const scope = context.getScope(nodeElement);
// Look in local scope for variable reference
// @ts-expect-error - TS7006 - Parameter 'reference' implicitly has an 'any' type.
const variableNode = scope.references.find((reference) => reference.identifier.name === name);
return variableNode;
};
type GetComponentNameFromAttributeType = (arg1: { nodeAttribute: GenericNode }) => string;
/** This function returns the component's name containing the attribute's node (nodeAttribute).
Example:
\<div {...props} \/\> returns div for the spread props attribute
*/
const getComponentNameFromAttribute: GetComponentNameFromAttributeType = ({ nodeAttribute }) =>
nodeAttribute?.parent?.name?.name;
type HasImportType = (arg1: { importNode: GenericNode; path: string }) => boolean;
/** This function checks if a given node (importNode) contains a given import path (path), and returns true if so.
Example 1:
import { Box } from 'gestalt'; path="gestalt"
Example 2:
import { Box } from 'app/box'; path="app/box"
*/
const hasImport: HasImportType = ({ importNode, path }) => {
const importName = importNode.source ? importNode.source.value : null;
return importName === path;
};
type GetNamedImportsComponentsType = (arg1: {
importNode: GenericNode;
}) => ReadonlyArray<ReadonlyArray<string>> | null | undefined;
/** This function returns an array of arrays containing the named imports ([imported name, local or aliased name]) from a node (importNode).
*/
const getNamedImportsComponents: GetNamedImportsComponentsType = ({ importNode }) => {
// @ts-expect-error - TS7006 - Parameter 'node' implicitly has an 'any' type.
const namedImports = importNode?.specifiers?.map((node) => [
node.imported.name,
node?.local?.name,
]);
return namedImports;
};
type GetHtmlTagType = (arg1: { elementNode: GenericNode }) => string;
/** This function returns the tag of a given node (elementNode).
Examples:
\<div \/\> returns "div"
\<button \/\> returns "button"
*/
const getHtmlTag: GetHtmlTagType = ({ elementNode }) => elementNode?.openingElement?.name?.name;
type IsTagType = (arg1: {
elementNode: GenericNode;
tagName: string | ReadonlyArray<string>;
}) => boolean;
/** This function checks if a given node (elementNode) contains a given tag (tagName), and returns true if so.
Example 1:
\<div \/\> >> tagName="div" returns true
Example 2:
\<div \/\> >> tagName="button" returns false
*/
const isTag: IsTagType = ({ elementNode, tagName }) =>
Array.isArray(tagName)
? tagName.includes(elementNode?.name?.name)
: elementNode?.name?.name === tagName;
type HasSpreadAttributesType = (arg1: { elementNode: GenericNode }) => boolean;
/** This function checks if a given node (elementNode) contains spread attributes
Example 1:
\<div {...props} \/\> >> returns true
*/
const hasSpreadAttributes: HasSpreadAttributesType = ({ elementNode }) =>
// @ts-expect-error - TS7006 - Parameter 'attributeNode' implicitly has an 'any' type.
elementNode.attributes.some((attributeNode) => attributeNode.type === 'JSXSpreadAttribute');
type HasLonelyAttributeType = (arg1: {
elementNode: GenericNode;
tagName: string;
attribute: string;
}) => boolean;
/** This function checks if a given tag (tagName) in a node (elementNode) contains only a single attribute (attribute), and returns true if so.
Example 1:
\<div ref={} \/\> if attribute="ref" returns true
Example 2:
\<div ref={} style={} \/\> if attribute="ref" returns false
*/
const hasLonelyAttribute: HasLonelyAttributeType = ({ elementNode, tagName, attribute }) =>
isTag({ elementNode, tagName }) &&
elementNode?.attributes?.length === 1 &&
elementNode.attributes[0]?.name?.name === attribute;
type HasAttributesType = (arg1: {
elementNode: GenericNode;
tagName: string | ReadonlyArray<string>;
attributes: ReadonlyArray<string>;
}) => boolean;
/** This function checks if a given tag (tagName) in a node (elementNode) contains a given attribute (attribute), and returns true if so.
Example 1:
\<div role="button" \/\> if attribute="role" returns true
*/
const hasAttributes: HasAttributesType = ({ elementNode, tagName, attributes }) =>
isTag({ elementNode, tagName }) &&
// @ts-expect-error - TS7006 - Parameter 'nodeAttribute' implicitly has an 'any' type.
elementNode?.attributes.some((nodeAttribute) => attributes.includes(nodeAttribute?.name?.name));
type HasAriaAttributesType = (arg1: {
elementNode: GenericNode;
ignoreAttributes?: ReadonlyArray<string>;
tagName: string | ReadonlyArray<string>;
}) => boolean;
/** This function checks if a given tag (tagName) in a node (elementNode) contains an ARIA attribute, and returns true if so. Pass ignoreAttributes if not all aria attributes should be considered.
Example 1:
\<div aria-label="test" \/\> returns true
*/
const hasAriaAttributes: HasAriaAttributesType = ({ elementNode, ignoreAttributes, tagName }) =>
isTag({ elementNode, tagName }) &&
// @ts-expect-error - TS7006 - Parameter 'nodeAttribute' implicitly has an 'any' type.
elementNode?.attributes.some((nodeAttribute) => {
const attributeName = nodeAttribute?.name?.name;
return !ignoreAttributes?.includes(attributeName) && attributeName.startsWith('aria-');
});
type HasSupportedAttributesType = (arg1: {
elementNode: GenericNode;
tagName: string | ReadonlyArray<string>;
supportedAttributes: ReadonlyArray<string>;
}) => boolean;
/** This function checks if a given tag (tagName) in a node (elementNode) contains attribute that are not supported by the Gestalt alternative (supportedAttributes), and returns true if so.
Example 1:
\<div aria-label="test" \/\> returns true
*/
const hasUnsupportedAttributes: HasSupportedAttributesType = ({
elementNode,
tagName,
supportedAttributes,
}) =>
isTag({ elementNode, tagName }) &&
elementNode?.attributes.some(
// @ts-expect-error - TS7006 - Parameter 'nodeAttribute' implicitly has an 'any' type.
(nodeAttribute) => !supportedAttributes.includes(nodeAttribute?.name?.name),
);
type HasDataAttributesType = (arg1: {
elementNode: GenericNode;
tagName: string | ReadonlyArray<string>;
}) => boolean;
/** This function checks if a given tag (tagName) in a node (elementNode) contains an data-* attribute (attribute), and returns true if so.
Example 1:
\<div data-test-id="test" \/\> returns true
*/
const hasDataAttributes: HasDataAttributesType = ({ elementNode, tagName }) =>
isTag({ elementNode, tagName }) &&
// @ts-expect-error - TS7006 - Parameter 'nodeAttribute' implicitly has an 'any' type.
elementNode?.attributes.some((nodeAttribute) => nodeAttribute?.name?.name.startsWith('data-'));
type GetLocalComponentImportNameType = (arg1: {
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;
};
type IsGestaltComponentType = (arg1: {
elementNode: GenericNode;
gestaltImportNode: GenericNode;
componentName: string;
}) => boolean;
/** This function checks if component is a Gestalt import and return true if so */
const isGestaltComponent: IsGestaltComponentType = ({
elementNode,
gestaltImportNode,
componentName,
}) => {
const importedBoxName = getNamedImportsComponents({
importNode: gestaltImportNode,
})?.find((importName) => importName[0] === componentName)?.[1];
return importedBoxName === elementNode.name.name;
};
type KeyValueTypeArrayType = ReadonlyArray<{
name: string | null | undefined;
value?: string | null | undefined | number;
node: GenericNode;
}>;
type BuildKeyValueTypeArrayType = (arg1: {
elementNode: GenericNode;
nodeType: 'openingElementNode' | 'styleProperties';
}) => KeyValueTypeArrayType;
/** This function stores attributes/properties into an array store
*/
const buildKeyValueTypeArray: BuildKeyValueTypeArrayType = ({ elementNode, nodeType }) => {
if (nodeType === 'styleProperties') {
// @ts-expect-error - TS7006 - Parameter 'stylePropertyNode' implicitly has an 'any' type.
return elementNode.map((stylePropertyNode) => {
const { key, type, value } = stylePropertyNode;
return !key || value.value === undefined
? { name: type, value: null, node: stylePropertyNode }
: { name: key.name, value: value.value, node: stylePropertyNode };
});
}
if (nodeType === 'openingElementNode') {
// @ts-expect-error - TS7006 - Parameter 'propertyNode' implicitly has an 'any' type.
return elementNode.attributes.map((propertyNode) => {
const { name, value } = propertyNode;
return { name: name.name, value: value.value, node: propertyNode };
});
}
return [{ name: undefined, value: undefined, node: undefined }];
};
type BuildValidatorResponseFromPropertiesType = (arg1: {
context: GenericNode;
keyValueTypeArray: KeyValueTypeArrayType;
reducerCallbackFn: (callbackArg: { context: GenericNode }) => ReducerType;
}) => ReducerAccType;
/** This function returns props fixes and the associated messages
*/
const buildValidatorResponsesFromProperties: BuildValidatorResponseFromPropertiesType = ({
context,
keyValueTypeArray,
reducerCallbackFn,
// @ts-expect-error - TS2740 - Type '{ name: string | null | undefined; value?: string | number | null | undefined; node: any; }' is missing the following properties from type 'readonly { node: any; prop?: string | number | null | undefined; message?: string | number | null | undefined; }[]': length, concat, join, slice, and 18 more. | TS2345 - Argument of type 'ReducerType' is not assignable to parameter of type '(previousValue: any[], currentValue: { name: string | null | undefined; value?: string | number | null | undefined; node: any; }, currentIndex: number, array: readonly { name: string | null | undefined; value?: string | ... 2 more ... | undefined; node: any; }[]) => any[]'.
}) => keyValueTypeArray.reduce<Array<any>>(reducerCallbackFn({ context }), []);
// This export acts as an index of all helper functions for quick reference of helpers available
export {
buildKeyValueTypeArray,
buildLiteralValueString,
buildProps,
buildPropsFromKeyValues,
buildPropsFromKeyValuesVariable,
buildValidatorResponsesFromProperties,
getClosingElement,
getComponentFromAttribute,
getComponentNameFromAttribute,
getHtmlTag,
getInlineDefinedStyles,
getLocalComponentImportName,
getNamedImportsComponents,
getNodeFromPropName,
getOpeningElement,
getPropertiesFromVariable,
getTextNodeFromSourceCode,
getVariableDefinedStyles,
getVariableNodeInScopeFromName,
hasAriaAttributes,
hasAttributes,
hasDataAttributes,
hasImport,
hasLonelyAttribute,
hasSpreadAttributes,
hasUnsupportedAttributes,
isGestaltComponent,
isTag,
retrieveKeyValuesFromVariable,
};