Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
JSXFragment,
JSXOpeningElement,
MemberExpression,
Property,
SpreadElement,
} from "estree-jsx";

export function getAttribute(
Expand Down Expand Up @@ -35,37 +37,66 @@ export function getAnyAttribute(
return foundAttribute;
}

/**
* Attribute value and its type
*/
type Attribute =
| { type: "string"; value: string }
| {
type: "Literal";
value: string | number | bigint | boolean | RegExp | null | undefined;
}
| { type: "MemberExpression"; value: MemberExpression }
| { type: "ObjectExpression"; value: (Property | SpreadElement)[] }
| { type: "undefined"; value: undefined };

const UNDEFINED: Attribute = { type: "undefined", value: undefined };

/**
* Helper to get the raw value from a JSXAttribute value. If the JSXAttribute value is an Identifier, it tries to get the value of that variable. If the JSXAttribute value is a JSXExpressionContainer: {"value"}, it returns the inner content.
* MemberExpressions and ObjectExpressions are not parsed further.
* @param context Rule context
* @param node JSXAttribute value
* @returns Attribute in this form: { type: [specific type of the returned value], value: [actual value]}. Literal types are further processed, if their value is of type "string", type: "string" is returned, otherwise type: "Literal"
*/
export function getAttributeValue(
context: Rule.RuleContext,
node?: JSXAttribute["value"]
) {
): Attribute {
if (!node) {
return;
return UNDEFINED;
}

const valueType = node.type;

if (valueType === "Literal") {
return node.value;
if (typeof node.value === "string") {
return { type: "string", value: node.value };
}
return { type: "Literal", value: node.value };
}

if (valueType !== "JSXExpressionContainer") {
return;
return UNDEFINED;
}

if (node.expression.type === "Identifier") {
const variableScope = context.getSourceCode().getScope(node);
return getVariableValue(node.expression.name, variableScope, context);
}
if (node.expression.type === "MemberExpression") {
return getMemberExpression(node.expression);
return { type: "MemberExpression", value: node.expression };
}
if (node.expression.type === "Literal") {
return node.expression.value;
if (typeof node.expression.value === "string") {
return { type: "string", value: node.expression.value };
}
return { type: "Literal", value: node.expression.value };
}
if (node.expression.type === "ObjectExpression") {
return node.expression.properties;
return { type: "ObjectExpression", value: node.expression.properties };
}
return UNDEFINED;
}

export function getExpression(node?: JSXAttribute["value"]) {
Expand All @@ -78,15 +109,6 @@ export function getExpression(node?: JSXAttribute["value"]) {
}
}

function getMemberExpression(node: MemberExpression) {
if (!node) {
return;
}
const { object, property } = node;

return { object, property };
}

export function getVariableDeclaration(
name: string,
scope: Scope.Scope | null
Expand All @@ -103,22 +125,39 @@ export function getVariableDeclaration(
return undefined;
}

export function getVariableInit(
variableDeclaration: Scope.Variable | undefined
) {
if (!variableDeclaration || !variableDeclaration.defs.length) {
return;
}

const variableDefinition = variableDeclaration.defs[0];

if (variableDefinition.type !== "Variable") {
return;
}

return variableDefinition.node.init;
}

/**
* Helper to get the raw value of a variable, given by its name. Returns an Attribute object, similarly to getAttributeValue helper.
* @param name Variable name
* @param scope Scope where to look for the variable declaration
* @param context Rule context
* @returns Attribute in this form: { type: [specific type of the returned value], value: [actual value]}. Literal types are further processed, if their value is of type "string", type: "string" is returned, otherwise type: "Literal"
*/
export function getVariableValue(
name: string,
scope: Scope.Scope | null,
context: Rule.RuleContext
) {
): Attribute {
const variableDeclaration = getVariableDeclaration(name, scope);
if (!variableDeclaration) {
return;
}

const variableInit = variableDeclaration.defs.length
? variableDeclaration.defs[0].node.init
: undefined;
const variableInit = getVariableInit(variableDeclaration);

if (!variableInit) {
return;
return UNDEFINED;
}
if (variableInit.type === "Identifier") {
return getVariableValue(
Expand All @@ -128,12 +167,16 @@ export function getVariableValue(
);
}
if (variableInit.type === "Literal") {
return variableInit.value;
if (typeof variableInit.value === "string") {
return { type: "string", value: variableInit.value };
}
return { type: "Literal", value: variableInit.value };
}
if (variableInit.type === "MemberExpression") {
return getMemberExpression(variableInit);
return { type: "MemberExpression", value: variableInit };
}
if (variableInit.type === "ObjectExpression") {
return variableInit.properties;
return { type: "ObjectExpression", value: variableInit.properties };
}
return UNDEFINED;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Rule } from "eslint";
import { MemberExpression } from "estree-jsx";
import { getVariableValue } from ".";

/** Used to get a property name on an enum (MemberExpression). */
export function getEnumPropertyName(
context: Rule.RuleContext,
enumNode: MemberExpression
) {
if (enumNode.property.type === "Identifier") {
// E.g. const key = "key"; someEnum[key]
if (enumNode.computed) {
const scope = context.getSourceCode().getScope(enumNode);
const propertyName = enumNode.property.name;

return getVariableValue(propertyName, scope, context).value?.toString();
}
// E.g. someEnum.key
return enumNode.property.name;
}

// E.g. someEnum["key"]
if (enumNode.property.type === "Literal") {
return enumNode.property.value?.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Rule } from "eslint";
import { JSXAttribute } from "estree-jsx";
import { getVariableDeclaration } from "./JSXAttributes";
import { getVariableDeclaration, getVariableInit } from "./JSXAttributes";

/** Used to find the node where a prop value is initially assigned, to then be passed
* as a fixer function's nodeOrToken argument. Useful for when a prop may have an inline value, e.g. `<Comp prop="value" />`, or
Expand Down Expand Up @@ -41,6 +41,6 @@ function getJSXExpressionContainerValue(
scope
);

return variableDeclaration && variableDeclaration.defs[0].node.init;
return getVariableInit(variableDeclaration);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Rule } from "eslint";
import { Property, Identifier } from "estree-jsx";
import { getVariableDeclaration } from "./JSXAttributes";
import { Property } from "estree-jsx";
import { propertyNameMatches } from "./propertyNameMatches";

/** Can be used to run logic on the specified property of an ObjectExpression or
* only if the specified property exists.
Expand All @@ -14,26 +14,9 @@ export function getObjectProperty(
return;
}

const matchedProperty = properties.find((property) => {
const isIdentifier = property.key.type === "Identifier";
const { computed } = property;

// E.g. const key = "key"; {[key]: value}
if (isIdentifier && computed) {
const scope = context.getSourceCode().getScope(property);
const propertyName = (property.key as Identifier).name;
const propertyVariable = getVariableDeclaration(propertyName, scope);
return propertyVariable?.defs[0].node.init.value === name;
}
// E.g. {key: value}
if (isIdentifier && !computed) {
return (property.key as Identifier).name === name;
}
// E.g. {"key": value} or {["key"]: value}
if (property.key.type === "Literal") {
return property.key.value === name;
}
});
const matchedProperty = properties.find((property) =>
propertyNameMatches(context, property.key, property.computed, name)
);

return matchedProperty;
}
3 changes: 3 additions & 0 deletions packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "./getChildJSXElementByName";
export * from "./getCodeModDataTag";
export * from "./getComponentImportName";
export * from "./getEndRange";
export * from "./getEnumPropertyName";
export * from "./getFromPackage";
export * from "./getImportedName";
export * from "./getImportPath";
Expand All @@ -22,12 +23,14 @@ export * from "./helpers";
export * from "./importAndExport";
export * from "./includesImport";
export * from "./interfaces";
export * from "./isEnumValue";
export * from "./isReactIcon";
export * from "./JSXAttributes";
export * from "./makeJSXElementSelfClosing";
export * from "./nodeMatches/checkMatchingImportDeclaration";
export * from "./nodeMatches/checkMatchingJSXOpeningElement";
export * from "./pfPackageMatches";
export * from "./propertyNameMatches";
export * from "./removeElement";
export * from "./removeEmptyLineAfter";
export * from "./removePropertiesFromObjectExpression";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MemberExpression } from "estree-jsx";
import { propertyNameMatches } from "./propertyNameMatches";
import { Rule } from "eslint";

/** Checks whether an enum node (MemberExpression), e.g. ButtonVariant["primary"]
* has a given enumName ("ButtonVariant") and a given propertyName ("primary"), or one of given property names. */
export function isEnumValue(
context: Rule.RuleContext,
enumExpression: MemberExpression,
enumName: string,
propertyName: string | string[]
) {
if (
enumExpression?.object?.type === "Identifier" &&
enumExpression?.object?.name === enumName
) {
const nameMatches = (name: string) =>
propertyNameMatches(
context,
enumExpression.property,
enumExpression.computed,
name
);

if (Array.isArray(propertyName)) {
return propertyName.some((name) => nameMatches(name));
}

return nameMatches(propertyName);
}

return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Rule } from "eslint";
import { Expression, PrivateIdentifier } from "estree-jsx";
import { getVariableValue } from "./JSXAttributes";

/** Check whether a property name is of a given value.
* Property can either be of an ObjectExpression - {propName: "value"} or MemberExpression - someObject.propName */
export function propertyNameMatches(
context: Rule.RuleContext,
key: Expression | PrivateIdentifier,
computed: boolean,
name: string
) {
if (key.type === "Identifier") {
// E.g. const key = "key"; {[key]: value}; someObject[key]
if (computed) {
const scope = context.getSourceCode().getScope(key);
const propertyName = key.name;
const propertyVariableValue = getVariableValue(
propertyName,
scope,
context
).value;

return propertyVariableValue === name;
}
// E.g. {key: value}; someObject.key
return key.name === name;
}

// E.g. {"key": value} or {["key"]: value}; someObject["key"]
if (key.type === "Literal") {
return key.value === name;
}

return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = {
const attributeValue = getAttributeValue(
context,
attribute.value
);
).value;
const isValueDefault = attributeValue === "default";
const fixMessage = isValueDefault
? "remove the variant property"
Expand Down
Loading
Loading