Skip to content

Commit

Permalink
feat: CallExpression, ArrayExpression, ObjectExpression (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
AhmedBaset authored Aug 5, 2024
2 parents 8efcc85 + 3abb3bd commit 38cd839
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/itchy-knives-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-rtl-friendly": patch
---

feat: CallExpression, ArrayExpression, ObjectExpression
9 changes: 6 additions & 3 deletions .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: |
npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
pnpm changeset version --snapshot ${{ github.event.number }}
npm publish --access public --tag canary --no-git-checks
# npm publish --access public --tag canary --no-git-checks
echo "published=true" >> "$GITHUB_OUTPUT"
echo "version=$(npm pkg get version | sed -e 's/^"//;s/"$//')" >> "$GITHUB_OUTPUT"
env:
Expand All @@ -56,11 +56,14 @@ jobs:
🚀 A new Canary version has been released
You can install it by running:
\`\`\`bash
pnpm add eslint-plugin-rtl-friendly@${{ steps.publish.outputs.version }} -D
\`\`\`
`.replace(/\s+/g, ' ')
})
`.split("\n")
.map(l => l.trim())
.join("\n")
})
github.issues.removeLabel({
issue_number,
Expand Down
6 changes: 0 additions & 6 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import js from "@eslint/js";
import eslintPlugin from "eslint-plugin-eslint-plugin";
import globals from "globals";
import { config, configs } from "typescript-eslint";

import rtlFriendly from "./dist/index.js";
Expand All @@ -11,11 +10,6 @@ export default config(
{
ignores: ["dist/**/*"],
},
{
languageOptions: {
globals: globals.node,
},
},
eslintPlugin.configs["flat/recommended"],
js.configs.recommended,
...configs.recommended,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"lint": "eslint .",
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest --coverage",
"test:coverage": "vitest --run --coverage",
"prepublishOnly": "npm run build",
"gen-e2e": "tsx scripts/generate-e2e"
},
Expand Down
84 changes: 83 additions & 1 deletion src/rules/no-phyisical-properties/test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vitest from "vitest"
import { RuleTester } from "@typescript-eslint/rule-tester";
import * as vitest from "vitest";
import { NO_PHYSICAL_CLASSESS, noPhysicalProperties } from "./rule";

RuleTester.afterAll = vitest.afterAll;
Expand Down Expand Up @@ -130,6 +130,88 @@ tester.run("no-physical-properties", noPhysicalProperties, {
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: '{cn("...")}',
code: `<div className={cn("pl-1 text-right mr-2")} />`,
output: `<div className={cn("ps-1 text-end me-2")} />`,
errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
},
{
name: '{cn(isCondition && "...")}',
code: `<div className={cn(isCondition && "pl-1 text-right mr-2")} />`,
output: `<div className={cn(isCondition && "ps-1 text-end me-2")} />`,
errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
},
{
name: '{cn(isCondition ? "..." : "...")}',
code: '<div className={cn(isCondition ? "pl-1 text-left" : `pr-1 text-right`)} />',
output:
'<div className={cn(isCondition ? "ps-1 text-start" : `pe-1 text-end`)} />',
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: '{cn("...", isCondition && "...")}',
code: `<div className={cn("rounded-l-md", isCondition && "pl-1 text-right mr-2")} />`,
output: `<div className={cn("rounded-s-md", isCondition && "ps-1 text-end me-2")} />`,
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: '{cn(["...", "..."])}',
code: `<div className={cn(["pl-1 text-right", "mr-2"])} />`,
output: `<div className={cn(["ps-1 text-end", "me-2"])} />`,
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: '{cn(["...", ...["..."]])}',
code: `<div className={cn(["pl-1"], [["left-0"]], ...["text-right", "mr-2"])} />`,
output: `<div className={cn(["ps-1"], [["start-0"]], ...["text-end", "me-2"])} />`,
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: '{cn({"...": true})}',
code: `<div className={cn({"pl-1 text-right": true})} />`,
output: `<div className={cn({"ps-1 text-end": true})} />`,
errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
},
{
name: '{cn({"...": "..."}, isCondition && {"...": "..."})}',
code: `<div className={cn({"pl-1 text-right": "mr-2"}, isCondition && {"pl-2": "text-left"})} />`,
output: `<div className={cn({"ps-1 text-end": "me-2"}, isCondition && {"ps-2": "text-start"})} />`,
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: "clsx('...', [1 && '...', { ...: false, ...: null }, is && ['...', ['...']]], '...')",
code: `<div className={clsx('pl-1', [1 && 'text-right', { 'text-left': false, 'mr-2': null }, is && ['pr-2', ['pl-2']]], 'mr-1')} />`,
output: `<div className={clsx('ps-1', [1 && 'text-end', { 'text-start': false, 'me-2': null }, is && ['pe-2', ['ps-2']]], 'me-1')} />`,
errors: [
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
{
name: "should report if physical margin properties are used and fix them",
code: `<div className="ml-1 mr-2">text</div>`,
Expand Down
165 changes: 132 additions & 33 deletions src/utils/ast.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { TSESTree } from "@typescript-eslint/utils";
import { TSESTree } from "@typescript-eslint/utils";

const unimplemented = new Set<string>();

export type Token = (
| TSESTree.JSXAttribute
Expand All @@ -14,26 +16,27 @@ export function extractTokenFromNode(
runner: "checker" | "fixer"
): (Token | undefined | null)[] {
// value: Literal | JSXExpressionContainer | JSXElement | JSXFragment | null
const type = node.value?.type;
if (!type) return [];

const nodeValue = node.value;
const value = node.value;
if (!value) return [];

if (isStringLiteral(nodeValue))
return format(
nodeValue,
(n) => n.value,
(n) => n.raw
);
if (value?.type === "Literal") {
if (typeof value.value !== "string") return []; // boolean, number, null, undefined, etc...
return format(value, value.value, value.raw);
}

if (type === "JSXExpressionContainer") {
const expression = node.value?.expression;
if (value.type === "JSXExpressionContainer") {
const expression = value?.expression;

if (!expression || expression?.type === "JSXEmptyExpression") return [];

return extractTokenFromExpression(expression, runner);
}

if (value.type === "JSXElement" || value.type === "JSXSpreadChild") {
// JSXElement is like =>
return [];
}

return [];
}

Expand All @@ -54,34 +57,117 @@ function extractTokenFromExpression(
};

// const isFixer = runner === "fixer";
const type = exp.type;

if (isStringLiteral(exp))
if (is(exp, "Literal")) {
if (typeof exp.value !== "string") return []; // boolean, number, null, undefined, etc...

return format(
exp,
() => exp.value,
() => exp.raw
);
}

if (exp?.type === "TemplateLiteral") {
if (is(exp, "TemplateLiteral")) {
return format(
exp.quasis,
(q) => q.value.cooked,
(q) => `\`${q.value.raw}\``
);
}

if (exp.type === "LogicalExpression") {
if (is(exp, "LogicalExpression")) {
// isCondition && "..."
return rerun(exp.right);
}

if (exp.type === "ConditionalExpression") {
if (is(exp, "ConditionalExpression")) {
return [...rerun(exp.consequent), ...rerun(exp.alternate)];
}

if (is(exp, "ArrayExpression")) {
return exp.elements.flatMap((el) => {
if (!el) return [];

if (el.type === "SpreadElement") return rerun(el.argument);

return rerun(el);
});
}

// console.log("UNIMPLEMENTED: ", type);
if (is(exp, "ObjectExpression")) {
return exp.properties.flatMap((prop) => {
if (prop.type === "SpreadElement") return rerun(prop.argument);

return [prop.key, prop.value].flatMap((el) => {
if (
el.type === "AssignmentPattern" ||
el.type === "TSEmptyBodyFunctionExpression"
)
return [];

return rerun(el);
});
});
}

if (is(exp, "CallExpression")) {
return exp.arguments.flatMap((arg) => {
if (arg.type === "SpreadElement") {
return rerun(arg.argument);
}

return rerun(arg);
});
}

// if (
// is(exp, "BinaryExpression") ||
// is(exp, "Identifier") ||
// is(exp, "MemberExpression") ||
// is(exp, "TaggedTemplateExpression")
// ) {
// // Will be implemented
// return [];
// }

// if ((unsupported as typeof exp.type[]).includes(exp.type)) {
// if (
// is(exp, "ArrayPattern") ||
// is(exp, "ObjectPattern") ||
// is(exp, "ArrowFunctionExpression") ||
// is(exp, "AssignmentExpression") ||
// is(exp, "AwaitExpression") ||
// is(exp, "ChainExpression") ||
// is(exp, "ClassExpression") ||
// is(exp, "FunctionExpression") ||
// is(exp, "ImportExpression") ||
// is(exp, "JSXElement") ||
// is(exp, "JSXFragment") ||
// is(exp, "MetaProperty") ||
// is(exp, "NewExpression") ||
// is(exp, "SequenceExpression") ||
// is(exp, "Super") ||
// is(exp, "ThisExpression") ||
// is(exp, "UnaryExpression") ||
// is(exp, "UpdateExpression") ||
// is(exp, "VariableDeclaration") ||
// is(exp, "VariableDeclarator") ||
// is(exp, "WhileStatement") ||
// is(exp, "YieldExpression") ||
// is(exp, "TSAsExpression") ||
// is(exp, "TSInstantiationExpression") ||
// is(exp, "TSNonNullExpression") ||
// is(exp, "TSSatisfiesExpression") ||
// is(exp, "TSTypeAssertion")
// ) {
// return [];
// }

if (!unimplemented.has(exp.type)) {
console.log("Unimplemented: ", exp.type, exp);
unimplemented.add(exp.type);
}

// if (expression.type === "BinaryExpression") {
// result.push(...extractFromExpression(expression.left));
Expand Down Expand Up @@ -113,29 +199,42 @@ function format<
| TSESTree.Expression
| TSESTree.TemplateElement,
>(
nodeOrToken: T | T[],
getValue: (t: T) => string,
getRaw: (t: T) => string
token: T | T[],
getValue: string | ((t: T) => string),
getRaw: string | ((t: T) => string)
): (T & { getValue: () => string; getRaw: () => string })[] {
if (Array.isArray(nodeOrToken)) {
return nodeOrToken.map((t) => ({
if (Array.isArray(token)) {
return token.map((t) => ({
...t,
getValue: () => getValue(t),
getRaw: getRaw ? () => getRaw(t) : () => getValue(t),
getValue: () => callOrValue(getValue, t),
getRaw: () => callOrValue(getRaw, t),
}));
}

return [
{
...nodeOrToken,
getValue: () => getValue(nodeOrToken),
getRaw: () => (getRaw ?? getValue)(nodeOrToken),
...token,
getValue: () => callOrValue(getValue, token),
getRaw: () => callOrValue(getRaw, token),
},
] as const;
}

function isStringLiteral(
value: TSESTree.JSXAttribute["value"] | TSESTree.Expression
): value is TSESTree.StringLiteral {
return value?.type === "Literal" && typeof value?.value === "string";
function callOrValue<T extends string>(func: T | (() => T)): T;
function callOrValue<T extends string, P>(
func: T | ((arg: P) => T),
param: P
): T;
function callOrValue<T extends string, P>(
func: T | ((arg: P) => T),
param?: P
): T {
return typeof func === "function" ? func(param!) : func;
}

function is<T extends TSESTree.AST_NODE_TYPES>(
exp: TSESTree.Expression,
type: `${T}`
): exp is Extract<TSESTree.Expression, { type: T }> {
return exp.type === type;
}
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
cacheDir: path.resolve(__dirname, "./node_modules/.cache/vitest"),
test: {
include: ["src/**/test.ts"]
include: ["src/**/test.ts"],
},
});

0 comments on commit 38cd839

Please sign in to comment.