[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Prevent missing parentheses around multilines JSX
|
---|
| 3 | * @author Yannick Croissant
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | 'use strict';
|
---|
| 7 |
|
---|
| 8 | const has = require('hasown');
|
---|
| 9 | const docsUrl = require('../util/docsUrl');
|
---|
| 10 | const eslintUtil = require('../util/eslint');
|
---|
| 11 | const jsxUtil = require('../util/jsx');
|
---|
| 12 | const reportC = require('../util/report');
|
---|
| 13 | const isParenthesized = require('../util/ast').isParenthesized;
|
---|
| 14 |
|
---|
| 15 | const getSourceCode = eslintUtil.getSourceCode;
|
---|
| 16 | const getText = eslintUtil.getText;
|
---|
| 17 |
|
---|
| 18 | // ------------------------------------------------------------------------------
|
---|
| 19 | // Constants
|
---|
| 20 | // ------------------------------------------------------------------------------
|
---|
| 21 |
|
---|
| 22 | const DEFAULTS = {
|
---|
| 23 | declaration: 'parens',
|
---|
| 24 | assignment: 'parens',
|
---|
| 25 | return: 'parens',
|
---|
| 26 | arrow: 'parens',
|
---|
| 27 | condition: 'ignore',
|
---|
| 28 | logical: 'ignore',
|
---|
| 29 | prop: 'ignore',
|
---|
| 30 | };
|
---|
| 31 |
|
---|
| 32 | // ------------------------------------------------------------------------------
|
---|
| 33 | // Rule Definition
|
---|
| 34 | // ------------------------------------------------------------------------------
|
---|
| 35 |
|
---|
| 36 | const messages = {
|
---|
| 37 | missingParens: 'Missing parentheses around multilines JSX',
|
---|
| 38 | extraParens: 'Expected no parentheses around multilines JSX',
|
---|
| 39 | parensOnNewLines: 'Parentheses around JSX should be on separate lines',
|
---|
| 40 | };
|
---|
| 41 |
|
---|
| 42 | /** @type {import('eslint').Rule.RuleModule} */
|
---|
| 43 | module.exports = {
|
---|
| 44 | meta: {
|
---|
| 45 | docs: {
|
---|
| 46 | description: 'Disallow missing parentheses around multiline JSX',
|
---|
| 47 | category: 'Stylistic Issues',
|
---|
| 48 | recommended: false,
|
---|
| 49 | url: docsUrl('jsx-wrap-multilines'),
|
---|
| 50 | },
|
---|
| 51 | fixable: 'code',
|
---|
| 52 |
|
---|
| 53 | messages,
|
---|
| 54 |
|
---|
| 55 | schema: [{
|
---|
| 56 | type: 'object',
|
---|
| 57 | // true/false are for backwards compatibility
|
---|
| 58 | properties: {
|
---|
| 59 | declaration: {
|
---|
| 60 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 61 | },
|
---|
| 62 | assignment: {
|
---|
| 63 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 64 | },
|
---|
| 65 | return: {
|
---|
| 66 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 67 | },
|
---|
| 68 | arrow: {
|
---|
| 69 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 70 | },
|
---|
| 71 | condition: {
|
---|
| 72 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 73 | },
|
---|
| 74 | logical: {
|
---|
| 75 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 76 | },
|
---|
| 77 | prop: {
|
---|
| 78 | enum: [true, false, 'ignore', 'parens', 'parens-new-line', 'never'],
|
---|
| 79 | },
|
---|
| 80 | },
|
---|
| 81 | additionalProperties: false,
|
---|
| 82 | }],
|
---|
| 83 | },
|
---|
| 84 |
|
---|
| 85 | create(context) {
|
---|
| 86 | function getOption(type) {
|
---|
| 87 | const userOptions = context.options[0] || {};
|
---|
| 88 | if (has(userOptions, type)) {
|
---|
| 89 | return userOptions[type];
|
---|
| 90 | }
|
---|
| 91 | return DEFAULTS[type];
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | function isEnabled(type) {
|
---|
| 95 | const option = getOption(type);
|
---|
| 96 | return option && option !== 'ignore';
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | function needsOpeningNewLine(node) {
|
---|
| 100 | const previousToken = getSourceCode(context).getTokenBefore(node);
|
---|
| 101 |
|
---|
| 102 | if (!isParenthesized(context, node)) {
|
---|
| 103 | return false;
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | if (previousToken.loc.end.line === node.loc.start.line) {
|
---|
| 107 | return true;
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | return false;
|
---|
| 111 | }
|
---|
| 112 |
|
---|
| 113 | function needsClosingNewLine(node) {
|
---|
| 114 | const nextToken = getSourceCode(context).getTokenAfter(node);
|
---|
| 115 |
|
---|
| 116 | if (!isParenthesized(context, node)) {
|
---|
| 117 | return false;
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | if (node.loc.end.line === nextToken.loc.end.line) {
|
---|
| 121 | return true;
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | return false;
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | function isMultilines(node) {
|
---|
| 128 | return node.loc.start.line !== node.loc.end.line;
|
---|
| 129 | }
|
---|
| 130 |
|
---|
| 131 | function report(node, messageId, fix) {
|
---|
| 132 | reportC(context, messages[messageId], messageId, {
|
---|
| 133 | node,
|
---|
| 134 | fix,
|
---|
| 135 | });
|
---|
| 136 | }
|
---|
| 137 |
|
---|
| 138 | function trimTokenBeforeNewline(node, tokenBefore) {
|
---|
| 139 | // if the token before the jsx is a bracket or curly brace
|
---|
| 140 | // we don't want a space between the opening parentheses and the multiline jsx
|
---|
| 141 | const isBracket = tokenBefore.value === '{' || tokenBefore.value === '[';
|
---|
| 142 | return `${tokenBefore.value.trim()}${isBracket ? '' : ' '}`;
|
---|
| 143 | }
|
---|
| 144 |
|
---|
| 145 | function check(node, type) {
|
---|
| 146 | if (!node || !jsxUtil.isJSX(node)) {
|
---|
| 147 | return;
|
---|
| 148 | }
|
---|
| 149 |
|
---|
| 150 | const sourceCode = getSourceCode(context);
|
---|
| 151 | const option = getOption(type);
|
---|
| 152 |
|
---|
| 153 | if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) {
|
---|
| 154 | report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${getText(context, node)})`));
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | if (option === 'parens-new-line' && isMultilines(node)) {
|
---|
| 158 | if (!isParenthesized(context, node)) {
|
---|
| 159 | const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
|
---|
| 160 | const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true });
|
---|
| 161 | const start = node.loc.start;
|
---|
| 162 | if (tokenBefore.loc.end.line < start.line) {
|
---|
| 163 | // Strip newline after operator if parens newline is specified
|
---|
| 164 | report(
|
---|
| 165 | node,
|
---|
| 166 | 'missingParens',
|
---|
| 167 | (fixer) => fixer.replaceTextRange(
|
---|
| 168 | [tokenBefore.range[0], tokenAfter && (tokenAfter.value === ';' || tokenAfter.value === '}') ? tokenAfter.range[0] : node.range[1]],
|
---|
| 169 | `${trimTokenBeforeNewline(node, tokenBefore)}(\n${start.column > 0 ? ' '.repeat(start.column) : ''}${getText(context, node)}\n${start.column > 0 ? ' '.repeat(start.column - 2) : ''})`
|
---|
| 170 | )
|
---|
| 171 | );
|
---|
| 172 | } else {
|
---|
| 173 | report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(\n${getText(context, node)}\n)`));
|
---|
| 174 | }
|
---|
| 175 | } else {
|
---|
| 176 | const needsOpening = needsOpeningNewLine(node);
|
---|
| 177 | const needsClosing = needsClosingNewLine(node);
|
---|
| 178 | if (needsOpening || needsClosing) {
|
---|
| 179 | report(node, 'parensOnNewLines', (fixer) => {
|
---|
| 180 | const text = getText(context, node);
|
---|
| 181 | let fixed = text;
|
---|
| 182 | if (needsOpening) {
|
---|
| 183 | fixed = `\n${fixed}`;
|
---|
| 184 | }
|
---|
| 185 | if (needsClosing) {
|
---|
| 186 | fixed = `${fixed}\n`;
|
---|
| 187 | }
|
---|
| 188 | return fixer.replaceText(node, fixed);
|
---|
| 189 | });
|
---|
| 190 | }
|
---|
| 191 | }
|
---|
| 192 | }
|
---|
| 193 |
|
---|
| 194 | if (option === 'never' && isParenthesized(context, node)) {
|
---|
| 195 | const tokenBefore = sourceCode.getTokenBefore(node);
|
---|
| 196 | const tokenAfter = sourceCode.getTokenAfter(node);
|
---|
| 197 | report(node, 'extraParens', (fixer) => fixer.replaceTextRange(
|
---|
| 198 | [tokenBefore.range[0], tokenAfter.range[1]],
|
---|
| 199 | getText(context, node)
|
---|
| 200 | ));
|
---|
| 201 | }
|
---|
| 202 | }
|
---|
| 203 |
|
---|
| 204 | // --------------------------------------------------------------------------
|
---|
| 205 | // Public
|
---|
| 206 | // --------------------------------------------------------------------------
|
---|
| 207 |
|
---|
| 208 | return {
|
---|
| 209 |
|
---|
| 210 | VariableDeclarator(node) {
|
---|
| 211 | const type = 'declaration';
|
---|
| 212 | if (!isEnabled(type)) {
|
---|
| 213 | return;
|
---|
| 214 | }
|
---|
| 215 | if (!isEnabled('condition') && node.init && node.init.type === 'ConditionalExpression') {
|
---|
| 216 | check(node.init.consequent, type);
|
---|
| 217 | check(node.init.alternate, type);
|
---|
| 218 | return;
|
---|
| 219 | }
|
---|
| 220 | check(node.init, type);
|
---|
| 221 | },
|
---|
| 222 |
|
---|
| 223 | AssignmentExpression(node) {
|
---|
| 224 | const type = 'assignment';
|
---|
| 225 | if (!isEnabled(type)) {
|
---|
| 226 | return;
|
---|
| 227 | }
|
---|
| 228 | if (!isEnabled('condition') && node.right.type === 'ConditionalExpression') {
|
---|
| 229 | check(node.right.consequent, type);
|
---|
| 230 | check(node.right.alternate, type);
|
---|
| 231 | return;
|
---|
| 232 | }
|
---|
| 233 | check(node.right, type);
|
---|
| 234 | },
|
---|
| 235 |
|
---|
| 236 | ReturnStatement(node) {
|
---|
| 237 | const type = 'return';
|
---|
| 238 | if (isEnabled(type)) {
|
---|
| 239 | check(node.argument, type);
|
---|
| 240 | }
|
---|
| 241 | },
|
---|
| 242 |
|
---|
| 243 | 'ArrowFunctionExpression:exit': (node) => {
|
---|
| 244 | const arrowBody = node.body;
|
---|
| 245 | const type = 'arrow';
|
---|
| 246 |
|
---|
| 247 | if (isEnabled(type) && arrowBody.type !== 'BlockStatement') {
|
---|
| 248 | check(arrowBody, type);
|
---|
| 249 | }
|
---|
| 250 | },
|
---|
| 251 |
|
---|
| 252 | ConditionalExpression(node) {
|
---|
| 253 | const type = 'condition';
|
---|
| 254 | if (isEnabled(type)) {
|
---|
| 255 | check(node.consequent, type);
|
---|
| 256 | check(node.alternate, type);
|
---|
| 257 | }
|
---|
| 258 | },
|
---|
| 259 |
|
---|
| 260 | LogicalExpression(node) {
|
---|
| 261 | const type = 'logical';
|
---|
| 262 | if (isEnabled(type)) {
|
---|
| 263 | check(node.right, type);
|
---|
| 264 | }
|
---|
| 265 | },
|
---|
| 266 |
|
---|
| 267 | JSXAttribute(node) {
|
---|
| 268 | const type = 'prop';
|
---|
| 269 | if (isEnabled(type) && node.value && node.value.type === 'JSXExpressionContainer') {
|
---|
| 270 | check(node.value.expression, type);
|
---|
| 271 | }
|
---|
| 272 | },
|
---|
| 273 | };
|
---|
| 274 | },
|
---|
| 275 | };
|
---|