Skip to content

Commit d9181ef

Browse files
authored
Add react-x/jsx-dollar rule, closes #1300 (#1302)
1 parent 6088f95 commit d9181ef

File tree

8 files changed

+304
-3
lines changed

8 files changed

+304
-3
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",

apps/website/content/docs/rules/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ full: true
2727

2828
| Rule || 🌟 | Description | `react` |
2929
| :----------------------------------------------------------------------------------- | :-----: | :-------: | :-------------------------------------------------------------------------------------------------- | :------: |
30+
| [`jsx-dollar`](./jsx-dollar) | 0️⃣ 0️⃣ | | Prevents unnecessary dollar signs (`$`) from being inserted before an expression in JSX | |
3031
| [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ 1️⃣ | | Enforces that the 'key' prop is placed before the spread prop in JSX elements | |
3132
| [`jsx-no-comment-textnodes`](./jsx-no-comment-textnodes) | 1️⃣ 1️⃣ | | Prevents comments from being inserted as text nodes | |
3233
| [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ 1️⃣ | | Disallow duplicate props in JSX elements | |

dprint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"packages/**/docs"
2626
],
2727
"plugins": [
28-
"https://plugins.dprint.dev/typescript-0.95.11.wasm",
28+
"https://plugins.dprint.dev/typescript-0.95.12.wasm",
2929
"https://plugins.dprint.dev/json-0.21.0.wasm",
3030
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
3131
"https://plugins.dprint.dev/toml-0.7.0.wasm",

packages/plugins/eslint-plugin-react-x/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CompatiblePlugin } from "@eslint-react/shared";
22

33
import { name, version } from "../package.json";
44

5+
import jsxDollar from "./rules/jsx-dollar";
56
import jsxKeyBeforeSpread from "./rules/jsx-key-before-spread";
67
import jsxNoCommentTextnodes from "./rules/jsx-no-comment-textnodes";
78
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
@@ -71,6 +72,7 @@ export const plugin: CompatiblePlugin = {
7172
version,
7273
},
7374
rules: {
75+
"jsx-dollar": jsxDollar,
7476
"jsx-key-before-spread": jsxKeyBeforeSpread,
7577
"jsx-no-comment-textnodes": jsxNoCommentTextnodes,
7678
"jsx-no-duplicate-props": jsxNoDuplicateProps,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
```tsx
62+
import React from "react";
63+
64+
function MyComponent({ user }) {
65+
return <div>${user.name} is your name</div>;
66+
// ^
67+
// - Possible unnecessary '$' character before expression.
68+
}
69+
```
70+
71+
### Passing
72+
73+
```tsx
74+
import React from "react";
75+
76+
function MyComponent({ user }) {
77+
return `Hello ${user.name}`;
78+
}
79+
```
80+
81+
```tsx
82+
import React from "react";
83+
84+
function MyComponent({ user }) {
85+
return <div>Hello {user.name}</div>;
86+
}
87+
```
88+
89+
```tsx
90+
import React from "react";
91+
92+
function MyComponent({ price }) {
93+
// 🟢 Good: If there are only two children (the dollar sign and the expression) it doesn't seem to be split from a template literal
94+
return <div>${price}</div>;
95+
}
96+
```
97+
98+
## Implementation
99+
100+
- [Rule Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts)
101+
- [Test Source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.spec.ts)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import tsx from "dedent";
2+
3+
import { allValid, ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./jsx-dollar";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
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+
},
26+
{
27+
code: tsx`
28+
const App = (props) => {
29+
return <div>Hello \${props.name}</div>;
30+
};
31+
`,
32+
errors: [
33+
{
34+
messageId: "jsxDollar",
35+
suggestions: [
36+
{
37+
messageId: "removeDollarSign",
38+
output: tsx`
39+
const App = (props) => {
40+
return <div>Hello {props.name}</div>;
41+
};
42+
`,
43+
},
44+
],
45+
},
46+
],
47+
},
48+
{
49+
code: tsx`
50+
const App = (props) => {
51+
return <div>\${props.name} is your name</div>;
52+
};
53+
`,
54+
errors: [
55+
{
56+
messageId: "jsxDollar",
57+
suggestions: [
58+
{
59+
messageId: "removeDollarSign",
60+
output: tsx`
61+
const App = (props) => {
62+
return <div>{props.name} is your name</div>;
63+
};
64+
`,
65+
},
66+
],
67+
},
68+
],
69+
},
70+
{
71+
code: tsx`
72+
const App = (props) => {
73+
return <div>Hello \${props.name} is your name</div>;
74+
};
75+
`,
76+
errors: [
77+
{
78+
messageId: "jsxDollar",
79+
suggestions: [
80+
{
81+
messageId: "removeDollarSign",
82+
output: tsx`
83+
const App = (props) => {
84+
return <div>Hello {props.name} is your name</div>;
85+
};
86+
`,
87+
},
88+
],
89+
},
90+
],
91+
},
92+
],
93+
valid: [
94+
...allValid,
95+
tsx`
96+
const MyComponent = () => \`Hello \${user.name}\`
97+
`,
98+
tsx`
99+
const App = (props) => {
100+
return [<div key="1">1</div>]
101+
};
102+
`,
103+
tsx`
104+
const App = (props) => {
105+
return <div>Hello $</div>;
106+
};
107+
`,
108+
tsx`
109+
const App = (props) => {
110+
return <div>Hello {props.name}</div>;
111+
};
112+
`,
113+
tsx`
114+
import React from "react";
115+
116+
function MyComponent({ price }) {
117+
// 🟢 Good: This is a legitimate use of the '$' character.
118+
return <div>{\`$\${price}\`}</div>;
119+
}
120+
`,
121+
tsx`
122+
import React from "react";
123+
function AnotherComponent({ price }) {
124+
// 🟢 Good: Another legitimate way to display a price.
125+
return <div>\${price}</div>;
126+
}
127+
`,
128+
],
129+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { RuleContext, RuleFeature } from "@eslint-react/shared";
2+
import type { TSESTree } from "@typescript-eslint/types";
3+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
4+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
5+
import type { CamelCase } from "string-ts";
6+
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "jsx-dollar";
10+
11+
export const RULE_FEATURES = [] as const satisfies RuleFeature[];
12+
13+
export type MessageID = CamelCase<typeof RULE_NAME> | RuleSuggestMessageID;
14+
15+
export type RuleSuggestMessageID = "removeDollarSign";
16+
17+
export default createRule<[], MessageID>({
18+
meta: {
19+
type: "problem",
20+
docs: {
21+
description: "Prevents dollar signs from being inserted as text nodes before expressions.",
22+
[Symbol.for("rule_features")]: RULE_FEATURES,
23+
},
24+
fixable: "code",
25+
hasSuggestions: true,
26+
messages: {
27+
jsxDollar: "Possible unnecessary '$' character before expression.",
28+
removeDollarSign: "Remove the dollar sign '$' before the expression.",
29+
},
30+
schema: [],
31+
},
32+
name: RULE_NAME,
33+
create,
34+
defaultOptions: [],
35+
});
36+
37+
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+
*/
42+
const visitorFunction = (node: TSESTree.JSXElement | TSESTree.JSXFragment) => {
43+
for (const [index, child] of node.children.entries()) {
44+
if (child.type !== T.JSXText || !child.value.endsWith("$")) continue;
45+
// Ensure the next sibling is a JSXExpressionContainer
46+
if (node.children[index + 1]?.type !== T.JSXExpressionContainer) continue;
47+
// Skip if there are only two children (the dollar sign and the expression) it doesn't seem to be split from a template literal
48+
if (child.value === "$" && node.children.length === 2) continue;
49+
context.report({
50+
messageId: "jsxDollar",
51+
node: child,
52+
suggest: [
53+
{
54+
messageId: "removeDollarSign",
55+
fix(fixer) {
56+
return fixer.removeRange([child.range[1] - 1, child.range[1]]);
57+
},
58+
},
59+
],
60+
});
61+
}
62+
};
63+
return {
64+
JSXElement: visitorFunction,
65+
JSXFragment: visitorFunction,
66+
};
67+
}

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)