Skip to content

Commit c94b1fb

Browse files
committed
feat: add 'jsx-shorthand-*' rules
1 parent 8fdbadd commit c94b1fb

File tree

7 files changed

+375
-0
lines changed

7 files changed

+375
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: jsx-shorthand-boolean
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```sh copy
8+
react-x/jsx-shorthand-boolean
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```sh copy
14+
@eslint-react/jsx-shorthand-boolean
15+
```
16+
17+
**Features**
18+
19+
`🔧`
20+
21+
## Description
22+
23+
Enforces shorthand syntax for boolean attributes.
24+
25+
## Examples
26+
27+
### Failing
28+
29+
```tsx
30+
import React from "react";
31+
32+
function MyComponent() {
33+
return <button disabled={true} />;
34+
// ^^^^^^^^^^^^^^^
35+
// - Use shorthand boolean attribute 'disabled'.
36+
}
37+
```
38+
39+
### Passing
40+
41+
```tsx
42+
import React from "react";
43+
44+
function MyComponent() {
45+
return <button disabled />;
46+
}
47+
```
48+
49+
## Implementation
50+
51+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.ts)
52+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.spec.ts)
53+
54+
---
55+
56+
## See Also
57+
58+
- [`jsx-shorthand-fragment`](./jsx-shorthand-fragment)\
59+
Enforces the use of shorthand syntax for fragments.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import tsx from "dedent";
2+
3+
import { allValid, ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./jsx-shorthand-boolean";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
{
9+
code: tsx`<input disabled={true} />`,
10+
errors: [{
11+
messageId: "omitAttributeValue",
12+
data: { propName: "disabled" },
13+
}],
14+
output: tsx`<input disabled />`,
15+
},
16+
{
17+
code: tsx`<App foo={true} />`,
18+
errors: [{
19+
messageId: "omitAttributeValue",
20+
data: { propName: "foo" },
21+
}],
22+
output: tsx`<App foo />`,
23+
},
24+
{
25+
code: tsx`<App foo={true} bar />`,
26+
errors: [{
27+
messageId: "omitAttributeValue",
28+
data: { propName: "foo" },
29+
}],
30+
output: tsx`<App foo bar />`,
31+
},
32+
],
33+
valid: [
34+
...allValid,
35+
tsx`<input disabled />`,
36+
"<App foo />",
37+
"<App foo bar />",
38+
"<App foo bar={false} />",
39+
"<App foo bar={false} baz />",
40+
],
41+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { _ } from "@eslint-react/eff";
2+
import type { RuleContext, RuleFeature, RulePolicy } from "@eslint-react/kit";
3+
import type { TSESTree } from "@typescript-eslint/types";
4+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
5+
6+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
7+
8+
import * as ER from "@eslint-react/core";
9+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
10+
import { createRule } from "../utils";
11+
12+
export const RULE_NAME = "jsx-shorthand-boolean";
13+
14+
export const RULE_FEATURES = [
15+
"CFG",
16+
"FIX",
17+
] as const satisfies RuleFeature[];
18+
19+
export type MessageID =
20+
| "omitAttributeValue"
21+
| "setAttributeValue";
22+
23+
type Options = readonly [
24+
| _
25+
| RulePolicy,
26+
];
27+
28+
const defaultOptions = ["prefer"] as const satisfies Options;
29+
30+
const schema = [
31+
{
32+
type: "string",
33+
enum: ["prefer", "avoid"],
34+
},
35+
] as const satisfies [JSONSchema4];
36+
37+
export default createRule<Options, MessageID>({
38+
meta: {
39+
type: "problem",
40+
docs: {
41+
description: "Enforces shorthand syntax for boolean attributes.",
42+
[Symbol.for("rule_features")]: RULE_FEATURES,
43+
},
44+
fixable: "code",
45+
messages: {
46+
omitAttributeValue: "Value must be omitted for boolean attribute `{{propName}}`",
47+
setAttributeValue: "Value must be set for boolean attribute `{{propName}}`",
48+
},
49+
schema,
50+
},
51+
name: RULE_NAME,
52+
create,
53+
defaultOptions,
54+
});
55+
56+
export function create(context: RuleContext<MessageID, Options>): RuleListener {
57+
const policy = context.options[0] ?? defaultOptions[0];
58+
return {
59+
JSXAttribute(node: TSESTree.JSXAttribute) {
60+
const { value } = node;
61+
if (policy === "avoid" && value == null) {
62+
context.report({
63+
messageId: "setAttributeValue",
64+
node,
65+
data: {
66+
propName: ER.getAttributeName(context, node),
67+
},
68+
fix: (fixer) => fixer.insertTextAfter(node.name, `={true}`),
69+
});
70+
return;
71+
}
72+
73+
const hasValueTrue = value?.type === T.JSXExpressionContainer
74+
&& value.expression.type === T.Literal
75+
&& value.expression.value === true;
76+
const propName = ER.getAttributeName(context, node);
77+
if (policy === "prefer" && hasValueTrue) {
78+
context.report({
79+
messageId: "omitAttributeValue",
80+
node: node.value ?? node,
81+
data: {
82+
propName,
83+
},
84+
fix: (fixer) => fixer.removeRange([node.name.range[1], value.range[1]]),
85+
});
86+
}
87+
},
88+
};
89+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
title: jsx-shorthand-fragment
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```sh copy
8+
react-x/jsx-shorthand-fragment
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```sh copy
14+
@eslint-react/jsx-shorthand-fragment
15+
```
16+
17+
**Features**
18+
19+
`🔧`
20+
21+
## Description
22+
23+
Enforces shorthand syntax for fragments.
24+
25+
## Examples
26+
27+
### Failing
28+
29+
```tsx
30+
import React, { Fragment } from "react";
31+
32+
function MyComponent() {
33+
return (
34+
<Fragment>
35+
<button />
36+
<button />
37+
</Fragment>
38+
);
39+
}
40+
```
41+
42+
### Passing
43+
44+
```tsx
45+
import React from "react";
46+
47+
function MyComponent() {
48+
return (
49+
<>
50+
<button />
51+
<button />
52+
</>
53+
);
54+
}
55+
```
56+
57+
## Implementation
58+
59+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts)
60+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.spec.ts)
61+
62+
---
63+
64+
## See Also
65+
66+
- [`jsx-shorthand-boolean`](./jsx-shorthand-boolean)\
67+
Enforces the use of shorthand syntax for boolean attributes.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import tsx from "dedent";
2+
3+
import { allValid, ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./jsx-shorthand-fragment";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
{
9+
code: tsx`<React.Fragment><div /></React.Fragment>`,
10+
errors: [
11+
{
12+
messageId: "jsxShorthandFragment",
13+
},
14+
],
15+
output: tsx`<><div /></>`,
16+
},
17+
{
18+
code: tsx`<Fragment><div /></Fragment>`,
19+
errors: [
20+
{
21+
messageId: "jsxShorthandFragment",
22+
},
23+
],
24+
output: tsx`<><div /></>`,
25+
},
26+
{
27+
code: tsx`
28+
<React.Fragment>
29+
<div />
30+
</React.Fragment>
31+
`,
32+
errors: [
33+
{
34+
messageId: "jsxShorthandFragment",
35+
},
36+
],
37+
output: tsx`
38+
<>
39+
<div />
40+
</>
41+
`,
42+
},
43+
],
44+
valid: [
45+
...allValid,
46+
tsx`<><Foo /><Bar /></>`,
47+
tsx`<>foo<div /></>`,
48+
tsx`<> <div /></>`,
49+
tsx`<>{"moo"} </>`,
50+
tsx`<NotFragment />`,
51+
tsx`<React.NotFragment />`,
52+
tsx`<Foo><><div /><div /></></Foo>`,
53+
tsx`<div p={<>{"a"}{"b"}</>} />`,
54+
tsx`<Fragment key={item.id}>{item.value}</Fragment>`,
55+
tsx`<Fooo content={<>eeee ee eeeeeee eeeeeeee</>} />`,
56+
tsx`<>{foos.map(foo => foo)}</>`,
57+
],
58+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
2+
import type { TSESTree } from "@typescript-eslint/types";
3+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
4+
import type { CamelCase } from "string-ts";
5+
import * as ER from "@eslint-react/core";
6+
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "jsx-shorthand-fragment";
10+
11+
export const RULE_FEATURES = [
12+
"FIX",
13+
] as const satisfies RuleFeature[];
14+
15+
export type MessageID = CamelCase<typeof RULE_NAME>;
16+
17+
export default createRule<[], MessageID>({
18+
meta: {
19+
type: "problem",
20+
docs: {
21+
description: "Enforces shorthand syntax for fragments.",
22+
[Symbol.for("rule_features")]: RULE_FEATURES,
23+
},
24+
fixable: "code",
25+
messages: {
26+
jsxShorthandFragment: "Use fragment shorthand syntax instead of 'Fragment' component.",
27+
},
28+
schema: [],
29+
},
30+
name: RULE_NAME,
31+
create,
32+
defaultOptions: [],
33+
});
34+
35+
export function create(context: RuleContext<MessageID, []>): RuleListener {
36+
return {
37+
JSXElement(node: TSESTree.JSXElement) {
38+
if (!ER.isFragmentElement(context, node)) return;
39+
const hasAttributes = node.openingElement.attributes.length > 0;
40+
if (hasAttributes) {
41+
return;
42+
}
43+
context.report({
44+
messageId: "jsxShorthandFragment",
45+
node,
46+
fix: (fixer) => {
47+
const { closingElement, openingElement } = node;
48+
if (closingElement == null) {
49+
return [];
50+
}
51+
return [
52+
fixer.replaceTextRange([openingElement.range[0], openingElement.range[1]], "<>"),
53+
fixer.replaceTextRange([closingElement.range[0], closingElement.range[1]], "</>"),
54+
];
55+
},
56+
});
57+
},
58+
};
59+
}

packages/utilities/kit/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ export type RuleFeature =
4242
| "MOD" // Codemod
4343
| "TSC" // TypeScript Type Checking
4444
| "EXP"; // Experimental
45+
46+
export type RulePolicy = "prefer" | "avoid";

0 commit comments

Comments
 (0)