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.