diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a23280e5..b67ee92808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2808,3 +2808,4 @@ If you're still not using React 15 you can keep the old behavior by setting the [`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md [`jsx-no-useless-fragment`]: docs/rules/jsx-no-useless-fragment.md [`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md +[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md diff --git a/docs/rules/no-adjacent-inline-elements.md b/docs/rules/no-adjacent-inline-elements.md new file mode 100644 index 0000000000..db7d9b41ec --- /dev/null +++ b/docs/rules/no-adjacent-inline-elements.md @@ -0,0 +1,24 @@ +# Prevent adjacent inline elements not separated by whitespace. (no-adjacent-inline-elements) + +Adjacent inline elements not separated by whitespace will bump up against each +other when viewed in an unstyled manner, which usually isn't desirable. + +## Rule Details + +The following patterns are considered warnings: + +```jsx +
+
+ +React.createElement("div", undefined, [React.createElement("a"), React.createElement("span")]); +``` + +The following patterns are not considered warnings: + +```jsx +
+
+ +React.createElement("div", undefined, [React.createElement("a"), " ", React.createElement("a")]); +``` diff --git a/index.js b/index.js index 6db76d0aee..2396353682 100644 --- a/index.js +++ b/index.js @@ -52,6 +52,7 @@ const allRules = { 'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'), 'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'), 'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'), + 'no-adjacent-inline-elements': require('./lib/rules/no-adjacent-inline-elements'), 'no-array-index-key': require('./lib/rules/no-array-index-key'), 'no-children-prop': require('./lib/rules/no-children-prop'), 'no-danger': require('./lib/rules/no-danger'), diff --git a/lib/rules/no-adjacent-inline-elements.js b/lib/rules/no-adjacent-inline-elements.js new file mode 100644 index 0000000000..0df74a75b6 --- /dev/null +++ b/lib/rules/no-adjacent-inline-elements.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Prevent adjacent inline elements not separated by whitespace. + * @author Sean Hayes + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +// https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements +const inlineNames = [ + 'a', + 'b', + 'big', + 'i', + 'small', + 'tt', + 'abbr', + 'acronym', + 'cite', + 'code', + 'dfn', + 'em', + 'kbd', + 'strong', + 'samp', + 'time', + 'var', + 'bdo', + 'br', + 'img', + 'map', + 'object', + 'q', + 'script', + 'span', + 'sub', + 'sup', + 'button', + 'input', + 'label', + 'select', + 'textarea' +]; +// Note: raw   will be transformed into \u00a0. +const whitespaceStart = /^\s/; +const whitespaceEnd = /\s$/; + +function isInline(node) { + if (node.type === 'Literal') { + // Regular whitespace will be removed. + const value = node.value; + // To properly separate inline elements, each end of the literal will need + // whitespace. + return !whitespaceStart.test(value) || !whitespaceEnd.test(value); + } + if (node.type === 'JSXElement') { + if (inlineNames.indexOf(node.openingElement.name.name) > -1) { + return true; + } + } + if (node.type === 'CallExpression') { + if (inlineNames.indexOf(node.arguments[0].value) > -1) { + return true; + } + } + return false; +} + +const ERROR = 'Child elements which render as inline HTML elements should be separated by a space or wrapped in block level elements.'; + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + ERROR, + meta: { + docs: { + description: 'Prevent adjacent inline elements not separated by whitespace.', + category: 'Best Practices', + recommended: false + }, + schema: [] + }, + create(context) { + function validate(node, children) { + let currentIsInline = false; + let previousIsInline = false; + for (let i = 0; i < children.length; i++) { + currentIsInline = isInline(children[i]); + if (previousIsInline && currentIsInline) { + context.report({ + node, + message: ERROR + }); + return; + } + previousIsInline = currentIsInline; + } + } + return { + JSXElement(node) { + validate(node, node.children); + }, + CallExpression(node) { + if (!node.callee || node.callee.type !== 'MemberExpression' || node.callee.property.name !== 'createElement') { + return; + } + if (node.arguments.length < 2) { + return; + } + const children = node.arguments[2].elements; + validate(node, children); + } + }; + } +}; diff --git a/tests/lib/rules/no-adjacent-inline-elements.js b/tests/lib/rules/no-adjacent-inline-elements.js new file mode 100644 index 0000000000..c196a0ee20 --- /dev/null +++ b/tests/lib/rules/no-adjacent-inline-elements.js @@ -0,0 +1,102 @@ +/** + * @fileoverview Tests for no-adjacent-inline-elements + * @author Sean Hayes + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-adjacent-inline-elements'); + +const ERROR = rule.ERROR; + +const parserOptions = { + ecmaVersion: 6, + ecmaFeatures: { + experimentalObjectRestSpread: true, + jsx: true + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester(); +ruleTester.run('no-adjacent-inline-elements', rule, { + valid: [ + { + code: '
;', + parserOptions + }, + { + code: '
;', + parserOptions + }, + { + code: '

;', + parserOptions + }, + { + code: '

;', + parserOptions + }, + { + code: '
 
;', + parserOptions + }, + { + code: '
 some text  
;', + parserOptions + }, + { + code: '
 some text
;', + parserOptions + }, + { + code: '
;', + parserOptions + }, + { + code: '
;', + parserOptions + }, + { + code: '
some text
;', + errors: [{message: ERROR}], + parserOptions + }, + { + code: ('React.createElement("div", undefined, [React.createElement("a"), ' + + '" some text ", React.createElement("a")]);'), + errors: [{message: ERROR}], + parserOptions + }, + { + code: 'React.createElement("div", undefined, [React.createElement("a"), " ", React.createElement("a")]);', + errors: [{message: ERROR}], + parserOptions + } + ], + invalid: [ + { + code: '
;', + errors: [{message: ERROR}], + parserOptions + }, + { + code: '
;', + errors: [{message: ERROR}], + parserOptions + }, + { + code: 'React.createElement("div", undefined, [React.createElement("a"), React.createElement("span")]);', + errors: [{message: ERROR}], + parserOptions + } + ] +});