Skip to content

Commit 8b3fc32

Browse files
not-an-aardvarknzakas
authored andcommitted
Update: Make indent report lines with mixed spaces/tabs (fixes #4274) (#7076)
1 parent b39ac2c commit 8b3fc32

File tree

2 files changed

+156
-122
lines changed

2 files changed

+156
-122
lines changed

lib/rules/indent.js

Lines changed: 92 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ module.exports = {
121121
},
122122

123123
create(context) {
124-
125-
const MESSAGE = "Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.";
126124
const DEFAULT_VARIABLE_INDENT = 1;
127125
const DEFAULT_PARAMETER_INDENT = null; // For backwards compatibility, don't check parameter indentation unless specified in the config
128126
const DEFAULT_FUNCTION_BODY_INDENT = 1;
@@ -192,108 +190,85 @@ module.exports = {
192190
}
193191
}
194192

195-
const indentPattern = {
196-
normal: indentType === "space" ? /^ +/ : /^\t+/,
197-
excludeCommas: indentType === "space" ? /^[ ,]+/ : /^[\t,]+/
198-
};
199-
200193
const caseIndentStore = {};
201194

202195
/**
203-
* Reports a given indent violation and properly pluralizes the message
196+
* Creates an error message for a line, given the expected/actual indentation.
197+
* @param {int} expectedAmount The expected amount of indentation characters for this line
198+
* @param {int} actualSpaces The actual number of indentation spaces that were found on this line
199+
* @param {int} actualTabs The actual number of indentation tabs that were found on this line
200+
* @returns {string} An error message for this line
201+
*/
202+
function createErrorMessage(expectedAmount, actualSpaces, actualTabs) {
203+
const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? "" : "s"}`; // e.g. "2 tabs"
204+
const foundSpacesWord = `space${actualSpaces === 1 ? "" : "s"}`; // e.g. "space"
205+
const foundTabsWord = `tab${actualTabs === 1 ? "" : "s"}`; // e.g. "tabs"
206+
let foundStatement;
207+
208+
if (actualSpaces > 0 && actualTabs > 0) {
209+
foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}`; // e.g. "1 space and 2 tabs"
210+
} else if (actualSpaces > 0) {
211+
212+
// Abbreviate the message if the expected indentation is also spaces.
213+
// e.g. 'Expected 4 spaces but found 2' rather than 'Expected 4 spaces but found 2 spaces'
214+
foundStatement = indentType === "space" ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`;
215+
} else if (actualTabs > 0) {
216+
foundStatement = indentType === "tab" ? actualTabs : `${actualTabs} ${foundTabsWord}`;
217+
} else {
218+
foundStatement = "0";
219+
}
220+
221+
return `Expected indentation of ${expectedStatement} but found ${foundStatement}.`;
222+
}
223+
224+
/**
225+
* Reports a given indent violation
204226
* @param {ASTNode} node Node violating the indent rule
205227
* @param {int} needed Expected indentation character count
206-
* @param {int} gotten Indentation character count in the actual node/code
228+
* @param {int} gottenSpaces Indentation space count in the actual node/code
229+
* @param {int} gottenTabs Indentation tab count in the actual node/code
207230
* @param {Object=} loc Error line and column location
208231
* @param {boolean} isLastNodeCheck Is the error for last node check
209232
* @returns {void}
210233
*/
211-
function report(node, needed, gotten, loc, isLastNodeCheck) {
212-
const msgContext = {
213-
needed,
214-
type: indentType,
215-
characters: needed === 1 ? "character" : "characters",
216-
gotten
217-
};
218-
const indentChar = indentType === "space" ? " " : "\t";
219-
220-
/**
221-
* Responsible for fixing the indentation issue fix
222-
* @returns {Function} function to be executed by the fixer
223-
* @private
224-
*/
225-
function getFixerFunction() {
226-
let rangeToFix = [];
227-
228-
if (needed > gotten) {
229-
const spaces = indentChar.repeat(needed - gotten);
230-
231-
if (isLastNodeCheck === true) {
232-
rangeToFix = [
233-
node.range[1] - 1,
234-
node.range[1] - 1
235-
];
236-
} else {
237-
rangeToFix = [
238-
node.range[0],
239-
node.range[0]
240-
];
241-
}
234+
function report(node, needed, gottenSpaces, gottenTabs, loc, isLastNodeCheck) {
242235

243-
return function(fixer) {
244-
return fixer.insertTextBeforeRange(rangeToFix, spaces);
245-
};
246-
} else {
247-
if (isLastNodeCheck === true) {
248-
rangeToFix = [
249-
node.range[1] - (gotten - needed) - 1,
250-
node.range[1] - 1
251-
];
252-
} else {
253-
rangeToFix = [
254-
node.range[0] - (gotten - needed),
255-
node.range[0]
256-
];
257-
}
236+
const desiredIndent = (indentType === "space" ? " " : "\t").repeat(needed);
258237

259-
return function(fixer) {
260-
return fixer.removeRange(rangeToFix);
261-
};
262-
}
263-
}
238+
const textRange = isLastNodeCheck
239+
? [node.range[1] - gottenSpaces - gottenTabs - 1, node.range[1] - 1]
240+
: [node.range[0] - gottenSpaces - gottenTabs, node.range[0]];
264241

265-
if (loc) {
266-
context.report({
267-
node,
268-
loc,
269-
message: MESSAGE,
270-
data: msgContext,
271-
fix: getFixerFunction()
272-
});
273-
} else {
274-
context.report({
275-
node,
276-
message: MESSAGE,
277-
data: msgContext,
278-
fix: getFixerFunction()
279-
});
280-
}
242+
context.report({
243+
node,
244+
loc,
245+
message: createErrorMessage(needed, gottenSpaces, gottenTabs),
246+
fix: fixer => fixer.replaceTextRange(textRange, desiredIndent)
247+
});
281248
}
282249

283250
/**
284251
* Get the actual indent of node
285252
* @param {ASTNode|Token} node Node to examine
286253
* @param {boolean} [byLastLine=false] get indent of node's last line
287254
* @param {boolean} [excludeCommas=false] skip comma on start of line
288-
* @returns {int} Indent
255+
* @returns {Object} The node's indent. Contains keys `space` and `tab`, representing the indent of each character. Also
256+
contains keys `goodChar` and `badChar`, where `goodChar` is the amount of the user's desired indentation character, and
257+
`badChar` is the amount of the other indentation character.
289258
*/
290-
function getNodeIndent(node, byLastLine, excludeCommas) {
259+
function getNodeIndent(node, byLastLine) {
291260
const token = byLastLine ? sourceCode.getLastToken(node) : sourceCode.getFirstToken(node);
292-
const src = sourceCode.getText(token, token.loc.start.column);
293-
const regExp = excludeCommas ? indentPattern.excludeCommas : indentPattern.normal;
294-
const indent = regExp.exec(src);
295-
296-
return indent ? indent[0].length : 0;
261+
const srcCharsBeforeNode = sourceCode.getText(token, token.loc.start.column).split("");
262+
const indentChars = srcCharsBeforeNode.slice(0, srcCharsBeforeNode.findIndex(char => char !== " " && char !== "\t"));
263+
const spaces = indentChars.filter(char => char === " ").length;
264+
const tabs = indentChars.filter(char => char === "\t").length;
265+
266+
return {
267+
space: spaces,
268+
tab: tabs,
269+
goodChar: indentType === "space" ? spaces : tabs,
270+
badChar: indentType === "space" ? tabs : spaces
271+
};
297272
}
298273

299274
/**
@@ -313,27 +288,29 @@ module.exports = {
313288
/**
314289
* Check indent for node
315290
* @param {ASTNode} node Node to check
316-
* @param {int} indent needed indent
291+
* @param {int} neededIndent needed indent
317292
* @param {boolean} [excludeCommas=false] skip comma on start of line
318293
* @returns {void}
319294
*/
320-
function checkNodeIndent(node, indent, excludeCommas) {
321-
const nodeIndent = getNodeIndent(node, false, excludeCommas);
295+
function checkNodeIndent(node, neededIndent) {
296+
const actualIndent = getNodeIndent(node, false);
322297

323298
if (
324-
node.type !== "ArrayExpression" && node.type !== "ObjectExpression" &&
325-
nodeIndent !== indent && isNodeFirstInLine(node)
299+
node.type !== "ArrayExpression" &&
300+
node.type !== "ObjectExpression" &&
301+
(actualIndent.goodChar !== neededIndent || actualIndent.badChar !== 0) &&
302+
isNodeFirstInLine(node)
326303
) {
327-
report(node, indent, nodeIndent);
304+
report(node, neededIndent, actualIndent.space, actualIndent.tab);
328305
}
329306

330307
if (node.type === "IfStatement" && node.alternate) {
331308
const elseToken = sourceCode.getTokenBefore(node.alternate);
332309

333-
checkNodeIndent(elseToken, indent, excludeCommas);
310+
checkNodeIndent(elseToken, neededIndent);
334311

335312
if (!isNodeFirstInLine(node.alternate)) {
336-
checkNodeIndent(node.alternate, indent, excludeCommas);
313+
checkNodeIndent(node.alternate, neededIndent);
337314
}
338315
}
339316
}
@@ -345,8 +322,8 @@ module.exports = {
345322
* @param {boolean} [excludeCommas=false] skip comma on start of line
346323
* @returns {void}
347324
*/
348-
function checkNodesIndent(nodes, indent, excludeCommas) {
349-
nodes.forEach(node => checkNodeIndent(node, indent, excludeCommas));
325+
function checkNodesIndent(nodes, indent) {
326+
nodes.forEach(node => checkNodeIndent(node, indent));
350327
}
351328

352329
/**
@@ -359,11 +336,12 @@ module.exports = {
359336
const lastToken = sourceCode.getLastToken(node);
360337
const endIndent = getNodeIndent(lastToken, true);
361338

362-
if (endIndent !== lastLineIndent && isNodeFirstInLine(node, true)) {
339+
if ((endIndent.goodChar !== lastLineIndent || endIndent.badChar !== 0) && isNodeFirstInLine(node, true)) {
363340
report(
364341
node,
365342
lastLineIndent,
366-
endIndent,
343+
endIndent.space,
344+
endIndent.tab,
367345
{ line: lastToken.loc.start.line, column: lastToken.loc.start.column },
368346
true
369347
);
@@ -379,11 +357,12 @@ module.exports = {
379357
function checkFirstNodeLineIndent(node, firstLineIndent) {
380358
const startIndent = getNodeIndent(node, false);
381359

382-
if (startIndent !== firstLineIndent && isNodeFirstInLine(node)) {
360+
if ((startIndent.goodChar !== firstLineIndent || startIndent.badChar !== 0) && isNodeFirstInLine(node)) {
383361
report(
384362
node,
385363
firstLineIndent,
386-
startIndent,
364+
startIndent.space,
365+
startIndent.tab,
387366
{ line: node.loc.start.line, column: node.loc.start.column }
388367
);
389368
}
@@ -526,25 +505,25 @@ module.exports = {
526505
calleeNode.parent.type === "ArrayExpression")) {
527506

528507
// If function is part of array or object, comma can be put at left
529-
indent = getNodeIndent(calleeNode, false, false);
508+
indent = getNodeIndent(calleeNode, false, false).goodChar;
530509
} else {
531510

532511
// If function is standalone, simple calculate indent
533-
indent = getNodeIndent(calleeNode);
512+
indent = getNodeIndent(calleeNode).goodChar;
534513
}
535514

536515
if (calleeNode.parent.type === "CallExpression") {
537516
const calleeParent = calleeNode.parent;
538517

539518
if (calleeNode.type !== "FunctionExpression" && calleeNode.type !== "ArrowFunctionExpression") {
540519
if (calleeParent && calleeParent.loc.start.line < node.loc.start.line) {
541-
indent = getNodeIndent(calleeParent);
520+
indent = getNodeIndent(calleeParent).goodChar;
542521
}
543522
} else {
544523
if (isArgBeforeCalleeNodeMultiline(calleeNode) &&
545524
calleeParent.callee.loc.start.line === calleeParent.callee.loc.end.line &&
546525
!isNodeFirstInLine(calleeNode)) {
547-
indent = getNodeIndent(calleeParent);
526+
indent = getNodeIndent(calleeParent).goodChar;
548527
}
549528
}
550529
}
@@ -644,7 +623,7 @@ module.exports = {
644623
effectiveParent = parent.parent;
645624
}
646625
}
647-
nodeIndent = getNodeIndent(effectiveParent);
626+
nodeIndent = getNodeIndent(effectiveParent).goodChar;
648627
if (parentVarNode && parentVarNode.loc.start.line !== node.loc.start.line) {
649628
if (parent.type !== "VariableDeclarator" || parentVarNode === parentVarNode.parent.declarations[0]) {
650629
if (parent.type === "VariableDeclarator" && parentVarNode.loc.start.line === effectiveParent.loc.start.line) {
@@ -668,7 +647,7 @@ module.exports = {
668647

669648
checkFirstNodeLineIndent(node, nodeIndent);
670649
} else {
671-
nodeIndent = getNodeIndent(node);
650+
nodeIndent = getNodeIndent(node).goodChar;
672651
elementsIndent = nodeIndent + indentSize;
673652
}
674653

@@ -680,8 +659,7 @@ module.exports = {
680659
elementsIndent += indentSize * options.VariableDeclarator[parentVarNode.parent.kind];
681660
}
682661

683-
// Comma can be placed before property name
684-
checkNodesIndent(elements, elementsIndent, true);
662+
checkNodesIndent(elements, elementsIndent);
685663

686664
if (elements.length > 0) {
687665

@@ -737,9 +715,9 @@ module.exports = {
737715
];
738716

739717
if (node.parent && statementsWithProperties.indexOf(node.parent.type) !== -1 && isNodeBodyBlock(node)) {
740-
indent = getNodeIndent(node.parent);
718+
indent = getNodeIndent(node.parent).goodChar;
741719
} else {
742-
indent = getNodeIndent(node);
720+
indent = getNodeIndent(node).goodChar;
743721
}
744722

745723
if (node.type === "IfStatement" && node.consequent.type !== "BlockStatement") {
@@ -785,13 +763,12 @@ module.exports = {
785763
*/
786764
function checkIndentInVariableDeclarations(node) {
787765
const elements = filterOutSameLineVars(node);
788-
const nodeIndent = getNodeIndent(node);
766+
const nodeIndent = getNodeIndent(node).goodChar;
789767
const lastElement = elements[elements.length - 1];
790768

791769
const elementsIndent = nodeIndent + indentSize * options.VariableDeclarator[node.kind];
792770

793-
// Comma can be placed before declaration
794-
checkNodesIndent(elements, elementsIndent, true);
771+
checkNodesIndent(elements, elementsIndent);
795772

796773
// Only check the last line if there is any token after the last item
797774
if (sourceCode.getLastToken(node).loc.end.line <= lastElement.loc.end.line) {
@@ -803,7 +780,7 @@ module.exports = {
803780
if (tokenBeforeLastElement.value === ",") {
804781

805782
// Special case for comma-first syntax where the semicolon is indented
806-
checkLastNodeLineIndent(node, getNodeIndent(tokenBeforeLastElement));
783+
checkLastNodeLineIndent(node, getNodeIndent(tokenBeforeLastElement).goodChar);
807784
} else {
808785
checkLastNodeLineIndent(node, elementsIndent - indentSize);
809786
}
@@ -835,7 +812,7 @@ module.exports = {
835812
return caseIndentStore[switchNode.loc.start.line];
836813
} else {
837814
if (typeof switchIndent === "undefined") {
838-
switchIndent = getNodeIndent(switchNode);
815+
switchIndent = getNodeIndent(switchNode).goodChar;
839816
}
840817

841818
if (switchNode.cases.length > 0 && options.SwitchCase === 0) {
@@ -854,7 +831,7 @@ module.exports = {
854831
if (node.body.length > 0) {
855832

856833
// Root nodes should have no indent
857-
checkNodesIndent(node.body, getNodeIndent(node));
834+
checkNodesIndent(node.body, getNodeIndent(node).goodChar);
858835
}
859836
},
860837

@@ -913,7 +890,7 @@ module.exports = {
913890
return;
914891
}
915892

916-
const propertyIndent = getNodeIndent(node) + indentSize * options.MemberExpression;
893+
const propertyIndent = getNodeIndent(node).goodChar + indentSize * options.MemberExpression;
917894

918895
const checkNodes = [node.property];
919896

@@ -929,7 +906,7 @@ module.exports = {
929906
SwitchStatement(node) {
930907

931908
// Switch is not a 'BlockStatement'
932-
const switchIndent = getNodeIndent(node);
909+
const switchIndent = getNodeIndent(node).goodChar;
933910
const caseIndent = expectedCaseIndent(node, switchIndent);
934911

935912
checkNodesIndent(node.cases, caseIndent);

0 commit comments

Comments
 (0)