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 rule: padding-lines-between-tags #3554 #3789

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ module.exports = [
| [no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Disallow definitions of unused propTypes | | | | | |
| [no-unused-state](docs/rules/no-unused-state.md) | Disallow definitions of unused state | | | | | |
| [no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Disallow usage of setState in componentWillUpdate | | | | | |
| [padding-lines-between-tags](docs/rules/padding-lines-between-tags.md) | Enforce no padding lines between tags for React Components | | | 🔧 | | |
| [prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components | | | | | |
| [prefer-exact-props](docs/rules/prefer-exact-props.md) | Prefer exact proptype definitions | | | | | |
| [prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Enforce that props are read-only | | | 🔧 | | |
Expand Down
127 changes: 127 additions & 0 deletions docs/rules/padding-lines-between-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Enforce no padding lines between tags for React Components (`react/padding-lines-between-tags`)

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Require or disallow newlines between sibling tags in React.

## Rule Details Options

```json
{
"padding-line-between-tags": [
"error",
[{ "blankLine": "always", "prev": "*", "next": "*" }]
]
}
```

This rule requires blank lines between each sibling HTML tag by default.

A configuration is an object which has 3 properties; `blankLine`, `prev`, and `next`. For example, `{ blankLine: "always", prev: "br", next: "div" }` means “one or more blank lines are required between a `br` tag and a `div` tag.” You can supply any number of configurations. If a tag pair matches multiple configurations, the last matched configuration will be used.

- `blankLine` is one of the following:
- `always` requires one or more blank lines.
- `never` disallows blank lines.
- `consistent` requires or disallows a blank line based on the first sibling element.
- `prev` any tag name without brackets.
- `next` any tag name without brackets.

### Disallow blank lines between all tags

`{ blankLine: 'never', prev: '*', next: '*' }`

```jsx
<App>
<div>
<div></div>
<div>
</div>
<div />
</div>
</App>
```

### Require newlines after `<br>`

`{ blankLine: 'always', prev: 'br', next: '*' }`

```jsx
<App>
<div>
<ul>
<li>
</li>
<br />

<li>
</li>
</ul>
</div>
</App>
```

### Require newlines between `<br>` and `<img>`

`{ blankLine: 'always', prev: 'br', next: 'img' }`

```jsx
<App>
<div>
<ul>
<li>
</li>
<li>
</li>
<br />
<img />
<li>
</li>
</ul>
</div>
</App>
```

```jsx [Fixed]
<App>
<div>
<ul>
<li>
</li>

<li>
</li>
<br />

<img />
<li>
</li>
</ul>
</div>
</App>
```

### Require consistent newlines

`{ blankLine: 'consistent', prev: '*', next: '*' }`

```jsx
<App>
<div>
<ul>
<li />
<li />
<li />
</ul>

<div />

<div />
</div>
</App>
```

## When Not To Use It

If you are not using jsx.
1 change: 1 addition & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ module.exports = {
'no-unused-state': require('./no-unused-state'),
'no-object-type-as-default-prop': require('./no-object-type-as-default-prop'),
'no-will-update-set-state': require('./no-will-update-set-state'),
'padding-lines-between-tags': require('./padding-lines-between-tags'),
'prefer-es6-class': require('./prefer-es6-class'),
'prefer-exact-props': require('./prefer-exact-props'),
'prefer-read-only-props': require('./prefer-read-only-props'),
Expand Down
187 changes: 187 additions & 0 deletions lib/rules/padding-lines-between-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* @fileoverview Enforce no padding lines between tags for React Components
* @author Alankar Anand
* Based on https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-tags.js
* https://github.com/jsx-eslint/eslint-plugin-react/issues/3554
*/

'use strict';

const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
const eslintUtil = require('../util/eslint');

/**
* Split the source code into multiple lines based on the line delimiters.
* Copied from padding-line-between-blocks
* @param {string} text Source code as a string.
* @returns {string[]} Array of source code lines.
*/

function splitLines(text) {
return text.split(/\r\n|[\r\n\u2028\u2029]/gu);
}

function insertNewLine(tag, sibling, lineDifference) {
const endTag = tag.closingElement || tag.openingElement;

if (lineDifference === 1) {
report({
message: 'Unexpected blank line before {{endTag}}.',
loc: sibling && sibling.loc,
// @ts-ignore
fix(fixer) {
return fixer.insertTextAfter(tag, '\n');
},
});
} else if (lineDifference === 0) {
report({
message: 'Expected blank line before {{endTag}}.',
loc: sibling && sibling.loc,
// @ts-ignore
fix(fixer) {
const lastSpaces = /** @type {RegExpExecArray} */ (
/^\s*/.exec(eslintUtil.getSourceCode().lines[endTag.loc.start.line - 1])
)[0];

return fixer.insertTextAfter(endTag, `\n\n${lastSpaces}`);
},
});
}
}

function removeExcessLines(endTag, sibling, lineDifference) {
if (lineDifference > 1) {
let hasOnlyTextBetween = true;
for (
let i = endTag.loc && endTag.loc.start.line;
i < sibling.loc.start.line - 1 && hasOnlyTextBetween;
i++
) {
hasOnlyTextBetween = !/^\s*$/.test(eslintUtil.getSourceCode().lines[i]);
}
if (!hasOnlyTextBetween) {
report({
messageId: 'never',
loc: sibling && sibling.loc,
// @ts-ignore
fix(fixer) {
const start = endTag.range[1];
const end = sibling.range[0];
const paddingText = eslintUtil.getSourceCode().text.slice(start, end);
const textBetween = splitLines(paddingText);
let newTextBetween = `\n${textBetween.pop()}`;
for (let i = textBetween.length - 1; i >= 0; i--) {
if (!/^\s*$/.test(textBetween[i])) {
newTextBetween = `${i === 0 ? '' : '\n'}${
textBetween[i]
}${newTextBetween}`;
}
}
return fixer.replaceTextRange([start, end], `${newTextBetween}`);
},
});
}
}
}

function checkNewLine(configureList) {
const firstConsistentBlankLines = new Map();

const reverseConfigureList = [].concat(configureList).reverse();

return (node) => {
if (!node.parent.parent) {
return;
}

const endTag = node.closingElement || node.openingElement;

if (!node.parent.children) {
return;
}
const lowerSiblings = node.parent.children
.filter(
(element) => element.type === 'JSXElement' && element.range !== node.range
)
.filter((sibling) => sibling.range[0] - endTag.range[1] >= 0);

if (lowerSiblings.length === 0) {
return;
}
const closestSibling = lowerSiblings[0];

const lineDifference = closestSibling.loc.start.line - endTag.loc.end.line;

const configure = reverseConfigureList.find(
(config) => (config.prev === '*'
|| node.openingElement.name.name === config.prev)
&& (config.next === '*'
|| closestSibling.openingElement.name.name === config.next)
);

if (!configure) {
return;
}

let blankLine = configure.blankLine;

if (blankLine === 'consistent') {
const firstConsistentBlankLine = firstConsistentBlankLines.get(
node.parent
);
if (firstConsistentBlankLine == null) {
firstConsistentBlankLines.set(
node.parent,
lineDifference > 1 ? 'always' : 'never'
);
return;
}
blankLine = firstConsistentBlankLine;
}

if (blankLine === 'always') {
insertNewLine(node, closestSibling, lineDifference);
} else {
removeExcessLines(endTag, closestSibling, lineDifference);
}
};
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
docs: {
description: 'Enforce no padding lines between tags for React Components',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('padding-lines-between-tags'),
},
fixable: 'code',
schema: [
{
type: 'array',
items: {
type: 'object',
properties: {
blankLine: { enum: ['always', 'never', 'consistent'] },
prev: { type: 'string' },
next: { type: 'string' },
},
additionalProperties: false,
required: ['blankLine', 'prev', 'next'],
},
},
],
},

create(context) {
const configureList = context.options[0] || [
{ blankLine: 'always', prev: '*', next: '*' },
];
return {
JSXElement: checkNewLine(configureList),
};
},
};