You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
7.6 KiB
260 lines
7.6 KiB
/** |
|
* @fileoverview Prevent missing parentheses around multilines JSX |
|
* @author Yannick Croissant |
|
*/ |
|
|
|
'use strict'; |
|
|
|
const has = require('object.hasown/polyfill')(); |
|
const docsUrl = require('../util/docsUrl'); |
|
const jsxUtil = require('../util/jsx'); |
|
const reportC = require('../util/report'); |
|
const isParenthesized = require('../util/ast').isParenthesized; |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Constants |
|
// ------------------------------------------------------------------------------ |
|
|
|
const DEFAULTS = { |
|
declaration: 'parens', |
|
assignment: 'parens', |
|
return: 'parens', |
|
arrow: 'parens', |
|
condition: 'ignore', |
|
logical: 'ignore', |
|
prop: 'ignore', |
|
}; |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Rule Definition |
|
// ------------------------------------------------------------------------------ |
|
|
|
const messages = { |
|
missingParens: 'Missing parentheses around multilines JSX', |
|
parensOnNewLines: 'Parentheses around JSX should be on separate lines', |
|
}; |
|
|
|
module.exports = { |
|
meta: { |
|
docs: { |
|
description: 'Prevent missing parentheses around multilines JSX', |
|
category: 'Stylistic Issues', |
|
recommended: false, |
|
url: docsUrl('jsx-wrap-multilines'), |
|
}, |
|
fixable: 'code', |
|
|
|
messages, |
|
|
|
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 needsOpeningNewLine(node) { |
|
const previousToken = context.getSourceCode().getTokenBefore(node); |
|
|
|
if (!isParenthesized(context, 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 (!isParenthesized(context, 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) { |
|
reportC(context, messages[messageId], messageId, { |
|
node, |
|
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') && !isParenthesized(context, node) && isMultilines(node)) { |
|
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); |
|
} |
|
|
|
if (option === 'parens-new-line' && isMultilines(node)) { |
|
if (!isParenthesized(context, 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); |
|
} |
|
}, |
|
}; |
|
}, |
|
};
|
|
|