diff --git a/.changeset/itchy-knives-teach.md b/.changeset/itchy-knives-teach.md
new file mode 100644
index 0000000..f44fd14
--- /dev/null
+++ b/.changeset/itchy-knives-teach.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-rtl-friendly": patch
+---
+
+feat: CallExpression, ArrayExpression, ObjectExpression
diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml
index 2c0ced2..93f04f3 100644
--- a/.github/workflows/release-canary.yml
+++ b/.github/workflows/release-canary.yml
@@ -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:
@@ -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,
diff --git a/eslint.config.js b/eslint.config.js
index afaf91c..91e69cd 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -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";
@@ -11,11 +10,6 @@ export default config(
{
ignores: ["dist/**/*"],
},
- {
- languageOptions: {
- globals: globals.node,
- },
- },
eslintPlugin.configs["flat/recommended"],
js.configs.recommended,
...configs.recommended,
diff --git a/package.json b/package.json
index 10f50d4..47e5dc8 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/rules/no-phyisical-properties/test.ts b/src/rules/no-phyisical-properties/test.ts
index 0fb924f..a7eb3ab 100644
--- a/src/rules/no-phyisical-properties/test.ts
+++ b/src/rules/no-phyisical-properties/test.ts
@@ -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;
@@ -130,6 +130,88 @@ tester.run("no-physical-properties", noPhysicalProperties, {
{ messageId: NO_PHYSICAL_CLASSESS },
],
},
+ {
+ name: '{cn("...")}',
+ code: `
`,
+ output: ``,
+ errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
+ },
+ {
+ name: '{cn(isCondition && "...")}',
+ code: ``,
+ output: ``,
+ errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
+ },
+ {
+ name: '{cn(isCondition ? "..." : "...")}',
+ code: '',
+ output:
+ '',
+ errors: [
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ ],
+ },
+ {
+ name: '{cn("...", isCondition && "...")}',
+ code: ``,
+ output: ``,
+ errors: [
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ ],
+ },
+ {
+ name: '{cn(["...", "..."])}',
+ code: ``,
+ output: ``,
+ errors: [
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ ],
+ },
+ {
+ name: '{cn(["...", ...["..."]])}',
+ code: ``,
+ output: ``,
+ errors: [
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ ],
+ },
+ {
+ name: '{cn({"...": true})}',
+ code: ``,
+ output: ``,
+ errors: [{ messageId: NO_PHYSICAL_CLASSESS }],
+ },
+ {
+ name: '{cn({"...": "..."}, isCondition && {"...": "..."})}',
+ code: ``,
+ output: ``,
+ errors: [
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ { messageId: NO_PHYSICAL_CLASSESS },
+ ],
+ },
+ {
+ name: "clsx('...', [1 && '...', { ...: false, ...: null }, is && ['...', ['...']]], '...')",
+ code: ``,
+ output: ``,
+ 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: `text
`,
diff --git a/src/utils/ast.ts b/src/utils/ast.ts
index e3c0859..9b9e919 100644
--- a/src/utils/ast.ts
+++ b/src/utils/ast.ts
@@ -1,4 +1,6 @@
-import type { TSESTree } from "@typescript-eslint/utils";
+import { TSESTree } from "@typescript-eslint/utils";
+
+const unimplemented = new Set();
export type Token = (
| TSESTree.JSXAttribute
@@ -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 [];
}
@@ -54,16 +57,18 @@ 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,
@@ -71,17 +76,98 @@ function extractTokenFromExpression(
);
}
- 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));
@@ -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(func: T | (() => T)): T;
+function callOrValue(
+ func: T | ((arg: P) => T),
+ param: P
+): T;
+function callOrValue(
+ func: T | ((arg: P) => T),
+ param?: P
+): T {
+ return typeof func === "function" ? func(param!) : func;
+}
+
+function is(
+ exp: TSESTree.Expression,
+ type: `${T}`
+): exp is Extract {
+ return exp.type === type;
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 024d121..50fba4a 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -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"],
},
});