123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /**
- * @fileoverview Prevent missing parentheses around multilines JSX
- * @author Yannick Croissant
- */
- 'use strict';
- const has = require('has');
- const docsUrl = require('../util/docsUrl');
- const jsxUtil = require('../util/jsx');
- // ------------------------------------------------------------------------------
- // Constants
- // ------------------------------------------------------------------------------
- const DEFAULTS = {
- declaration: 'parens',
- assignment: 'parens',
- return: 'parens',
- arrow: 'parens',
- condition: 'ignore',
- logical: 'ignore',
- prop: 'ignore'
- };
- // ------------------------------------------------------------------------------
- // Rule Definition
- // ------------------------------------------------------------------------------
- module.exports = {
- meta: {
- docs: {
- description: 'Prevent missing parentheses around multilines JSX',
- category: 'Stylistic Issues',
- recommended: false,
- url: docsUrl('jsx-wrap-multilines')
- },
- fixable: 'code',
- messages: {
- missingParens: 'Missing parentheses around multilines JSX',
- parensOnNewLines: 'Parentheses around JSX should be on separate lines'
- },
- schema: [{
- type: 'object',
- // true/false are for backwards compatibility
- properties: {
- declaration: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- assignment: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- return: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- arrow: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- condition: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- logical: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- },
- prop: {
- enum: [true, false, 'ignore', 'parens', 'parens-new-line']
- }
- },
- additionalProperties: false
- }]
- },
- create(context) {
- function getOption(type) {
- const userOptions = context.options[0] || {};
- if (has(userOptions, type)) {
- return userOptions[type];
- }
- return DEFAULTS[type];
- }
- function isEnabled(type) {
- const option = getOption(type);
- return option && option !== 'ignore';
- }
- function isParenthesised(node) {
- const sourceCode = context.getSourceCode();
- const previousToken = sourceCode.getTokenBefore(node);
- const nextToken = sourceCode.getTokenAfter(node);
- return previousToken && nextToken
- && previousToken.value === '(' && previousToken.range[1] <= node.range[0]
- && nextToken.value === ')' && nextToken.range[0] >= node.range[1];
- }
- function needsOpeningNewLine(node) {
- const previousToken = context.getSourceCode().getTokenBefore(node);
- if (!isParenthesised(node)) {
- return false;
- }
- if (previousToken.loc.end.line === node.loc.start.line) {
- return true;
- }
- return false;
- }
- function needsClosingNewLine(node) {
- const nextToken = context.getSourceCode().getTokenAfter(node);
- if (!isParenthesised(node)) {
- return false;
- }
- if (node.loc.end.line === nextToken.loc.end.line) {
- return true;
- }
- return false;
- }
- function isMultilines(node) {
- return node.loc.start.line !== node.loc.end.line;
- }
- function report(node, messageId, fix) {
- context.report({
- node,
- messageId,
- fix
- });
- }
- function trimTokenBeforeNewline(node, tokenBefore) {
- // if the token before the jsx is a bracket or curly brace
- // we don't want a space between the opening parentheses and the multiline jsx
- const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
- return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
- }
- function check(node, type) {
- if (!node || !jsxUtil.isJSX(node)) {
- return;
- }
- const sourceCode = context.getSourceCode();
- const option = getOption(type);
- if ((option === true || option === 'parens') && !isParenthesised(node) && isMultilines(node)) {
- report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`));
- }
- if (option === 'parens-new-line' && isMultilines(node)) {
- if (!isParenthesised(node)) {
- const tokenBefore = sourceCode.getTokenBefore(node, {includeComments: true});
- const tokenAfter = sourceCode.getTokenAfter(node, {includeComments: true});
- const start = node.loc.start;
- if (tokenBefore.loc.end.line < start.line) {
- // Strip newline after operator if parens newline is specified
- report(
- node,
- 'missingParens',
- (fixer) => fixer.replaceTextRange(
- [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
- `${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${sourceCode.getText(node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
- )
- );
- } else {
- report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${sourceCode.getText(node)}\n)`));
- }
- } else {
- const needsOpening = needsOpeningNewLine(node);
- const needsClosing = needsClosingNewLine(node);
- if (needsOpening || needsClosing) {
- report(node, 'parensOnNewLines', (fixer) => {
- const text = sourceCode.getText(node);
- let fixed = text;
- if (needsOpening) {
- fixed = `\n${fixed}`;
- }
- if (needsClosing) {
- fixed = `${fixed}\n`;
- }
- return fixer.replaceText(node, fixed);
- });
- }
- }
- }
- }
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
- return {
- VariableDeclarator(node) {
- const type = 'declaration';
- if (!isEnabled(type)) {
- return;
- }
- if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
- check(node.init.consequent, type);
- check(node.init.alternate, type);
- return;
- }
- check(node.init, type);
- },
- AssignmentExpression(node) {
- const type = 'assignment';
- if (!isEnabled(type)) {
- return;
- }
- if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
- check(node.right.consequent, type);
- check(node.right.alternate, type);
- return;
- }
- check(node.right, type);
- },
- ReturnStatement(node) {
- const type = 'return';
- if (isEnabled(type)) {
- check(node.argument, type);
- }
- },
- 'ArrowFunctionExpression:exit': (node) => {
- const arrowBody = node.body;
- const type = 'arrow';
- if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
- check(arrowBody, type);
- }
- },
- ConditionalExpression(node) {
- const type = 'condition';
- if (isEnabled(type)) {
- check(node.consequent, type);
- check(node.alternate, type);
- }
- },
- LogicalExpression(node) {
- const type = 'logical';
- if (isEnabled(type)) {
- check(node.right, type);
- }
- },
- JSXAttribute(node) {
- const type = 'prop';
- if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
- check(node.value.expression, type);
- }
- }
- };
- }
- };
|