Skip to content

Commit 1b9a179

Browse files
committed
Add jsx-dollar rule to react-x plugin
1 parent e6c7b35 commit 1b9a179

File tree

5 files changed

+162
-8
lines changed

5 files changed

+162
-8
lines changed

apps/website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"pages": [
33
"overview",
44
"---X Rules---",
5+
"jsx-dollar",
56
"jsx-key-before-spread",
67
"jsx-no-comment-textnodes",
78
"jsx-no-duplicate-props",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
title: jsx-dollar
3+
---
4+
5+
**Full Name in `@eslint-react/eslint-plugin`**
6+
7+
```plain copy
8+
@eslint-react/jsx-dollar
9+
```
10+
11+
**Full Name in `eslint-plugin-react-x`**
12+
13+
```plain copy
14+
react-x/jsx-dollar
15+
```
16+
17+
**Features**
18+
19+
`🔧`
20+
21+
## Description
22+
23+
Prevents unnecessary dollar signs (`$`) from being inserted before an expression in JSX.
24+
25+
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.
26+
27+
```tsx
28+
import React from "react";
29+
30+
function MyComponent({ user }) {
31+
return `Hello ${user.name}`;
32+
}
33+
```
34+
35+
When refactored to JSX, it might look like this:
36+
37+
```tsx
38+
import React from "react";
39+
40+
function MyComponent({ user }) {
41+
return <>Hello ${user.name}</>;
42+
}
43+
```
44+
45+
In this example, the `$` before `{user.name}` is unnecessary and will be rendered as part of the output.
46+
47+
## Examples
48+
49+
### Failing
50+
51+
```tsx
52+
import React from "react";
53+
54+
function MyComponent({ user }) {
55+
return <div>Hello ${user.name}</div>;
56+
// ^^^^^^^^^^^^^^
57+
// - Possible unnecessary '$' character before expression.
58+
}
59+
```
60+
61+
### Passing
62+
63+
```tsx
64+
import React from "react";
65+
66+
function MyComponent({ user }) {
67+
return `Hello ${user.name}`;
68+
}
69+
```
70+
71+
```tsx
72+
import React from "react";
73+
74+
function MyComponent({ user }) {
75+
return <div>Hello {user.name}</div>;
76+
}
77+
```
78+
79+
```tsx
80+
import React from "react";
81+
82+
function MyComponent({ price }) {
83+
return <div>${price}</div>;
84+
}
85+
```
86+
87+
### Legitimate uses
88+
89+
If you legitimately need to output a dollar sign before an expression (for example, to display a price), you can wrap it in a template literal or use a string literal.
90+
91+
```tsx
92+
import React from "react";
93+
94+
function MyComponent({ price }) {
95+
// 🟢 Good: This is a legitimate use of the '$' character.
96+
return <div>{`$${price}`}</div>;
97+
}
98+
99+
function AnotherComponent({ price }) {
100+
// 🟢 Good: Another legitimate way to display a price.
101+
return <div>${price}</div>;
102+
}
103+
```
104+
105+
## Implementation
106+
107+
- [Rule Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts)
108+
- [Test Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts)

packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ import rule, { RULE_NAME } from "./jsx-dollar";
55

66
ruleTester.run(RULE_NAME, rule, {
77
invalid: [
8+
{
9+
code: tsx`
10+
const MyComponent = () => <>Hello \${user.name}</>
11+
`,
12+
errors: [
13+
{
14+
messageId: "jsxDollar",
15+
suggestions: [
16+
{
17+
messageId: "removeDollarSign",
18+
output: tsx`
19+
const MyComponent = () => <>Hello {user.name}</>
20+
`,
21+
},
22+
],
23+
},
24+
],
25+
},
826
{
927
code: tsx`
1028
const App = (props) => {
@@ -30,6 +48,9 @@ ruleTester.run(RULE_NAME, rule, {
3048
],
3149
valid: [
3250
...allValid,
51+
tsx`
52+
const MyComponent = () => \`Hello \${user.name}\`
53+
`,
3354
tsx`
3455
const App = (props) => {
3556
return [<div key="1">1</div>]
@@ -40,5 +61,25 @@ ruleTester.run(RULE_NAME, rule, {
4061
return <div>Hello $</div>;
4162
};
4263
`,
64+
tsx`
65+
const App = (props) => {
66+
return <div>Hello {props.name}</div>;
67+
};
68+
`,
69+
tsx`
70+
import React from "react";
71+
72+
function MyComponent({ price }) {
73+
// 🟢 Good: This is a legitimate use of the '$' character.
74+
return <div>{\`$\${price}\`}</div>;
75+
}
76+
`,
77+
tsx`
78+
import React from "react";
79+
function AnotherComponent({ price }) {
80+
// 🟢 Good: Another legitimate way to display a price.
81+
return <div>\${price}</div>;
82+
}
83+
`,
4384
],
4485
});

packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as AST from "@eslint-react/ast";
21
import type { RuleContext, RuleFeature } from "@eslint-react/shared";
32
import type { TSESTree } from "@typescript-eslint/types";
43
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
@@ -11,7 +10,9 @@ export const RULE_NAME = "jsx-dollar";
1110

1211
export const RULE_FEATURES = [] as const satisfies RuleFeature[];
1312

14-
export type MessageID = CamelCase<typeof RULE_NAME> | "removeDollarSign";
13+
export type MessageID = CamelCase<typeof RULE_NAME> | RuleSuggestMessageID;
14+
15+
export type RuleSuggestMessageID = "removeDollarSign";
1516

1617
export default createRule<[], MessageID>({
1718
meta: {
@@ -23,8 +24,7 @@ export default createRule<[], MessageID>({
2324
fixable: "code",
2425
hasSuggestions: true,
2526
messages: {
26-
jsxDollar:
27-
"Possible misused dollar sign in text node. If you want to explicitly display '$' character i.e. show price, you can use template literals.",
27+
jsxDollar: "Possible unnecessary '$' character before expression.",
2828
removeDollarSign: "Remove the dollar sign '$' before the expression.",
2929
},
3030
schema: [],
@@ -35,10 +35,14 @@ export default createRule<[], MessageID>({
3535
});
3636

3737
export function create(context: RuleContext<MessageID, []>): RuleListener {
38+
/**
39+
* Visitor function for JSXElement and JSXFragment nodes
40+
* @param node The JSXElement or JSXFragment node to be checked
41+
*/
3842
const visitorFunction = (node: TSESTree.JSXElement | TSESTree.JSXFragment) => {
3943
for (const [index, child] of node.children.entries()) {
40-
if (child.type !== T.JSXText) continue;
41-
if (!child.raw.endsWith("$")) continue;
44+
if (child.type !== T.JSXText || child.raw === "$" || !child.raw.endsWith("$")) continue;
45+
// Ensure the next sibling is a JSXExpressionContainer
4246
if (node.children[index + 1]?.type !== T.JSXExpressionContainer) continue;
4347
context.report({
4448
messageId: "jsxDollar",

packages/plugins/eslint-plugin/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ ESLint React is not affiliated with Meta Corporation or [facebook/react](https:/
186186

187187
Contributions are welcome!
188188

189-
Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/main/.github/CONTRIBUTING.md).
189+
Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/jsx-dollar/.github/CONTRIBUTING.md).
190190

191191
## License
192192

193-
This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/main/LICENSE) file for details.
193+
This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/jsx-dollar/LICENSE) file for details.

0 commit comments

Comments
 (0)