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"], }, });