Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
101 changes: 101 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,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 <div>Hello ${user.name}</div>;
// ^^^^^^^
// - Possible unnecessary '$' character before expression.
}
```

```tsx
import React from "react";

function MyComponent({ user }) {
return <div>${user.name} is your 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 }) {
// 🟢 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 <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)
129 changes: 129 additions & 0 deletions 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,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 <div>Hello \${props.name}</div>;
};
`,
errors: [
{
messageId: "jsxDollar",
suggestions: [
{
messageId: "removeDollarSign",
output: tsx`
const App = (props) => {
return <div>Hello {props.name}</div>;
};
`,
},
],
},
],
},
{
code: tsx`
const App = (props) => {
return <div>\${props.name} is your name</div>;
};
`,
errors: [
{
messageId: "jsxDollar",
suggestions: [
{
messageId: "removeDollarSign",
output: tsx`
const App = (props) => {
return <div>{props.name} is your name</div>;
};
`,
},
],
},
],
},
{
code: tsx`
const App = (props) => {
return <div>Hello \${props.name} is your name</div>;
};
`,
errors: [
{
messageId: "jsxDollar",
suggestions: [
{
messageId: "removeDollarSign",
output: tsx`
const App = (props) => {
return <div>Hello {props.name} is your 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>;
}
`,
],
});
67 changes: 67 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,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<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.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,
};
}
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.