[d565449] | 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 |
|
---|
| 33 | const matchAll = require('string.prototype.matchall');
|
---|
| 34 | const repeat = require('string.prototype.repeat');
|
---|
| 35 |
|
---|
| 36 | const astUtil = require('../util/ast');
|
---|
| 37 | const docsUrl = require('../util/docsUrl');
|
---|
| 38 | const reportC = require('../util/report');
|
---|
| 39 | const jsxUtil = require('../util/jsx');
|
---|
| 40 | const eslintUtil = require('../util/eslint');
|
---|
| 41 |
|
---|
| 42 | const getSourceCode = eslintUtil.getSourceCode;
|
---|
| 43 | const getText = eslintUtil.getText;
|
---|
| 44 |
|
---|
| 45 | // ------------------------------------------------------------------------------
|
---|
| 46 | // Rule Definition
|
---|
| 47 | // ------------------------------------------------------------------------------
|
---|
| 48 |
|
---|
| 49 | const messages = {
|
---|
| 50 | wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
|
---|
| 51 | };
|
---|
| 52 |
|
---|
| 53 | module.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 | };
|
---|