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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

  • 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
53/** @type {import('eslint').Rule.RuleModule} */
54module.exports = {
55 meta: {
56 docs: {
57 description: 'Enforce JSX indentation',
58 category: 'Stylistic Issues',
59 recommended: false,
60 url: docsUrl('jsx-indent'),
61 },
62 fixable: 'whitespace',
63
64 messages,
65
66 schema: [{
67 anyOf: [{
68 enum: ['tab'],
69 }, {
70 type: 'integer',
71 }],
72 }, {
73 type: 'object',
74 properties: {
75 checkAttributes: {
76 type: 'boolean',
77 },
78 indentLogicalExpressions: {
79 type: 'boolean',
80 },
81 },
82 additionalProperties: false,
83 }],
84 },
85
86 create(context) {
87 const extraColumnStart = 0;
88 let indentType = 'space';
89 let indentSize = 4;
90
91 if (context.options.length) {
92 if (context.options[0] === 'tab') {
93 indentSize = 1;
94 indentType = 'tab';
95 } else if (typeof context.options[0] === 'number') {
96 indentSize = context.options[0];
97 indentType = 'space';
98 }
99 }
100
101 const indentChar = indentType === 'space' ? ' ' : '\t';
102 const options = context.options[1] || {};
103 const checkAttributes = options.checkAttributes || false;
104 const indentLogicalExpressions = options.indentLogicalExpressions || false;
105
106 /**
107 * Responsible for fixing the indentation issue fix
108 * @param {ASTNode} node Node violating the indent rule
109 * @param {number} needed Expected indentation character count
110 * @returns {Function} function to be executed by the fixer
111 * @private
112 */
113 function getFixerFunction(node, needed) {
114 const indent = repeat(indentChar, needed);
115
116 if (node.type === 'JSXText' || node.type === 'Literal') {
117 return function fix(fixer) {
118 const regExp = /\n[\t ]*(\S)/g;
119 const fixedText = node.raw.replace(regExp, (match, p1) => `\n${indent}${p1}`);
120 return fixer.replaceText(node, fixedText);
121 };
122 }
123
124 if (node.type === 'ReturnStatement') {
125 const raw = getText(context, node);
126 const lines = raw.split('\n');
127 if (lines.length > 1) {
128 return function fix(fixer) {
129 const lastLineStart = raw.lastIndexOf('\n');
130 const lastLine = raw.slice(lastLineStart).replace(/^\n[\t ]*(\S)/, (match, p1) => `\n${indent}${p1}`);
131 return fixer.replaceTextRange(
132 [node.range[0] + lastLineStart, node.range[1]],
133 lastLine
134 );
135 };
136 }
137 }
138
139 return function fix(fixer) {
140 return fixer.replaceTextRange(
141 [node.range[0] - node.loc.start.column, node.range[0]],
142 indent
143 );
144 };
145 }
146
147 /**
148 * Reports a given indent violation and properly pluralizes the message
149 * @param {ASTNode} node Node violating the indent rule
150 * @param {number} needed Expected indentation character count
151 * @param {number} gotten Indentation character count in the actual node/code
152 * @param {Object} [loc] Error line and column location
153 */
154 function report(node, needed, gotten, loc) {
155 const msgContext = {
156 needed,
157 type: indentType,
158 characters: needed === 1 ? 'character' : 'characters',
159 gotten,
160 };
161
162 reportC(context, messages.wrongIndent, 'wrongIndent', Object.assign({
163 node,
164 data: msgContext,
165 fix: getFixerFunction(node, needed),
166 }, loc && { loc }));
167 }
168
169 /**
170 * Get node indent
171 * @param {ASTNode} node Node to examine
172 * @param {boolean} [byLastLine] get indent of node's last line
173 * @param {boolean} [excludeCommas] skip comma on start of line
174 * @return {number} Indent
175 */
176 function getNodeIndent(node, byLastLine, excludeCommas) {
177 let src = getText(context, node, node.loc.start.column + extraColumnStart);
178 const lines = src.split('\n');
179 if (byLastLine) {
180 src = lines[lines.length - 1];
181 } else {
182 src = lines[0];
183 }
184
185 const skip = excludeCommas ? ',' : '';
186
187 let regExp;
188 if (indentType === 'space') {
189 regExp = new RegExp(`^[ ${skip}]+`);
190 } else {
191 regExp = new RegExp(`^[\t${skip}]+`);
192 }
193
194 const indent = regExp.exec(src);
195 return indent ? indent[0].length : 0;
196 }
197
198 /**
199 * Check if the node is the right member of a logical expression
200 * @param {ASTNode} node The node to check
201 * @return {boolean} true if its the case, false if not
202 */
203 function isRightInLogicalExp(node) {
204 return (
205 node.parent
206 && node.parent.parent
207 && node.parent.parent.type === 'LogicalExpression'
208 && node.parent.parent.right === node.parent
209 && !indentLogicalExpressions
210 );
211 }
212
213 /**
214 * Check if the node is the alternate member of a conditional expression
215 * @param {ASTNode} node The node to check
216 * @return {boolean} true if its the case, false if not
217 */
218 function isAlternateInConditionalExp(node) {
219 return (
220 node.parent
221 && node.parent.parent
222 && node.parent.parent.type === 'ConditionalExpression'
223 && node.parent.parent.alternate === node.parent
224 && getSourceCode(context).getTokenBefore(node).value !== '('
225 );
226 }
227
228 /**
229 * Check if the node is within a DoExpression block but not the first expression (which need to be indented)
230 * @param {ASTNode} node The node to check
231 * @return {boolean} true if its the case, false if not
232 */
233 function isSecondOrSubsequentExpWithinDoExp(node) {
234 /*
235 It returns true when node.parent.parent.parent.parent matches:
236
237 DoExpression({
238 ...,
239 body: BlockStatement({
240 ...,
241 body: [
242 ..., // 1-n times
243 ExpressionStatement({
244 ...,
245 expression: JSXElement({
246 ...,
247 openingElement: JSXOpeningElement() // the node
248 })
249 }),
250 ... // 0-n times
251 ]
252 })
253 })
254
255 except:
256
257 DoExpression({
258 ...,
259 body: BlockStatement({
260 ...,
261 body: [
262 ExpressionStatement({
263 ...,
264 expression: JSXElement({
265 ...,
266 openingElement: JSXOpeningElement() // the node
267 })
268 }),
269 ... // 0-n times
270 ]
271 })
272 })
273 */
274 const isInExpStmt = (
275 node.parent
276 && node.parent.parent
277 && node.parent.parent.type === 'ExpressionStatement'
278 );
279 if (!isInExpStmt) {
280 return false;
281 }
282
283 const expStmt = node.parent.parent;
284 const isInBlockStmtWithinDoExp = (
285 expStmt.parent
286 && expStmt.parent.type === 'BlockStatement'
287 && expStmt.parent.parent
288 && expStmt.parent.parent.type === 'DoExpression'
289 );
290 if (!isInBlockStmtWithinDoExp) {
291 return false;
292 }
293
294 const blockStmt = expStmt.parent;
295 const blockStmtFirstExp = blockStmt.body[0];
296 return !(blockStmtFirstExp === expStmt);
297 }
298
299 /**
300 * Check indent for nodes list
301 * @param {ASTNode} node The node to check
302 * @param {number} indent needed indent
303 * @param {boolean} [excludeCommas] skip comma on start of line
304 */
305 function checkNodesIndent(node, indent, excludeCommas) {
306 const nodeIndent = getNodeIndent(node, false, excludeCommas);
307 const isCorrectRightInLogicalExp = isRightInLogicalExp(node) && (nodeIndent - indent) === indentSize;
308 const isCorrectAlternateInCondExp = isAlternateInConditionalExp(node) && (nodeIndent - indent) === 0;
309 if (
310 nodeIndent !== indent
311 && astUtil.isNodeFirstInLine(context, node)
312 && !isCorrectRightInLogicalExp
313 && !isCorrectAlternateInCondExp
314 ) {
315 report(node, indent, nodeIndent);
316 }
317 }
318
319 /**
320 * Check indent for Literal Node or JSXText Node
321 * @param {ASTNode} node The node to check
322 * @param {number} indent needed indent
323 */
324 function checkLiteralNodeIndent(node, indent) {
325 const value = node.value;
326 const regExp = indentType === 'space' ? /\n( *)[\t ]*\S/g : /\n(\t*)[\t ]*\S/g;
327 const nodeIndentsPerLine = Array.from(
328 matchAll(String(value), regExp),
329 (match) => (match[1] ? match[1].length : 0)
330 );
331 const hasFirstInLineNode = nodeIndentsPerLine.length > 0;
332 if (
333 hasFirstInLineNode
334 && !nodeIndentsPerLine.every((actualIndent) => actualIndent === indent)
335 ) {
336 nodeIndentsPerLine.forEach((nodeIndent) => {
337 report(node, indent, nodeIndent);
338 });
339 }
340 }
341
342 function handleOpeningElement(node) {
343 const sourceCode = getSourceCode(context);
344 let prevToken = sourceCode.getTokenBefore(node);
345 if (!prevToken) {
346 return;
347 }
348 // Use the parent in a list or an array
349 if (prevToken.type === 'JSXText' || ((prevToken.type === 'Punctuator') && prevToken.value === ',')) {
350 prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
351 prevToken = prevToken.type === 'Literal' || prevToken.type === 'JSXText' ? prevToken.parent : prevToken;
352 // Use the first non-punctuator token in a conditional expression
353 } else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
354 do {
355 prevToken = sourceCode.getTokenBefore(prevToken);
356 } while (prevToken.type === 'Punctuator' && prevToken.value !== '/');
357 prevToken = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
358 while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
359 prevToken = prevToken.parent;
360 }
361 }
362 prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
363 const parentElementIndent = getNodeIndent(prevToken);
364 const indent = (
365 prevToken.loc.start.line === node.loc.start.line
366 || isRightInLogicalExp(node)
367 || isAlternateInConditionalExp(node)
368 || isSecondOrSubsequentExpWithinDoExp(node)
369 ) ? 0 : indentSize;
370 checkNodesIndent(node, parentElementIndent + indent);
371 }
372
373 function handleClosingElement(node) {
374 if (!node.parent) {
375 return;
376 }
377 const peerElementIndent = getNodeIndent(node.parent.openingElement || node.parent.openingFragment);
378 checkNodesIndent(node, peerElementIndent);
379 }
380
381 function handleAttribute(node) {
382 if (!checkAttributes || (!node.value || node.value.type !== 'JSXExpressionContainer')) {
383 return;
384 }
385 const nameIndent = getNodeIndent(node.name);
386 const lastToken = getSourceCode(context).getLastToken(node.value);
387 const firstInLine = astUtil.getFirstNodeInLine(context, lastToken);
388 const indent = node.name.loc.start.line === firstInLine.loc.start.line ? 0 : nameIndent;
389 checkNodesIndent(firstInLine, indent);
390 }
391
392 function handleLiteral(node) {
393 if (!node.parent) {
394 return;
395 }
396 if (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXFragment') {
397 return;
398 }
399 const parentNodeIndent = getNodeIndent(node.parent);
400 checkLiteralNodeIndent(node, parentNodeIndent + indentSize);
401 }
402
403 return {
404 JSXOpeningElement: handleOpeningElement,
405 JSXOpeningFragment: handleOpeningElement,
406 JSXClosingElement: handleClosingElement,
407 JSXClosingFragment: handleClosingElement,
408 JSXAttribute: handleAttribute,
409 JSXExpressionContainer(node) {
410 if (!node.parent) {
411 return;
412 }
413 const parentNodeIndent = getNodeIndent(node.parent);
414 checkNodesIndent(node, parentNodeIndent + indentSize);
415 },
416 Literal: handleLiteral,
417 JSXText: handleLiteral,
418
419 ReturnStatement(node) {
420 if (
421 !node.parent
422 || !jsxUtil.isJSX(node.argument)
423 ) {
424 return;
425 }
426
427 let fn = node.parent;
428 while (fn && fn.type !== 'FunctionDeclaration' && fn.type !== 'FunctionExpression') {
429 fn = fn.parent;
430 }
431 if (
432 !fn
433 || !jsxUtil.isReturningJSX(context, node, true)
434 ) {
435 return;
436 }
437
438 const openingIndent = getNodeIndent(node);
439 const closingIndent = getNodeIndent(node, true);
440
441 if (openingIndent !== closingIndent) {
442 report(node, openingIndent, closingIndent);
443 }
444 },
445 };
446 },
447};
Note: See TracBrowser for help on using the repository browser.