1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | 'use strict';
|
32 |
|
33 | const matchAll = require('string.prototype.matchall');
|
34 |
|
35 | const astUtil = require('../util/ast');
|
36 | const docsUrl = require('../util/docsUrl');
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | module.exports = {
|
42 | meta: {
|
43 | docs: {
|
44 | description: 'Validate JSX indentation',
|
45 | category: 'Stylistic Issues',
|
46 | recommended: false,
|
47 | url: docsUrl('jsx-indent')
|
48 | },
|
49 | fixable: 'whitespace',
|
50 | schema: [{
|
51 | oneOf: [{
|
52 | enum: ['tab']
|
53 | }, {
|
54 | type: 'integer'
|
55 | }]
|
56 | }, {
|
57 | type: 'object',
|
58 | properties: {
|
59 | checkAttributes: {
|
60 | type: 'boolean'
|
61 | },
|
62 | indentLogicalExpressions: {
|
63 | type: 'boolean'
|
64 | }
|
65 | },
|
66 | additionalProperties: false
|
67 | }]
|
68 | },
|
69 |
|
70 | create(context) {
|
71 | const MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.';
|
72 |
|
73 | const extraColumnStart = 0;
|
74 | let indentType = 'space';
|
75 | let indentSize = 4;
|
76 |
|
77 | if (context.options.length) {
|
78 | if (context.options[0] === 'tab') {
|
79 | indentSize = 1;
|
80 | indentType = 'tab';
|
81 | } else if (typeof context.options[0] === 'number') {
|
82 | indentSize = context.options[0];
|
83 | indentType = 'space';
|
84 | }
|
85 | }
|
86 |
|
87 | const indentChar = indentType === 'space' ? ' ' : '\t';
|
88 | const options = context.options[1] || {};
|
89 | const checkAttributes = options.checkAttributes || false;
|
90 | const indentLogicalExpressions = options.indentLogicalExpressions || false;
|
91 |
|
92 | |
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 | function getFixerFunction(node, needed) {
|
100 | return function fix(fixer) {
|
101 | const indent = Array(needed + 1).join(indentChar);
|
102 | if (node.type === 'JSXText' || node.type === 'Literal') {
|
103 | const regExp = /\n[\t ]*(\S)/g;
|
104 | const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
|
105 | return fixer.replaceText(node, fixedText);
|
106 | }
|
107 | return fixer.replaceTextRange(
|
108 | [node.range[0] - node.loc.start.column, node.range[0]],
|
109 | indent
|
110 | );
|
111 | };
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | function report(node, needed, gotten, loc) {
|
122 | const msgContext = {
|
123 | needed,
|
124 | type: indentType,
|
125 | characters: needed === 1 ? 'character' : 'characters',
|
126 | gotten
|
127 | };
|
128 |
|
129 | if (loc) {
|
130 | context.report({
|
131 | node,
|
132 | loc,
|
133 | message: MESSAGE,
|
134 | data: msgContext,
|
135 | fix: getFixerFunction(node, needed)
|
136 | });
|
137 | } else {
|
138 | context.report({
|
139 | node,
|
140 | message: MESSAGE,
|
141 | data: msgContext,
|
142 | fix: getFixerFunction(node, needed)
|
143 | });
|
144 | }
|
145 | }
|
146 |
|
147 | |
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | function getNodeIndent(node, byLastLine, excludeCommas) {
|
155 | byLastLine = byLastLine || false;
|
156 | excludeCommas = excludeCommas || false;
|
157 |
|
158 | let src = context.getSourceCode().getText(node, node.loc.start.column + extraColumnStart);
|
159 | const lines = src.split('\n');
|
160 | if (byLastLine) {
|
161 | src = lines[lines.length - 1];
|
162 | } else {
|
163 | src = lines[0];
|
164 | }
|
165 |
|
166 | const skip = excludeCommas ? ',' : '';
|
167 |
|
168 | let regExp;
|
169 | if (indentType === 'space') {
|
170 | regExp = new RegExp(`^[ ${skip}]+`);
|
171 | } else {
|
172 | regExp = new RegExp(`^[\t${skip}]+`);
|
173 | }
|
174 |
|
175 | const indent = regExp.exec(src);
|
176 | return indent ? indent[0].length : 0;
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 | function isRightInLogicalExp(node) {
|
185 | return (
|
186 | node.parent
|
187 | && node.parent.parent
|
188 | && node.parent.parent.type === 'LogicalExpression'
|
189 | && node.parent.parent.right === node.parent
|
190 | && !indentLogicalExpressions
|
191 | );
|
192 | }
|
193 |
|
194 | |
195 |
|
196 |
|
197 |
|
198 |
|
199 | function isAlternateInConditionalExp(node) {
|
200 | return (
|
201 | node.parent
|
202 | && node.parent.parent
|
203 | && node.parent.parent.type === 'ConditionalExpression'
|
204 | && node.parent.parent.alternate === node.parent
|
205 | && context.getSourceCode().getTokenBefore(node).value !== '('
|
206 | );
|
207 | }
|
208 |
|
209 | |
210 |
|
211 |
|
212 |
|
213 |
|
214 | function isSecondOrSubsequentExpWithinDoExp(node) {
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 | const isInExpStmt = (
|
256 | node.parent
|
257 | && node.parent.parent
|
258 | && node.parent.parent.type === 'ExpressionStatement'
|
259 | );
|
260 | if (!isInExpStmt) {
|
261 | return false;
|
262 | }
|
263 |
|
264 | const expStmt = node.parent.parent;
|
265 | const isInBlockStmtWithinDoExp = (
|
266 | expStmt.parent
|
267 | && expStmt.parent.type === 'BlockStatement'
|
268 | && expStmt.parent.parent
|
269 | && expStmt.parent.parent.type === 'DoExpression'
|
270 | );
|
271 | if (!isInBlockStmtWithinDoExp) {
|
272 | return false;
|
273 | }
|
274 |
|
275 | const blockStmt = expStmt.parent;
|
276 | const blockStmtFirstExp = blockStmt.body[0];
|
277 | return !(blockStmtFirstExp === expStmt);
|
278 | }
|
279 |
|
280 | |
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | function checkNodesIndent(node, indent, excludeCommas) {
|
287 | const nodeIndent = getNodeIndent(node, false, excludeCommas);
|
288 | const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
|
289 | const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
|
290 | if (
|
291 | nodeIndent !== indent
|
292 | && astUtil.isNodeFirstInLine(context, node)
|
293 | && !isCorrectRightInLogicalExp
|
294 | && !isCorrectAlternateInCondExp
|
295 | ) {
|
296 | report(node, indent, nodeIndent);
|
297 | }
|
298 | }
|
299 |
|
300 | |
301 |
|
302 |
|
303 |
|
304 |
|
305 | function checkLiteralNodeIndent(node, indent) {
|
306 | const value = node.value;
|
307 | const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g;
|
308 | const nodeIndentsPerLine = Array.from(
|
309 | matchAll(String(value), regExp),
|
310 | (match) => (match[1] ? match[1].length : 0)
|
311 | );
|
312 | const hasFirstInLineNode = nodeIndentsPerLine.length > 0;
|
313 | if (
|
314 | hasFirstInLineNode
|
315 | && !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)
|
316 | ) {
|
317 | nodeIndentsPerLine.forEach((nodeIndent) => {
|
318 | report(node, indent, nodeIndent);
|
319 | });
|
320 | }
|
321 | }
|
322 |
|
323 | function handleOpeningElement(node) {
|
324 | const sourceCode = context.getSourceCode();
|
325 | let prevToken = sourceCode.getTokenBefore(node);
|
326 | if (!prevToken) {
|
327 | return;
|
328 | }
|
329 |
|
330 | if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
|
331 | prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
|
332 | prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
|
333 |
|
334 | } else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
|
335 | do {
|
336 | prevToken = sourceCode.getTokenBefore(prevToken);
|
337 | } while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
|
338 | prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
|
339 | while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
|
340 | prevToken = prevToken.parent;
|
341 | }
|
342 | }
|
343 | prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
|
344 | const parentElementIndent = getNodeIndent(prevToken);
|
345 | const indent = (
|
346 | prevToken.loc.start.line === node.loc.start.line
|
347 | || isRightInLogicalExp(node)
|
348 | || isAlternateInConditionalExp(node)
|
349 | || isSecondOrSubsequentExpWithinDoExp(node)
|
350 | ) ? 0 : indentSize;
|
351 | checkNodesIndent(node, parentElementIndent + indent);
|
352 | }
|
353 |
|
354 | function handleClosingElement(node) {
|
355 | if (!node.parent) {
|
356 | return;
|
357 | }
|
358 | const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
|
359 | checkNodesIndent(node, peerElementIndent);
|
360 | }
|
361 |
|
362 | function handleAttribute(node) {
|
363 | if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
|
364 | return;
|
365 | }
|
366 | const nameIndent = getNodeIndent(node.name);
|
367 | const lastToken = context.getSourceCode().getLastToken(node.value);
|
368 | const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
|
369 | const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
|
370 | checkNodesIndent(firstInLine, indent);
|
371 | }
|
372 |
|
373 | function handleLiteral(node) {
|
374 | if (!node.parent) {
|
375 | return;
|
376 | }
|
377 | if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') {
|
378 | return;
|
379 | }
|
380 | const parentNodeIndent = getNodeIndent(node.parent);
|
381 | checkLiteralNodeIndent(node, parentNodeIndent + indentSize);
|
382 | }
|
383 |
|
384 | return {
|
385 | JSXOpeningElement: handleOpeningElement,
|
386 | JSXOpeningFragment: handleOpeningElement,
|
387 | JSXClosingElement: handleClosingElement,
|
388 | JSXClosingFragment: handleClosingElement,
|
389 | JSXAttribute: handleAttribute,
|
390 | JSXExpressionContainer(node) {
|
391 | if (!node.parent) {
|
392 | return;
|
393 | }
|
394 | const parentNodeIndent = getNodeIndent(node.parent);
|
395 | checkNodesIndent(node, parentNodeIndent + indentSize);
|
396 | },
|
397 | Literal: handleLiteral,
|
398 | JSXText: handleLiteral
|
399 | };
|
400 | }
|
401 | };
|