Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"pages": [
"overview",
"---X Rules---",
"jsx-dollar",
"jsx-key-before-spread",
"jsx-no-comment-textnodes",
"jsx-no-duplicate-props",
Expand Down
1 change: 1 addition & 0 deletions apps/website/content/docs/rules/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down
2 changes: 1 addition & 1 deletion dprint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
108 changes: 108 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
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 <div>Hello ${user.name}</div>;
// ^^^^^^^^^^^^^^
// - 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 <div>Hello {user.name}</div>;
}
```

```tsx
import React from "react";

function MyComponent({ price }) {
return <div>${price}</div>;
}
```

### Legitimate uses

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.

```tsx
import React from "react";

function MyComponent({ price }) {
// 🟢 Good: This is a legitimate use of the '$' character.
return <div>{`$${price}`}</div>;
}

function AnotherComponent({ price }) {
// 🟢 Good: Another legitimate way to display a price.
return <div>${price}</div>;
}
```

## 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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 <div>Hello \${props.name}</div>;
};
`,
errors: [
{
messageId: "jsxDollar",
suggestions: [
{
messageId: "removeDollarSign",
output: tsx`
const App = (props) => {
return <div>Hello {props.name}</div>;
};
`,
},
],
},
],
},
],
valid: [
...allValid,
tsx`
const MyComponent = () => \`Hello \${user.name}\`
`,
tsx`
const App = (props) => {
return [<div key="1">1</div>]
};
`,
tsx`
const App = (props) => {
return <div>Hello $</div>;
};
`,
tsx`
const App = (props) => {
return <div>Hello {props.name}</div>;
};
`,
tsx`
import React from "react";

function MyComponent({ price }) {
// 🟢 Good: This is a legitimate use of the '$' character.
return <div>{\`$\${price}\`}</div>;
}
`,
tsx`
import React from "react";
function AnotherComponent({ price }) {
// 🟢 Good: Another legitimate way to display a price.
return <div>\${price}</div>;
}
`,
],
});
65 changes: 65 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/rules/jsx-dollar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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<typeof RULE_NAME> | 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<MessageID, []>): 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.raw === "$" || !child.raw.endsWith("$")) continue;
// Ensure the next sibling is a JSXExpressionContainer
if (node.children[index + 1]?.type !== T.JSXExpressionContainer) 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,
};
}
4 changes: 2 additions & 2 deletions packages/plugins/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.