UNPKG

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