Skip to content

Commit

Permalink
[New] add rule to enforce fragment syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzherdev authored and ljharb committed Sep 23, 2018
1 parent a92a0fb commit 2e60d0e
Show file tree
Hide file tree
Showing 6 changed files with 438 additions and 0 deletions.
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'
});
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
};

0 comments on commit 2e60d0e

Please sign in to comment.