/** * @fileoverview Disallow multiple spaces between inline JSX props * @author Adrian Moennich */ 'use strict'; const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ module.exports = { meta: { docs: { description: 'Disallow multiple spaces between inline JSX props', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-props-no-multi-spaces') }, fixable: 'code', messages: { noLineGap: 'Expected no line gap between “{{prop1}}” and “{{prop2}}”', onlyOneSpace: 'Expected only one space between “{{prop1}}” and “{{prop2}}”' }, schema: [] }, create(context) { const sourceCode = context.getSourceCode(); function getPropName(propNode) { switch (propNode.type) { case 'JSXSpreadAttribute': return context.getSourceCode().getText(propNode.argument); case 'JSXIdentifier': return propNode.name; case 'JSXMemberExpression': return `${getPropName(propNode.object)}.${propNode.property.name}`; default: return propNode.name.name; } } // First and second must be adjacent nodes function hasEmptyLines(first, second) { const comments = sourceCode.getCommentsBefore(second); const nodes = [].concat(first, comments, second); for (let i = 1; i < nodes.length; i += 1) { const prev = nodes[i - 1]; const curr = nodes[i]; if (curr.loc.start.line - prev.loc.end.line >= 2) { return true; } } return false; } function checkSpacing(prev, node) { if (hasEmptyLines(prev, node)) { context.report({ node, messageId: 'noLineGap', data: { prop1: getPropName(prev), prop2: getPropName(node) } }); } if (prev.loc.end.line !== node.loc.end.line) { return; } const between = context.getSourceCode().text.slice(prev.range[1], node.range[0]); if (between !== ' ') { context.report({ node, messageId: 'onlyOneSpace', data: { prop1: getPropName(prev), prop2: getPropName(node) }, fix(fixer) { return fixer.replaceTextRange([prev.range[1], node.range[0]], ' '); } }); } } function containsGenericType(node) { const containsTypeParams = typeof node.typeParameters !== 'undefined'; return containsTypeParams && node.typeParameters.type === 'TSTypeParameterInstantiation'; } function getGenericNode(node) { const name = node.name; if (containsGenericType(node)) { const type = node.typeParameters; return Object.assign( {}, node, { range: [ name.range[0], type.range[1] ] } ); } return name; } return { JSXOpeningElement(node) { node.attributes.reduce((prev, prop) => { checkSpacing(prev, prop); return prop; }, getGenericNode(node)); } }; } };