diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 153f03a9a..81f730c27 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -2,6 +2,7 @@ "pages": [ "overview", "---X Rules---", + "jsx-dollar", "jsx-key-before-spread", "jsx-no-comment-textnodes", "jsx-no-duplicate-props", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 1bfd8415d..61e596a26 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -27,6 +27,7 @@ full: true | Rule | ✅ | 🌟 | Description | `react` | | :----------------------------------------------------------------------------------- | :-----: | :-------: | :-------------------------------------------------------------------------------------------------- | :------: | +| [`jsx-dollar`](./jsx-dollar) | 0️⃣ 0️⃣ | | Prevents unnecessary dollar signs (`$`) from being inserted before an expression in JSX | | | [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ 1️⃣ | | Enforces that the 'key' prop is placed before the spread prop in JSX elements | | | [`jsx-no-comment-textnodes`](./jsx-no-comment-textnodes) | 1️⃣ 1️⃣ | | Prevents comments from being inserted as text nodes | | | [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ 1️⃣ | | Disallow duplicate props in JSX elements | | diff --git a/dprint.json b/dprint.json index 7ef61712f..5c162bf15 100644 --- a/dprint.json +++ b/dprint.json @@ -25,7 +25,7 @@ "packages/**/docs" ], "plugins": [ - "https://plugins.dprint.dev/typescript-0.95.11.wasm", + "https://plugins.dprint.dev/typescript-0.95.12.wasm", "https://plugins.dprint.dev/json-0.21.0.wasm", "https://plugins.dprint.dev/markdown-0.20.0.wasm", "https://plugins.dprint.dev/toml-0.7.0.wasm", diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index b50461c47..bb90cd05a 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -2,6 +2,7 @@ import type { CompatiblePlugin } from "@eslint-react/shared"; import { name, version } from "../package.json"; +import jsxDollar from "./rules/jsx-dollar"; import jsxKeyBeforeSpread from "./rules/jsx-key-before-spread"; import jsxNoCommentTextnodes from "./rules/jsx-no-comment-textnodes"; import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props"; @@ -71,6 +72,7 @@ export const plugin: CompatiblePlugin = { version, }, rules: { + "jsx-dollar": jsxDollar, "jsx-key-before-spread": jsxKeyBeforeSpread, "jsx-no-comment-textnodes": jsxNoCommentTextnodes, "jsx-no-duplicate-props": jsxNoDuplicateProps, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.mdx b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.mdx new file mode 100644 index 000000000..83aeb1cd1 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.mdx @@ -0,0 +1,101 @@ +--- +title: jsx-dollar +--- + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/jsx-dollar +``` + +**Full Name in `eslint-plugin-react-x`** + +```plain copy +react-x/jsx-dollar +``` + +**Features** + +`🔧` + +## Description + +Prevents unnecessary dollar signs (`$`) from being inserted before an expression in JSX. + +This can happen when refactoring from a template literal to JSX and forgetting to remove the dollar sign. This results in an unintentional `$` being rendered in the output. + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return `Hello ${user.name}`; +} +``` + +When refactored to JSX, it might look like this: + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return <>Hello ${user.name}; +} +``` + +In this example, the `$` before `{user.name}` is unnecessary and will be rendered as part of the output. + +## Examples + +### Failing + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return
Hello ${user.name}
; + // ^^^^^^^ + // - Possible unnecessary '$' character before expression. +} +``` + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return
${user.name} is your name
; + // ^ + // - Possible unnecessary '$' character before expression. +} +``` + +### Passing + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return `Hello ${user.name}`; +} +``` + +```tsx +import React from "react"; + +function MyComponent({ user }) { + return
Hello {user.name}
; +} +``` + +```tsx +import React from "react"; + +function MyComponent({ price }) { + // 🟢 Good: If there are only two children (the dollar sign and the expression) it doesn't seem to be split from a template literal + return
${price}
; +} +``` + +## Implementation + +- [Rule Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts) +- [Test Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts new file mode 100644 index 000000000..a0c402d3c --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts @@ -0,0 +1,129 @@ +import tsx from "dedent"; + +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./jsx-dollar"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: tsx` + const MyComponent = () => <>Hello \${user.name} + `, + errors: [ + { + messageId: "jsxDollar", + suggestions: [ + { + messageId: "removeDollarSign", + output: tsx` + const MyComponent = () => <>Hello {user.name} + `, + }, + ], + }, + ], + }, + { + code: tsx` + const App = (props) => { + return
Hello \${props.name}
; + }; + `, + errors: [ + { + messageId: "jsxDollar", + suggestions: [ + { + messageId: "removeDollarSign", + output: tsx` + const App = (props) => { + return
Hello {props.name}
; + }; + `, + }, + ], + }, + ], + }, + { + code: tsx` + const App = (props) => { + return
\${props.name} is your name
; + }; + `, + errors: [ + { + messageId: "jsxDollar", + suggestions: [ + { + messageId: "removeDollarSign", + output: tsx` + const App = (props) => { + return
{props.name} is your name
; + }; + `, + }, + ], + }, + ], + }, + { + code: tsx` + const App = (props) => { + return
Hello \${props.name} is your name
; + }; + `, + errors: [ + { + messageId: "jsxDollar", + suggestions: [ + { + messageId: "removeDollarSign", + output: tsx` + const App = (props) => { + return
Hello {props.name} is your name
; + }; + `, + }, + ], + }, + ], + }, + ], + valid: [ + ...allValid, + tsx` + const MyComponent = () => \`Hello \${user.name}\` + `, + tsx` + const App = (props) => { + return [
1
] + }; + `, + tsx` + const App = (props) => { + return
Hello $
; + }; + `, + tsx` + const App = (props) => { + return
Hello {props.name}
; + }; + `, + tsx` + import React from "react"; + + function MyComponent({ price }) { + // 🟢 Good: This is a legitimate use of the '$' character. + return
{\`$\${price}\`}
; + } + `, + tsx` + import React from "react"; + function AnotherComponent({ price }) { + // 🟢 Good: Another legitimate way to display a price. + return
\${price}
; + } + `, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts new file mode 100644 index 000000000..52377ce42 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts @@ -0,0 +1,67 @@ +import type { RuleContext, RuleFeature } from "@eslint-react/shared"; +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "jsx-dollar"; + +export const RULE_FEATURES = [] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase | RuleSuggestMessageID; + +export type RuleSuggestMessageID = "removeDollarSign"; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "Prevents dollar signs from being inserted as text nodes before expressions.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + fixable: "code", + hasSuggestions: true, + messages: { + jsxDollar: "Possible unnecessary '$' character before expression.", + removeDollarSign: "Remove the dollar sign '$' before the expression.", + }, + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +export function create(context: RuleContext): RuleListener { + /** + * Visitor function for JSXElement and JSXFragment nodes + * @param node The JSXElement or JSXFragment node to be checked + */ + const visitorFunction = (node: TSESTree.JSXElement | TSESTree.JSXFragment) => { + for (const [index, child] of node.children.entries()) { + if (child.type !== T.JSXText || !child.value.endsWith("$")) continue; + // Ensure the next sibling is a JSXExpressionContainer + if (node.children[index + 1]?.type !== T.JSXExpressionContainer) continue; + // Skip if there are only two children (the dollar sign and the expression) it doesn't seem to be split from a template literal + if (child.value === "$" && node.children.length === 2) continue; + context.report({ + messageId: "jsxDollar", + node: child, + suggest: [ + { + messageId: "removeDollarSign", + fix(fixer) { + return fixer.removeRange([child.range[1] - 1, child.range[1]]); + }, + }, + ], + }); + } + }; + return { + JSXElement: visitorFunction, + JSXFragment: visitorFunction, + }; +} diff --git a/packages/plugins/eslint-plugin/README.md b/packages/plugins/eslint-plugin/README.md index fcb6b6a91..1493c4e40 100644 --- a/packages/plugins/eslint-plugin/README.md +++ b/packages/plugins/eslint-plugin/README.md @@ -186,8 +186,8 @@ ESLint React is not affiliated with Meta Corporation or [facebook/react](https:/ Contributions are welcome! -Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/main/.github/CONTRIBUTING.md). +Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/jsx-dollar/.github/CONTRIBUTING.md). ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/main/LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/jsx-dollar/LICENSE) file for details.