diff --git a/README.md b/README.md index e77a809efe..56e7c3f670 100644 --- a/README.md +++ b/README.md @@ -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 | | | πŸ”§ | | | diff --git a/docs/rules/padding-lines-between-tags.md b/docs/rules/padding-lines-between-tags.md new file mode 100644 index 0000000000..f25fed53f1 --- /dev/null +++ b/docs/rules/padding-lines-between-tags.md @@ -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). + + + +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 + +
+
+
+
+
+
+ +``` + +### Require newlines after `
` + +`{ blankLine: 'always', prev: 'br', next: '*' }` + +```jsx + +
+
    +
  • +
  • +
    + +
  • +
  • +
+
+
+``` + +### Require newlines between `
` and `` + +`{ blankLine: 'always', prev: 'br', next: 'img' }` + +```jsx + +
+
    +
  • +
  • +
  • +
  • +
    + +
  • +
  • +
+
+
+``` + +```jsx [Fixed] + +
+
    +
  • +
  • + +
  • +
  • +
    + + +
  • +
  • +
+
+
+``` + +### Require consistent newlines + +`{ blankLine: 'consistent', prev: '*', next: '*' }` + +```jsx + +
+
    +
  • +
  • +
  • +
+ +
+ +
+
+ +``` + +## When Not To Use It + +If you are not using jsx. diff --git a/lib/rules/index.js b/lib/rules/index.js index 11a4475ba2..2b17db2401 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -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'), diff --git a/lib/rules/padding-lines-between-tags.js b/lib/rules/padding-lines-between-tags.js new file mode 100644 index 0000000000..a979d988bb --- /dev/null +++ b/lib/rules/padding-lines-between-tags.js @@ -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), + }; + }, +};