Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] add rule to enforce fragment syntax #1994

Merged
merged 1 commit into from Oct 30, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -158,6 +158,7 @@ Enable the rules that you would like to use.
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
* [react/jsx-one-expression-per-line](docs/rules/jsx-one-expression-per-line.md): Limit to one expression per line in JSX
* [react/jsx-curly-brace-presence](docs/rules/jsx-curly-brace-presence.md): Enforce curly braces or disallow unnecessary curly braces in JSX
* [react/jsx-fragments](docs/rules/jsx-fragments.md): Enforce shorthand or standard form for React fragments
* [react/jsx-pascal-case](docs/rules/jsx-pascal-case.md): Enforce PascalCase for user-defined JSX components
* [react/jsx-props-no-multi-spaces](docs/rules/jsx-props-no-multi-spaces.md): Disallow multiple spaces between inline JSX props (fixable)
* [react/jsx-sort-default-props](docs/rules/jsx-sort-default-props.md): Enforce default props alphabetical sorting
Expand Down
57 changes: 57 additions & 0 deletions docs/rules/jsx-fragments.md
@@ -0,0 +1,57 @@
# Enforce shorthand or standard form for React fragments (react/jsx-fragments)

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.

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].

## Rule Options

```js
...
"react/jsx-fragments": [<enabled>, <mode>]
...
```

### `syntax` mode

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.

The following pattern is considered a warning:

```jsx
<React.Fragment><Foo /></React.Fragment>
```

The following patterns are **not** considered warnings:

```jsx
<><Foo /></>
```

```jsx
<React.Fragment key="key"><Foo /></React.Fragment>
```

### `element` mode

This mode enforces the standard form for React fragments.

The following pattern is considered a warning:

```jsx
<><Foo /></>
```

The following patterns are **not** considered warnings:

```jsx
<React.Fragment><Foo /></React.Fragment>
```

```jsx
<React.Fragment key="key"><Foo /></React.Fragment>
```

[fragments]: https://reactjs.org/docs/fragments.html
[shared_settings]: /README.md#configuration
[short_syntax]: https://reactjs.org/docs/fragments.html#short-syntax
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -36,6 +36,7 @@ const allRules = {
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
'jsx-fragments': require('./lib/rules/jsx-fragments'),
'jsx-props-no-multi-spaces': require('./lib/rules/jsx-props-no-multi-spaces'),
'jsx-sort-default-props': require('./lib/rules/jsx-sort-default-props'),
'jsx-sort-props': require('./lib/rules/jsx-sort-props'),
Expand Down
179 changes: 179 additions & 0 deletions lib/rules/jsx-fragments.js
@@ -0,0 +1,179 @@
/**
* @fileoverview Enforce shorthand or standard form for React fragments.
* @author Alex Zherdev
*/
'use strict';

const elementType = require('jsx-ast-utils/elementType');
const pragmaUtil = require('../util/pragma');
const variableUtil = require('../util/variable');
const versionUtil = require('../util/version');
const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

function replaceNode(source, node, text) {
return `${source.slice(0, node.range[0])}${text}${source.slice(node.range[1])}`;
}

module.exports = {
meta: {
docs: {
description: 'Enforce shorthand or standard form for React fragments',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-fragments')
},
fixable: 'code',

schema: [{
enum: ['syntax', 'element']
}]
},

create: function(context) {
const configuration = context.options[0] || 'syntax';
const sourceCode = context.getSourceCode();
const reactPragma = pragmaUtil.getFromContext(context);
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
const openFragShort = '<>';
const closeFragShort = '</>';
const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;

function reportOnReactVersion(node) {
if (!versionUtil.testReactVersion(context, '16.2.0')) {
context.report({
node,
message: 'Fragments are only supported starting from React v16.2'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add something along the lines of:

Please disable the `jsx-fragments` rule or upgrade your version of React.

I think it's always a good idea to give users an action to do to help them resolve the error/warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a great improvement.

});
return true;
}

return false;
}

function getFixerToLong(jsxFragment) {
return function(fixer) {
let source = sourceCode.getText();
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
const lengthDiff = openFragLong.length - sourceCode.getText(jsxFragment.openingFragment).length
+ closeFragLong.length - sourceCode.getText(jsxFragment.closingFragment).length;
const range = jsxFragment.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
};
}

function getFixerToShort(jsxElement) {
return function(fixer) {
let source = sourceCode.getText();
source = replaceNode(source, jsxElement.closingElement, closeFragShort);
source = replaceNode(source, jsxElement.openingElement, openFragShort);
const lengthDiff = sourceCode.getText(jsxElement.openingElement).length - openFragShort.length
+ sourceCode.getText(jsxElement.closingElement).length - closeFragShort.length;
const range = jsxElement.range;
return fixer.replaceTextRange(range, source.slice(range[0], range[1] - lengthDiff));
};
}

function refersToReactFragment(name) {
const variableInit = variableUtil.findVariableByName(context, name);
if (!variableInit) {
return false;
}

// const { Fragment } = React;
if (variableInit.type === 'Identifier' && variableInit.name === reactPragma) {
return true;
}

// const Fragment = React.Fragment;
if (
variableInit.type === 'MemberExpression'
&& variableInit.object.type === 'Identifier'
&& variableInit.object.name === reactPragma
&& variableInit.property.type === 'Identifier'
&& variableInit.property.name === fragmentPragma
) {
return true;
}

// const { Fragment } = require('react');
if (
variableInit.callee
&& variableInit.callee.name === 'require'
&& variableInit.arguments
&& variableInit.arguments[0]
&& variableInit.arguments[0].value === 'react'
) {
return true;
}

return false;
}

const jsxElements = [];
const fragmentNames = new Set([`${reactPragma}.${fragmentPragma}`]);

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------

return {
JSXElement(node) {
jsxElements.push(node);
},

JSXFragment(node) {
if (reportOnReactVersion(node)) {
return;
}

if (configuration === 'element') {
context.report({
node,
message: `Prefer ${reactPragma}.${fragmentPragma} over fragment shorthand`,
fix: getFixerToLong(node)
});
}
},

ImportDeclaration(node) {
if (node.source && node.source.value === 'react') {
node.specifiers.forEach(spec => {
if (spec.imported && spec.imported.name === fragmentPragma) {
if (spec.local) {
fragmentNames.add(spec.local.name);
}
}
});
}
},

'Program:exit'() {
jsxElements.forEach(node => {
const openingEl = node.openingElement;
const elName = elementType(openingEl);

if (fragmentNames.has(elName) || refersToReactFragment(elName)) {
if (reportOnReactVersion(node)) {
return;
}

const attrs = openingEl.attributes;
if (configuration === 'syntax' && !(attrs && attrs.length > 0)) {
context.report({
node,
message: `Prefer fragment shorthand over ${reactPragma}.${fragmentPragma}`,
fix: getFixerToShort(node)
});
}
}
});
}
};
}
};
13 changes: 13 additions & 0 deletions lib/util/pragma.js
Expand Up @@ -21,6 +21,18 @@ function getCreateClassFromContext(context) {
return pragma;
}

function getFragmentFromContext(context) {
let pragma = 'Fragment';
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
if (context.settings.react && context.settings.react.fragment) {
pragma = context.settings.react.fragment;
}
if (!JS_IDENTIFIER_REGEX.test(pragma)) {
throw new Error(`Fragment pragma ${pragma} is not a valid identifier`);
}
return pragma;
}

function getFromContext(context) {
let pragma = 'React';

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

module.exports = {
getCreateClassFromContext: getCreateClassFromContext,
getFragmentFromContext: getFragmentFromContext,
getFromContext: getFromContext
};