diff --git a/.changeset/shiny-kangaroos-hunt.md b/.changeset/shiny-kangaroos-hunt.md new file mode 100644 index 0000000..a93f696 --- /dev/null +++ b/.changeset/shiny-kangaroos-hunt.md @@ -0,0 +1,16 @@ +--- +"eslint-plugin-rtl-friendly": minor +--- + +# Add support for extra className cases + +Previously the plugin only supported literal values like `className="..."`, which lead to a big miss, Now it supports more cases: + +- `className={"..."}` (Auto Fixable) +- Template Literal: `className={``}` (Auto Fixable) +- Terenary Operator: `className={condition ? "..." : "..."}`: + - It reports errors on both consequent and alternate values + - But it only fixes the consequent value + + +Currently, Auto Fixing works with only literal values but the goal is to get it to fix all expressions diff --git a/e2e/no-physical-properties.tsx b/e2e/no-physical-properties.tsx index f7be22e..df66af7 100644 --- a/e2e/no-physical-properties.tsx +++ b/e2e/no-physical-properties.tsx @@ -476,4 +476,9 @@ declare const React; {/* eslint-disable-next-line rtl-friendly/no-physical-properties */}
+ + <> + {/* eslint-disable-next-line rtl-friendly/no-physical-properties */} +
0.5 ? "pl-1 text-right mr-2" : "pl-1 text-right mr-2"} /> + ; diff --git a/package.json b/package.json index f878bc1..c978ccc 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/estree": "^1.0.5", "@types/estree-jsx": "^1.0.5", "@types/jest": "^29.5.12", + "@types/node": "20", "eslint": "^9.8.0", "eslint-plugin-eslint-plugin": "^6.2.0", "globals": "^15.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1df390f..f581cd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.12 + '@types/node': + specifier: '20' + version: 20.14.14 eslint: specifier: ^9.8.0 version: 9.8.0 diff --git a/src/rules/no-phyisical-properties/rule.ts b/src/rules/no-phyisical-properties/rule.ts index f6a3a41..9f00f1a 100644 --- a/src/rules/no-phyisical-properties/rule.ts +++ b/src/rules/no-phyisical-properties/rule.ts @@ -1,11 +1,14 @@ -import { Rule } from "eslint"; +import { Rule, type AST } from "eslint"; import * as ESTree from "estree"; import type { JSXAttribute } from "estree-jsx"; -import { extractFromNode } from "../../utils/ast.js"; +import { extractTokenFromNode } from "../../utils/ast.js"; import { parseForPhysicalClasses } from "../../utils/tailwind.js"; -const cache = new Map(); +// const cache = new Map< +// /** invalid */ string, +// /** valid */ string +// >(); export const NO_PHYSICAL_CLASSESS = "NO_PHYSICAL_CLASSESS"; @@ -33,19 +36,23 @@ export const noPhysicalProperties: Rule.RuleModule = { const isClassAttribute = ["className", "class"].includes(attr); if (!isClassAttribute) return; - let result = extractFromNode(node); - if (!result) return; + // let result = extractFromNode(node); + // if (!result) return; - result = result.filter((c) => typeof c === "string"); - if (!result.length) return; + // result = result.filter((c) => typeof c === "string"); + // if (!result.length) return; - const classesAsString = result.join(" "); - const cachedValid = cache.get(classesAsString); - if (cachedValid) { - report({ ctx, node, invalid: classesAsString, valid: cachedValid }); - return; - } + // const classesAsString = result.join(" "); + // const cachedValid = cache.get(classesAsString); + // if (cachedValid) { + // console.log("cachedValid", cachedValid); + // report({ ctx, node, invalid: classesAsString, valid: cachedValid }); + // return; + // } + const classesAsString = extractTokenFromNode(node, "checker")?.value; + if (!classesAsString) return; + const classes = classesAsString.split(" "); const parsed = parseForPhysicalClasses(classes); @@ -56,7 +63,7 @@ export const noPhysicalProperties: Rule.RuleModule = { const invalid = parsed.map((p) => p.original).join(" "); const valid = parsed.map((p) => p.valid).join(" "); - cache.set(classesAsString, valid); + // cache.set(classesAsString, valid); report({ ctx, node, invalid, valid }); }, }; @@ -86,10 +93,11 @@ function report({ end: node.loc!.end, }, fix: (fixer) => { - if (node.value?.type === "Literal") { + const token = extractTokenFromNode(node, "fixer"); + if (token?.raw) { return fixer.replaceText( - node.value, - node.value.raw?.replace(invalid, valid) ?? "" + token as AST.Token, + token.raw?.replace(invalid, valid) ); } diff --git a/src/rules/no-phyisical-properties/test.ts b/src/rules/no-phyisical-properties/test.ts index fdb4043..b5c6446 100644 --- a/src/rules/no-phyisical-properties/test.ts +++ b/src/rules/no-phyisical-properties/test.ts @@ -47,6 +47,22 @@ tester.run("no-physical-properties", noPhysicalProperties, { name: "empty class is ok", code: "
", }, + { + name: '{"..."}', + code: "
", + }, + { + name: "{`...`}", + code: "
", + }, + { + name: '{isCondition && "..."}', + code: `
`, + }, + { + name: '{isCondition && "..."}', + code: `
`, + }, ], invalid: [ { @@ -67,6 +83,33 @@ tester.run("no-physical-properties", noPhysicalProperties, { { messageId: NO_PHYSICAL_CLASSESS }, ], }, + { + name: `{"..."}`, + code: `
text
`, + output: `
text
`, + errors: [ + { messageId: NO_PHYSICAL_CLASSESS }, + { messageId: NO_PHYSICAL_CLASSESS }, + ], + }, + { + name: "{`...`}", + code: "
", + output: "
", + errors: [{ messageId: NO_PHYSICAL_CLASSESS }], + }, + { + name: '{isCondition && "..."}', + code: `
`, + output: `
`, + errors: [{ messageId: NO_PHYSICAL_CLASSESS }], + }, + { + name: '{isCondition ? "..." : "..."}', + code: `
`, + // output: `
`, // TODO: Fix this + errors: [{ messageId: NO_PHYSICAL_CLASSESS }], + }, { name: "should report if physical margin properties are used and fix them", code: `
text
`, @@ -112,7 +155,6 @@ tester.run("no-physical-properties", noPhysicalProperties, { output: `
text
`, errors: [{ messageId: NO_PHYSICAL_CLASSESS }], }, - { name: "should report if physical properties are used with the important flag and fix it", code: `
text
`, diff --git a/src/utils/ast.ts b/src/utils/ast.ts index 2eb9f57..937f099 100644 --- a/src/utils/ast.ts +++ b/src/utils/ast.ts @@ -3,6 +3,11 @@ import type { Expression, JSXAttribute } from "estree-jsx"; type Val = string | number | bigint | boolean | RegExp | null | undefined; export function extractFromNode(node: JSXAttribute) { + // const token = extractTokenFromNode(node) + // if (token) { + // return token.raw; + // } + // value: Literal | JSXExpressionContainer | JSXElement | JSXFragment | null const valueType = node.value?.type; if (!valueType) return; @@ -17,6 +22,10 @@ export function extractFromNode(node: JSXAttribute) { if (expression?.type === "JSXEmptyExpression" || !expression) return; + if (expression.type === "Literal") return [expression.value]; + if (expression.type === "TemplateLiteral") + result.push(expression.quasis[0].value.raw); + result.push(...extractFromExpression(expression)); } @@ -46,9 +55,10 @@ function extractFromExpression(expression: Expression) { if (expression.type === "Literal") result.push(expression.value); if (expression.type === "TemplateLiteral") result.push(expression.quasis[0].value.raw); - if (expression.type === "BinaryExpression") { - result.push(...extractFromExpression(expression.left)); - result.push(...extractFromExpression(expression.right)); + if (expression.type === "ConditionalExpression") { + console.log(expression); + // result.push(extractFromExpression(expression.left)); + // result.push(extractFromExpression(expression.right)); } if (expression.type === "CallExpression") { expression.arguments.forEach((arg) => { @@ -69,3 +79,113 @@ function extractFromExpression(expression: Expression) { return result; } + +export function extractTokenFromNode( + node: JSXAttribute, + runner: "checker" | "fixer" +): { type: string; value?: string; raw?: string } | undefined { + // value: Literal | JSXExpressionContainer | JSXElement | JSXFragment | null + const type = node.value?.type; + if (!type) return; + + if (type === "Literal") return validate(node.value); + + if (type === "JSXExpressionContainer") { + const expression = node.value?.expression; + + if (expression?.type === "JSXEmptyExpression" || !expression) return; + + return extractTokenFromExpression(expression, runner); + } + + return; +} + +function extractTokenFromExpression( + expression: Expression, + runner: "checker" | "fixer" +): { type: string; value: string; raw: string } | undefined { + // We care about: + // -> Literal; + // -> TemplateLiteral; + // -> BinaryExpression + // -> CallExpression; + // -> ConditionalExpression; + // -> LogicalExpression; + + const rerun = (expression: Expression) => { + return extractTokenFromExpression(expression, runner); + }; + + const isFixer = runner === "fixer"; + const type = expression.type; + + if (type === "Literal") + return validate({ + ...expression, + value: expression.value || expression.raw, + }); + if (type === "TemplateLiteral") { + return validate({ + ...expression.quasis[0], + value: expression.quasis[0].value.cooked, + raw: "`" + expression.quasis[0].value.raw + "`", + }); + } + + if (type === "LogicalExpression") { + return rerun(expression.right); + } + + if (type === "ConditionalExpression") { + // TODO: Currently, Auto Fixer works on the consequent only + if (isFixer) { + return rerun(expression.consequent); + } else { + const consequent = rerun(expression.consequent); + const alternate = rerun(expression.alternate); + consequent!.value = `${consequent!.value}" : "${alternate!.value}`; + return consequent; + } + } + + console.log("UNIMPLEMENTED: ", type); + + // if (expression.type === "BinaryExpression") { + // result.push(...extractFromExpression(expression.left)); + // result.push(...extractFromExpression(expression.right)); + // } + // if (expression.type === "CallExpression") { + // expression.arguments.forEach((arg) => { + // if (arg.type === "SpreadElement") { + // result.push(...extractFromExpression(arg.argument)); + // } else { + // result.push(...extractFromExpression(arg)); + // } + // }); + // } + // if (expression.type === "ConditionalExpression") { + // result.push(...extractFromExpression(expression.consequent)); + // result.push(...extractFromExpression(expression.alternate)); + // } + // if (expression.type === "LogicalExpression") { + // result.push(...extractFromExpression(expression.right)); + // } + + return; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function validate(result: null | { type: string; value?: any; raw?: string }): + | undefined + | { + type: string; + value: string; + raw: string; + } { + if (!result) return; + if (typeof result.value !== "string") return; + if (typeof result.raw !== "string") return; + + return result as { type: string; value: string; raw: string }; +}