source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-indent.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 14.3 KB
Line 
1/**
2 * @fileoverview Validate JSX indentation
3 * @author Yannick Croissant
4
5 * This rule has been ported and modified from eslint and nodeca.
6 * @author Vitaly Puzrin
7 * @author Gyandeep Singh
8 * @copyright 2015 Vitaly Puzrin. All rights reserved.
9 * @copyright 2015 Gyandeep Singh. All rights reserved.
10 Copyright (C) 2014 by Vitaly Puzrin
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the 'Software'), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in
20 all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28 THE SOFTWARE.
29 */
30
31'use strict';
32
33const matchAll = require('string.prototype.matchall');
34const repeat = require('string.prototype.repeat');
35
36const astUtil = require('../util/ast');
37const docsUrl = require('../util/docsUrl');
38const reportC = require('../util/report');
39const jsxUtil = require('../util/jsx');
40const eslintUtil = require('../util/eslint');
41
42const getSourceCode = eslintUtil.getSourceCode;
43const getText = eslintUtil.getText;
44
45// ------------------------------------------------------------------------------
46// Rule Definition
47// ------------------------------------------------------------------------------
48
49const messages = {
50 wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
51};
52
53module.exports = {
54 meta: {
55 docs: {
56 description: 'Enforce JSX indentation',
57 category: 'Stylistic Issues',
58 recommended: false,
59 url: docsUrl('jsx-indent'),
60 },
61 fixable: 'whitespace',
62
63 messages,
64
65 schema: [{
66 anyOf: [{
67 enum: ['tab'],
68 }, {
69 type: 'integer',
70 }],
71 }, {
72 type: 'object',
73 properties: {
74 checkAttributes: {
75 type: 'boolean',
76 },
77 indentLogicalExpressions: {
78 type: 'boolean',
79 },
80 },
81 additionalProperties: false,
82 }],
83 },
84
85 create(context) {
86 const extraColumnStart = 0;
87 let indentType = 'space';
88 let indentSize = 4;
89
90 if (context.options.length) {
91 if (context.options[0] === 'tab') {
92 indentSize = 1;
93 indentType = 'tab';
94 } else if (typeof context.options[0] === 'number') {
95 indentSize = context.options[0];
96 indentType = 'space';
97 }
98 }
99
100 const indentChar = indentType === 'space' ? ' ' : '\t';
101 const options = context.options[1] || {};
102 const checkAttributes = options.checkAttributes || false;
103 const indentLogicalExpressions = options.indentLogicalExpressions || false;
104
105 /**
106 * Responsible for fixing the indentation issue fix
107 * @param {ASTNode} node Node violating the indent rule
108 * @param {Number} needed Expected indentation character count
109 * @returns {Function} function to be executed by the fixer
110 * @private
111 */
112 function getFixerFunction(node, needed) {
113 const indent = repeat(indentChar, needed);
114
115 if (node.type === 'JSXText' || node.type === 'Literal') {
116 return function fix(fixer) {
117 const regExp = /\n[\t ]*(\S)/g;
118 const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
119 return fixer.replaceText(node, fixedText);
120 };
121 }
122
123 if (node.type === 'ReturnStatement') {
124 const raw = getText(context, node);
125 const lines = raw.split('\n');
126 if (lines.length > 1) {
127 return function fix(fixer) {
128 const lastLineStart = raw.lastIndexOf('\n');
129 const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`);
130 return fixer.replaceTextRange(
131 [node.range[0] + lastLineStart, node.range[1]],
132 lastLine
133 );
134 };
135 }
136 }
137
138 return function fix(fixer) {
139 return fixer.replaceTextRange(
140 [node.range[0] - node.loc.start.column, node.range[0]],
141 indent
142 );
143 };
144 }
145
146 /**
147 * Reports a given indent violation and properly pluralizes the message
148 * @param {ASTNode} node Node violating the indent rule
149 * @param {Number} needed Expected indentation character count
150 * @param {Number} gotten Indentation character count in the actual node/code
151 * @param {Object} [loc] Error line and column location
152 */
153 function report(node, needed, gotten, loc) {
154 const msgContext = {
155 needed,
156 type: indentType,
157 characters: needed === 1 ? 'character' : 'characters',
158 gotten,
159 };
160
161 reportC(context, messages.wrongIndent, 'wrongIndent', Object.assign({
162 node,
163 data: msgContext,
164 fix: getFixerFunction(node, needed),
165 }, loc && { loc }));
166 }
167
168 /**
169 * Get node indent
170 * @param {ASTNode} node Node to examine
171 * @param {Boolean} [byLastLine] get indent of node's last line
172 * @param {Boolean} [excludeCommas] skip comma on start of line
173 * @return {Number} Indent
174 */
175 function getNodeIndent(node, byLastLine, excludeCommas) {
176 let src = getText(context, node, node.loc.start.column + extraColumnStart);
177 const lines = src.split('\n');
178 if (byLastLine) {
179 src = lines[lines.length - 1];
180 } else {
181 src = lines[0];
182 }
183
184 const skip = excludeCommas ? ',' : '';
185
186 let regExp;
187 if (indentType === 'space') {
188 regExp = new RegExp(`^[ ${skip}]+`);
189 } else {
190 regExp = new RegExp(`^[\t${skip}]+`);
191 }
192
193 const indent = regExp.exec(src);
194 return indent ? indent[0].length : 0;
195 }
196
197 /**
198 * Check if the node is the right member of a logical expression
199 * @param {ASTNode} node The node to check
200 * @return {Boolean} true if its the case, false if not
201 */
202 function isRightInLogicalExp(node) {
203 return (
204 node.parent
205 && node.parent.parent
206 && node.parent.parent.type === 'LogicalExpression'
207 && node.parent.parent.right === node.parent
208 && !indentLogicalExpressions
209 );
210 }
211
212 /**
213 * Check if the node is the alternate member of a conditional expression
214 * @param {ASTNode} node The node to check
215 * @return {Boolean} true if its the case, false if not
216 */
217 function isAlternateInConditionalExp(node) {
218 return (
219 node.parent
220 && node.parent.parent
221 && node.parent.parent.type === 'ConditionalExpression'
222 && node.parent.parent.alternate === node.parent
223 && getSourceCode(context).getTokenBefore(node).value !== '('
224 );
225 }
226
227 /**
228 * Check if the node is within a DoExpression block but not the first expression (which need to be indented)
229 * @param {ASTNode} node The node to check
230 * @return {Boolean} true if its the case, false if not
231 */
232 function isSecondOrSubsequentExpWithinDoExp(node) {
233 /*
234 It returns true when node.parent.parent.parent.parent matches:
235
236 DoExpression({
237 ...,
238 body: BlockStatement({
239 ...,
240 body: [
241 ..., // 1-n times
242 ExpressionStatement({
243 ...,
244 expression: JSXElement({
245 ...,
246 openingElement: JSXOpeningElement() // the node
247 })
248 }),
249 ... // 0-n times
250 ]
251 })
252 })
253
254 except:
255
256 DoExpression({
257 ...,
258 body: BlockStatement({
259 ...,
260 body: [
261 ExpressionStatement({
262 ...,
263 expression: JSXElement({
264 ...,
265 openingElement: JSXOpeningElement() // the node
266 })
267 }),
268 ... // 0-n times
269 ]
270 })
271 })
272 */
273 const isInExpStmt = (
274 node.parent
275 && node.parent.parent
276 && node.parent.parent.type === 'ExpressionStatement'
277 );
278 if (!isInExpStmt) {
279 return false;
280 }
281
282 const expStmt = node.parent.parent;
283 const isInBlockStmtWithinDoExp = (
284 expStmt.parent
285 && expStmt.parent.type === 'BlockStatement'
286 && expStmt.parent.parent
287 && expStmt.parent.parent.type === 'DoExpression'
288 );
289 if (!isInBlockStmtWithinDoExp) {
290 return false;
291 }
292
293 const blockStmt = expStmt.parent;
294 const blockStmtFirstExp = blockStmt.body[0];
295 return !(blockStmtFirstExp === expStmt);
296 }
297
298 /**
299 * Check indent for nodes list
300 * @param {ASTNode} node The node to check
301 * @param {Number} indent needed indent
302 * @param {Boolean} [excludeCommas] skip comma on start of line
303 */
304 function checkNodesIndent(node, indent, excludeCommas) {
305 const nodeIndent = getNodeIndent(node, false, excludeCommas);
306 const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
307 const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
308 if (
309 nodeIndent !== indent
310 && astUtil.isNodeFirstInLine(context, node)
311 && !isCorrectRightInLogicalExp
312 && !isCorrectAlternateInCondExp
313 ) {
314 report(node, indent, nodeIndent);
315 }
316 }
317
318 /**
319 * Check indent for Literal Node or JSXText Node
320 * @param {ASTNode} node The node to check
321 * @param {Number} indent needed indent
322 */
323 function checkLiteralNodeIndent(node, indent) {
324 const value = node.value;
325 const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g;
326 const nodeIndentsPerLine = Array.from(
327 matchAll(String(value), regExp),
328 (match) => (match[1] ? match[1].length : 0)
329 );
330 const hasFirstInLineNode = nodeIndentsPerLine.length > 0;
331 if (
332 hasFirstInLineNode
333 && !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)
334 ) {
335 nodeIndentsPerLine.forEach((nodeIndent) => {
336 report(node, indent, nodeIndent);
337 });
338 }
339 }
340
341 function handleOpeningElement(node) {
342 const sourceCode = getSourceCode(context);
343 let prevToken = sourceCode.getTokenBefore(node);
344 if (!prevToken) {
345 return;
346 }
347 // Use the parent in a list or an array
348 if (prevToken.type === 'JSXText' || ((prevToken.type === 'Punctuator') && prevToken.value === ',')) {
349 prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
350 prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
351 // Use the first non-punctuator token in a conditional expression
352 } else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
353 do {
354 prevToken = sourceCode.getTokenBefore(prevToken);
355 } while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
356 prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
357 while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
358 prevToken = prevToken.parent;
359 }
360 }
361 prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
362 const parentElementIndent = getNodeIndent(prevToken);
363 const indent = (
364 prevToken.loc.start.line === node.loc.start.line
365 || isRightInLogicalExp(node)
366 || isAlternateInConditionalExp(node)
367 || isSecondOrSubsequentExpWithinDoExp(node)
368 ) ? 0 : indentSize;
369 checkNodesIndent(node, parentElementIndent + indent);
370 }
371
372 function handleClosingElement(node) {
373 if (!node.parent) {
374 return;
375 }
376 const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
377 checkNodesIndent(node, peerElementIndent);
378 }
379
380 function handleAttribute(node) {
381 if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
382 return;
383 }
384 const nameIndent = getNodeIndent(node.name);
385 const lastToken = getSourceCode(context).getLastToken(node.value);
386 const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
387 const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
388 checkNodesIndent(firstInLine, indent);
389 }
390
391 function handleLiteral(node) {
392 if (!node.parent) {
393 return;
394 }
395 if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') {
396 return;
397 }
398 const parentNodeIndent = getNodeIndent(node.parent);
399 checkLiteralNodeIndent(node, parentNodeIndent + indentSize);
400 }
401
402 return {
403 JSXOpeningElement: handleOpeningElement,
404 JSXOpeningFragment: handleOpeningElement,
405 JSXClosingElement: handleClosingElement,
406 JSXClosingFragment: handleClosingElement,
407 JSXAttribute: handleAttribute,
408 JSXExpressionContainer(node) {
409 if (!node.parent) {
410 return;
411 }
412 const parentNodeIndent = getNodeIndent(node.parent);
413 checkNodesIndent(node, parentNodeIndent + indentSize);
414 },
415 Literal: handleLiteral,
416 JSXText: handleLiteral,
417
418 ReturnStatement(node) {
419 if (
420 !node.parent
421 || !jsxUtil.isJSX(node.argument)
422 ) {
423 return;
424 }
425
426 let fn = node.parent;
427 while (fn && fn.type !== 'FunctionDeclaration' && fn.type !== 'FunctionExpression') {
428 fn = fn.parent;
429 }
430 if (
431 !fn
432 || !jsxUtil.isReturningJSX(context, node, true)
433 ) {
434 return;
435 }
436
437 const openingIndent = getNodeIndent(node);
438 const closingIndent = getNodeIndent(node, true);
439
440 if (openingIndent !== closingIndent) {
441 report(node, openingIndent, closingIndent);
442 }
443 },
444 };
445 },
446};
Note: See TracBrowser for help on using the repository browser.