Skip to content

Commit 37773e8

Browse files
committed
feat: update jsx-shorthand rules to use new message format and policy options
1 parent 77c45a6 commit 37773e8

File tree

5 files changed

+175
-68
lines changed

5 files changed

+175
-68
lines changed

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,52 @@ ruleTester.run(RULE_NAME, rule, {
88
{
99
code: tsx`<input disabled={true} />`,
1010
errors: [{
11-
messageId: "omitAttributeValue",
12-
data: { propName: "disabled" },
11+
messageId: "jsxShorthandBoolean",
12+
data: { message: "Omit attribute value for 'disabled'." },
1313
}],
1414
output: tsx`<input disabled />`,
1515
},
1616
{
1717
code: tsx`<App foo={true} />`,
1818
errors: [{
19-
messageId: "omitAttributeValue",
20-
data: { propName: "foo" },
19+
messageId: "jsxShorthandBoolean",
20+
data: { message: "Omit attribute value for 'foo'." },
2121
}],
2222
output: tsx`<App foo />`,
2323
},
2424
{
2525
code: tsx`<App foo={true} bar />`,
2626
errors: [{
27-
messageId: "omitAttributeValue",
28-
data: { propName: "foo" },
27+
messageId: "jsxShorthandBoolean",
28+
data: { message: "Omit attribute value for 'foo'." },
2929
}],
3030
output: tsx`<App foo bar />`,
3131
},
32+
{
33+
code: tsx`<App foo={true} bar={false} />`,
34+
errors: [{
35+
messageId: "jsxShorthandBoolean",
36+
data: { message: "Omit attribute value for 'foo'." },
37+
}],
38+
output: tsx`<App foo bar={false} />`,
39+
},
40+
{
41+
code: tsx`<App foo={true} bar={false} baz />`,
42+
errors: [{
43+
messageId: "jsxShorthandBoolean",
44+
data: { message: "Omit attribute value for 'foo'." },
45+
}],
46+
output: tsx`<App foo bar={false} baz />`,
47+
},
48+
{
49+
code: tsx`<input disabled />`,
50+
errors: [{
51+
messageId: "jsxShorthandBoolean",
52+
data: { message: "Set attribute value for 'disabled'." },
53+
}],
54+
options: [-1],
55+
output: tsx`<input disabled={true} />`,
56+
},
3257
],
3358
valid: [
3459
...allValid,
@@ -37,5 +62,9 @@ ruleTester.run(RULE_NAME, rule, {
3762
"<App foo bar />",
3863
"<App foo bar={false} />",
3964
"<App foo bar={false} baz />",
65+
{
66+
code: tsx`<App foo={true} />`,
67+
options: [-1],
68+
},
4069
],
4170
});

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

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
55

66
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
77

8+
import type { CamelCase } from "string-ts";
89
import * as ER from "@eslint-react/core";
910
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
1011
import { createRule } from "../utils";
@@ -16,21 +17,19 @@ export const RULE_FEATURES = [
1617
"FIX",
1718
] as const satisfies RuleFeature[];
1819

19-
export type MessageID =
20-
| "omitAttributeValue"
21-
| "setAttributeValue";
20+
export type MessageID = CamelCase<typeof RULE_NAME>;
2221

2322
type Options = readonly [
2423
| _
2524
| RulePolicy,
2625
];
2726

28-
const defaultOptions = ["prefer"] as const satisfies Options;
27+
const defaultOptions = [1] as const satisfies Options;
2928

3029
const schema = [
3130
{
32-
type: "string",
33-
enum: ["prefer", "avoid"],
31+
type: "integer",
32+
enum: [-1, 1],
3433
},
3534
] as const satisfies [JSONSchema4];
3635

@@ -43,8 +42,7 @@ export default createRule<Options, MessageID>({
4342
},
4443
fixable: "code",
4544
messages: {
46-
omitAttributeValue: "Value must be omitted for boolean attribute `{{propName}}`",
47-
setAttributeValue: "Value must be set for boolean attribute `{{propName}}`",
45+
jsxShorthandBoolean: "{{message}}",
4846
},
4947
schema,
5048
},
@@ -58,31 +56,35 @@ export function create(context: RuleContext<MessageID, Options>): RuleListener {
5856
return {
5957
JSXAttribute(node: TSESTree.JSXAttribute) {
6058
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;
7659
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-
});
60+
61+
switch (true) {
62+
case policy === 1
63+
&& value?.type === T.JSXExpressionContainer
64+
&& value.expression.type === T.Literal
65+
&& value.expression.value === true: {
66+
context.report({
67+
messageId: "jsxShorthandBoolean",
68+
node,
69+
data: {
70+
message: `Omit attribute value for '${propName}'.`,
71+
},
72+
fix: (fixer) => fixer.removeRange([node.name.range[1], value.range[1]]),
73+
});
74+
break;
75+
}
76+
case policy === -1
77+
&& value === null: { // eslint-disable-line local/prefer-eqeq-nullish-comparison
78+
context.report({
79+
messageId: "jsxShorthandBoolean",
80+
node: node.value ?? node,
81+
data: {
82+
message: `Set attribute value for '${propName}'.`,
83+
},
84+
fix: (fixer) => fixer.insertTextAfter(node.name, `={true}`),
85+
});
86+
break;
87+
}
8688
}
8789
},
8890
};

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ ruleTester.run(RULE_NAME, rule, {
4040
</>
4141
`,
4242
},
43+
{
44+
code: tsx`
45+
<>
46+
<div />
47+
</>
48+
`,
49+
errors: [
50+
{
51+
messageId: "jsxShorthandFragment",
52+
data: { message: "Use 'Fragment' component instead of fragment shorthand syntax." },
53+
},
54+
],
55+
options: [-1],
56+
output: tsx`
57+
<React.Fragment>
58+
<div />
59+
</React.Fragment>
60+
`,
61+
},
4362
],
4463
valid: [
4564
...allValid,
@@ -54,5 +73,13 @@ ruleTester.run(RULE_NAME, rule, {
5473
tsx`<Fragment key={item.id}>{item.value}</Fragment>`,
5574
tsx`<Fooo content={<>eeee ee eeeeeee eeeeeeee</>} />`,
5675
tsx`<>{foos.map(foo => foo)}</>`,
76+
{
77+
code: tsx`
78+
<React.Fragment>
79+
<div />
80+
</React.Fragment>
81+
`,
82+
options: [-1],
83+
},
5784
],
5885
});
Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
1-
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
1+
import type { _ } from "@eslint-react/eff";
22
import type { TSESTree } from "@typescript-eslint/types";
3+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
34
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
45
import type { CamelCase } from "string-ts";
5-
import * as ER from "@eslint-react/core";
66

7+
import * as ER from "@eslint-react/core";
8+
import { JsxConfig, type RuleContext, type RuleFeature, type RulePolicy } from "@eslint-react/kit";
9+
import { match } from "ts-pattern";
710
import { createRule } from "../utils";
811

912
export const RULE_NAME = "jsx-shorthand-fragment";
1013

1114
export const RULE_FEATURES = [
15+
"CFG",
1216
"FIX",
1317
] as const satisfies RuleFeature[];
1418

1519
export type MessageID = CamelCase<typeof RULE_NAME>;
1620

17-
export default createRule<[], MessageID>({
21+
type Options = readonly [
22+
| _
23+
| RulePolicy,
24+
];
25+
26+
const defaultOptions = [1] as const satisfies Options;
27+
28+
const schema = [
29+
{
30+
type: "integer",
31+
enum: [-1, 1],
32+
},
33+
] as const satisfies [JSONSchema4];
34+
35+
export default createRule<Options, MessageID>({
1836
meta: {
1937
type: "problem",
2038
docs: {
@@ -23,37 +41,68 @@ export default createRule<[], MessageID>({
2341
},
2442
fixable: "code",
2543
messages: {
26-
jsxShorthandFragment: "Use fragment shorthand syntax instead of 'Fragment' component.",
44+
jsxShorthandFragment: "{{message}}",
2745
},
28-
schema: [],
46+
schema,
2947
},
3048
name: RULE_NAME,
3149
create,
32-
defaultOptions: [],
50+
defaultOptions,
3351
});
3452

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-
},
53+
export function create(context: RuleContext<MessageID, Options>): RuleListener {
54+
const policy = context.options[0] ?? defaultOptions[0];
55+
const jsxConfig = {
56+
...JsxConfig.getFromContext(context),
57+
...JsxConfig.getFromAnnotation(context),
5858
};
59+
60+
const { jsxFragmentFactory } = jsxConfig;
61+
62+
return match<number, RuleListener>(policy)
63+
.with(1, () => ({
64+
JSXElement(node: TSESTree.JSXElement) {
65+
if (!ER.isFragmentElement(context, node)) return;
66+
const hasAttributes = node.openingElement.attributes.length > 0;
67+
if (hasAttributes) return;
68+
context.report({
69+
messageId: "jsxShorthandFragment",
70+
node,
71+
data: { message: "Use fragment shorthand syntax instead of 'Fragment' component." },
72+
fix: (fixer) => {
73+
const { closingElement, openingElement } = node;
74+
if (closingElement == null) {
75+
return [];
76+
}
77+
return [
78+
fixer.replaceTextRange([openingElement.range[0], openingElement.range[1]], "<>"),
79+
fixer.replaceTextRange([closingElement.range[0], closingElement.range[1]], "</>"),
80+
];
81+
},
82+
});
83+
},
84+
}))
85+
.with(-1, () => ({
86+
JSXFragment(node: TSESTree.JSXFragment) {
87+
context.report({
88+
messageId: "jsxShorthandFragment",
89+
node,
90+
data: { message: "Use 'Fragment' component instead of fragment shorthand syntax." },
91+
fix: (fixer) => {
92+
const { closingFragment, openingFragment } = node;
93+
return [
94+
fixer.replaceTextRange(
95+
[openingFragment.range[0], openingFragment.range[1]],
96+
"<" + jsxFragmentFactory + ">",
97+
),
98+
fixer.replaceTextRange(
99+
[closingFragment.range[0], closingFragment.range[1]],
100+
"</" + jsxFragmentFactory + ">",
101+
),
102+
];
103+
},
104+
});
105+
},
106+
}))
107+
.otherwise(() => ({}));
59108
}

packages/utilities/kit/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ export type RuleFeature =
4343
| "TSC" // TypeScript Type Checking
4444
| "EXP"; // Experimental
4545

46-
export type RulePolicy = "prefer" | "avoid";
46+
export type RulePolicy = number;

0 commit comments

Comments
 (0)