1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const has = require('has');
|
9 | const docsUrl = require('../util/docsUrl');
|
10 | const jsxUtil = require('../util/jsx');
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const DEFAULTS = {
|
17 | declaration: 'parens',
|
18 | assignment: 'parens',
|
19 | return: 'parens',
|
20 | arrow: 'parens',
|
21 | condition: 'ignore',
|
22 | logical: 'ignore',
|
23 | prop: 'ignore'
|
24 | };
|
25 |
|
26 | const MISSING_PARENS = 'Missing parentheses around multilines JSX';
|
27 | const PARENS_NEW_LINES = 'Parentheses around JSX should be on separate lines';
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | module.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 |
|
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 |
|
139 |
|
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 |
|
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 |
|
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 | };
|