UNPKG

eslint-plugin-react

Version:
292 lines (259 loc) 8.63 kB
/** * @fileoverview Validates whitespace in and around the JSX opening and closing brackets * @author Diogo Franco (Kovensky) */ 'use strict'; const getTokenBeforeClosingBracket = require('../util/getTokenBeforeClosingBracket'); const docsUrl = require('../util/docsUrl'); // ------------------------------------------------------------------------------ // Validators // ------------------------------------------------------------------------------ function validateClosingSlash(context, node, option) { const sourceCode = context.getSourceCode(); const SELF_CLOSING_NEVER_MESSAGE = 'Whitespace is forbidden between `/` and `>`; write `/>`'; const SELF_CLOSING_ALWAYS_MESSAGE = 'Whitespace is required between `/` and `>`; write `/ >`'; const NEVER_MESSAGE = 'Whitespace is forbidden between `<` and `/`; write `</`'; const ALWAYS_MESSAGE = 'Whitespace is required between `<` and `/`; write `< /`'; let adjacent; if (node.selfClosing) { const lastTokens = sourceCode.getLastTokens(node, 2); adjacent = !sourceCode.isSpaceBetweenTokens(lastTokens[0], lastTokens[1]); if (option === 'never') { if (!adjacent) { context.report({ node, loc: { start: lastTokens[0].loc.start, end: lastTokens[1].loc.end }, message: SELF_CLOSING_NEVER_MESSAGE, fix(fixer) { return fixer.removeRange([lastTokens[0].range[1], lastTokens[1].range[0]]); } }); } } else if (option === 'always' && adjacent) { context.report({ node, loc: { start: lastTokens[0].loc.start, end: lastTokens[1].loc.end }, message: SELF_CLOSING_ALWAYS_MESSAGE, fix(fixer) { return fixer.insertTextBefore(lastTokens[1], ' '); } }); } } else { const firstTokens = sourceCode.getFirstTokens(node, 2); adjacent = !sourceCode.isSpaceBetweenTokens(firstTokens[0], firstTokens[1]); if (option === 'never') { if (!adjacent) { context.report({ node, loc: { start: firstTokens[0].loc.start, end: firstTokens[1].loc.end }, message: NEVER_MESSAGE, fix(fixer) { return fixer.removeRange([firstTokens[0].range[1], firstTokens[1].range[0]]); } }); } } else if (option === 'always' && adjacent) { context.report({ node, loc: { start: firstTokens[0].loc.start, end: firstTokens[1].loc.end }, message: ALWAYS_MESSAGE, fix(fixer) { return fixer.insertTextBefore(firstTokens[1], ' '); } }); } } } function validateBeforeSelfClosing(context, node, option) { const sourceCode = context.getSourceCode(); const NEVER_MESSAGE = 'A space is forbidden before closing bracket'; const ALWAYS_MESSAGE = 'A space is required before closing bracket'; const leftToken = getTokenBeforeClosingBracket(node); const closingSlash = sourceCode.getTokenAfter(leftToken); if (leftToken.loc.end.line !== closingSlash.loc.start.line) { return; } if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { context.report({ node, loc: closingSlash.loc.start, message: ALWAYS_MESSAGE, fix(fixer) { return fixer.insertTextBefore(closingSlash, ' '); } }); } else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { context.report({ node, loc: closingSlash.loc.start, message: NEVER_MESSAGE, fix(fixer) { const previousToken = sourceCode.getTokenBefore(closingSlash); return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]); } }); } } function validateAfterOpening(context, node, option) { const sourceCode = context.getSourceCode(); const NEVER_MESSAGE = 'A space is forbidden after opening bracket'; const ALWAYS_MESSAGE = 'A space is required after opening bracket'; const openingToken = sourceCode.getTokenBefore(node.name); if (option === 'allow-multiline') { if (openingToken.loc.start.line !== node.name.loc.start.line) { return; } } const adjacent = !sourceCode.isSpaceBetweenTokens(openingToken, node.name); if (option === 'never' || option === 'allow-multiline') { if (!adjacent) { context.report({ node, loc: { start: openingToken.loc.start, end: node.name.loc.start }, message: NEVER_MESSAGE, fix(fixer) { return fixer.removeRange([openingToken.range[1], node.name.range[0]]); } }); } } else if (option === 'always' && adjacent) { context.report({ node, loc: { start: openingToken.loc.start, end: node.name.loc.start }, message: ALWAYS_MESSAGE, fix(fixer) { return fixer.insertTextBefore(node.name, ' '); } }); } } function validateBeforeClosing(context, node, option) { // Don't enforce this rule for self closing tags if (!node.selfClosing) { const sourceCode = context.getSourceCode(); const NEVER_MESSAGE = 'A space is forbidden before closing bracket'; const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket'; const lastTokens = sourceCode.getLastTokens(node, 2); const closingToken = lastTokens[1]; const leftToken = lastTokens[0]; if (leftToken.loc.start.line !== closingToken.loc.start.line) { return; } const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken); if (option === 'never' && !adjacent) { context.report({ node, loc: { start: leftToken.loc.end, end: closingToken.loc.start }, message: NEVER_MESSAGE, fix(fixer) { return fixer.removeRange([leftToken.range[1], closingToken.range[0]]); } }); } else if (option === 'always' && adjacent) { context.report({ node, loc: { start: leftToken.loc.end, end: closingToken.loc.start }, message: ALWAYS_MESSAGE, fix(fixer) { return fixer.insertTextBefore(closingToken, ' '); } }); } } } // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ const optionDefaults = { closingSlash: 'never', beforeSelfClosing: 'always', afterOpening: 'never', beforeClosing: 'allow' }; module.exports = { meta: { docs: { description: 'Validate whitespace in and around the JSX opening and closing brackets', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-tag-spacing') }, fixable: 'whitespace', schema: [ { type: 'object', properties: { closingSlash: { enum: ['always', 'never', 'allow'] }, beforeSelfClosing: { enum: ['always', 'never', 'allow'] }, afterOpening: { enum: ['always', 'allow-multiline', 'never', 'allow'] }, beforeClosing: { enum: ['always', 'never', 'allow'] } }, default: optionDefaults, additionalProperties: false } ] }, create(context) { const options = Object.assign({}, optionDefaults, context.options[0]); return { JSXOpeningElement(node) { if (options.closingSlash !== 'allow' && node.selfClosing) { validateClosingSlash(context, node, options.closingSlash); } if (options.afterOpening !== 'allow') { validateAfterOpening(context, node, options.afterOpening); } if (options.beforeSelfClosing !== 'allow' && node.selfClosing) { validateBeforeSelfClosing(context, node, options.beforeSelfClosing); } if (options.beforeClosing !== 'allow') { validateBeforeClosing(context, node, options.beforeClosing); } }, JSXClosingElement(node) { if (options.afterOpening !== 'allow') { validateAfterOpening(context, node, options.afterOpening); } if (options.closingSlash !== 'allow') { validateClosingSlash(context, node, options.closingSlash); } if (options.beforeClosing !== 'allow') { validateBeforeClosing(context, node, options.beforeClosing); } } }; } };