Skip to content

Commit 2e60d0e

Browse files
alexzherdevljharb
authored andcommitted
[New] add rule to enforce fragment syntax
1 parent a92a0fb commit 2e60d0e

File tree

6 files changed

+438
-0
lines changed

6 files changed

+438
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Enable the rules that you would like to use.
158158
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
159159
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
160160
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
161+
* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments
161162
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
162163
* [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable)
163164
* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting

docs/rules/jsx-fragments.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Enforce shorthand or standard form for React fragments (react/jsx-fragments)
2+
3+
In JSX, a React fragment is created either with `<React.Fragment>...</React.Fragment>`, or, using the shorthand syntax, `<>...</>`. This rule allows you to enforce one way or the other.
4+
5+
Support for fragments was added in React v16.2, so the rule will warn on either of these forms if an older React version is specified in [shared settings][shared_settings].
6+
7+
## Rule Options
8+
9+
```js
10+
...
11+
"react/jsx-fragments": [<enabled>, <mode>]
12+
...
13+
```
14+
15+
### `syntax` mode
16+
17+
This is the default mode. It will enforce the shorthand syntax for React fragments, with one exception. [Keys or attributes are not supported by the shorthand syntax][short_syntax], so the rule will not warn on standard-form fragments that use those.
18+
19+
The following pattern is considered a warning:
20+
21+
```jsx
22+
<React.Fragment><Foo /></React.Fragment>
23+
```
24+
25+
The following patterns are **not** considered warnings:
26+
27+
```jsx
28+
<><Foo /></>
29+
```
30+
31+
```jsx
32+
<React.Fragment key="key"><Foo /></React.Fragment>
33+
```
34+
35+
### `element` mode
36+
37+
This mode enforces the standard form for React fragments.
38+
39+
The following pattern is considered a warning:
40+
41+
```jsx
42+
<><Foo /></>
43+
```
44+
45+
The following patterns are **not** considered warnings:
46+
47+
```jsx
48+
<React.Fragment><Foo /></React.Fragment>
49+
```
50+
51+
```jsx
52+
<React.Fragment key="key"><Foo /></React.Fragment>
53+
```
54+
55+
[fragments]: https://reactjs.org/docs/fragments.html
56+
[shared_settings]: /README.md#configuration
57+
[short_syntax]: https://reactjs.org/docs/fragments.html#short-syntax

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const allRules = {
3636
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
3737
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
3838
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
39+
'jsx-fragments': require('./lib/rules/jsx-fragments'),
3940
'jsx-props-no-multi-spaces': require('./lib/rules/jsx-props-no-multi-spaces'),
4041
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
4142
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),

lib/rules/jsx-fragments.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* @fileoverview Enforce shorthand or standard form for React fragments.
3+
* @author Alex Zherdev
4+
*/
5+
'use strict';
6+
7+
const elementType = require('jsx-ast-utils/elementType');
8+
const pragmaUtil = require('../util/pragma');
9+
const variableUtil = require('../util/variable');
10+
const versionUtil = require('../util/version');
11+
const docsUrl = require('../util/docsUrl');
12+
13+
// ------------------------------------------------------------------------------
14+
// Rule Definition
15+
// ------------------------------------------------------------------------------
16+
17+
function replaceNode(source, node, text) {
18+
return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
19+
}
20+
21+
module.exports = {
22+
meta: {
23+
docs: {
24+
description: 'Enforce shorthand or standard form for React fragments',
25+
category: 'Stylistic Issues',
26+
recommended: false,
27+
url: docsUrl('jsx-fragments')
28+
},
29+
fixable: 'code',
30+
31+
schema: [{
32+
enum: ['syntax', 'element']
33+
}]
34+
},
35+
36+
create: function(context) {
37+
const configuration = context.options[0] || 'syntax';
38+
const sourceCode = context.getSourceCode();
39+
const reactPragma = pragmaUtil.getFromContext(context);
40+
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
41+
const openFragShort = '<>';
42+
const closeFragShort = '</>';
43+
const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
44+
const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;
45+
46+
function reportOnReactVersion(node) {
47+
if (!versionUtil.testReactVersion(context, '16.2.0')) {
48+
context.report({
49+
node,
50+
message: 'Fragments are only supported starting from React v16.2'
51+
});
52+
return true;
53+
}
54+
55+
return false;
56+
}
57+
58+
function getFixerToLong(jsxFragment) {
59+
return function(fixer) {
60+
let source = sourceCode.getText();
61+
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
62+
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
63+
const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length
64+
+ closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length;
65+
const range = jsxFragment.range;
66+
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
67+
};
68+
}
69+
70+
function getFixerToShort(jsxElement) {
71+
return function(fixer) {
72+
let source = sourceCode.getText();
73+
source = replaceNode(source, jsxElement.closingElement, closeFragShort);
74+
source = replaceNode(source, jsxElement.openingElement, openFragShort);
75+
const lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
76+
+ sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length;
77+
const range = jsxElement.range;
78+
return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
79+
};
80+
}
81+
82+
function refersToReactFragment(name) {
83+
const variableInit = variableUtil.findVariableByName(context, name);
84+
if (!variableInit) {
85+
return false;
86+
}
87+
88+
// const { Fragment } = React;
89+
if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
90+
return true;
91+
}
92+
93+
// const Fragment = React.Fragment;
94+
if (
95+
variableInit.type === 'MemberExpression'
96+
&& variableInit.object.type === 'Identifier'
97+
&& variableInit.object.name === reactPragma
98+
&& variableInit.property.type === 'Identifier'
99+
&& variableInit.property.name === fragmentPragma
100+
) {
101+
return true;
102+
}
103+
104+
// const { Fragment } = require('react');
105+
if (
106+
variableInit.callee
107+
&& variableInit.callee.name === 'require'
108+
&& variableInit.arguments
109+
&& variableInit.arguments[0]
110+
&& variableInit.arguments[0].value === 'react'
111+
) {
112+
return true;
113+
}
114+
115+
return false;
116+
}
117+
118+
const jsxElements = [];
119+
const fragmentNames = new Set([`${reactPragma}.${fragmentPragma}`]);
120+
121+
// --------------------------------------------------------------------------
122+
// Public
123+
// --------------------------------------------------------------------------
124+
125+
return {
126+
JSXElement(node) {
127+
jsxElements.push(node);
128+
},
129+
130+
JSXFragment(node) {
131+
if (reportOnReactVersion(node)) {
132+
return;
133+
}
134+
135+
if (configuration === 'element') {
136+
context.report({
137+
node,
138+
message: `Prefer ${reactPragma}.${fragmentPragma} over fragment shorthand`,
139+
fix: getFixerToLong(node)
140+
});
141+
}
142+
},
143+
144+
ImportDeclaration(node) {
145+
if (node.source && node.source.value === 'react') {
146+
node.specifiers.forEach(spec => {
147+
if (spec.imported && spec.imported.name === fragmentPragma) {
148+
if (spec.local) {
149+
fragmentNames.add(spec.local.name);
150+
}
151+
}
152+
});
153+
}
154+
},
155+
156+
'Program:exit'() {
157+
jsxElements.forEach(node => {
158+
const openingEl = node.openingElement;
159+
const elName = elementType(openingEl);
160+
161+
if (fragmentNames.has(elName) || refersToReactFragment(elName)) {
162+
if (reportOnReactVersion(node)) {
163+
return;
164+
}
165+
166+
const attrs = openingEl.attributes;
167+
if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
168+
context.report({
169+
node,
170+
message: `Prefer fragment shorthand over ${reactPragma}.${fragmentPragma}`,
171+
fix: getFixerToShort(node)
172+
});
173+
}
174+
}
175+
});
176+
}
177+
};
178+
}
179+
};

lib/util/pragma.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ function getCreateClassFromContext(context) {
2121
return pragma;
2222
}
2323

24+
function getFragmentFromContext(context) {
25+
let pragma = 'Fragment';
26+
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
27+
if (context.settings.react && context.settings.react.fragment) {
28+
pragma = context.settings.react.fragment;
29+
}
30+
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
31+
throw new Error(`Fragment pragma ${pragma} is not a valid identifier`);
32+
}
33+
return pragma;
34+
}
35+
2436
function getFromContext(context) {
2537
let pragma = 'React';
2638

@@ -43,5 +55,6 @@ function getFromContext(context) {
4355

4456
module.exports = {
4557
getCreateClassFromContext: getCreateClassFromContext,
58+
getFragmentFromContext: getFragmentFromContext,
4659
getFromContext: getFromContext
4760
};

0 commit comments

Comments
 (0)