Skip to content

Commit 1e7fbe5

Browse files
author
Kent C. Dodds
committed
[Fix] jsx-indent with tabs (fixes jsx-eslint#1057)
1 parent c97dd0f commit 1e7fbe5

File tree

2 files changed

+183
-86
lines changed

2 files changed

+183
-86
lines changed

lib/rules/jsx-indent.js

+133-60
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,16 @@ module.exports = {
7373

7474
/**
7575
* Responsible for fixing the indentation issue fix
76-
* @param {ASTNode} node Node violating the indent rule
76+
* @param {Boolean} rangeToReplace is used to specify the range
77+
* to replace with the correct indentation.
7778
* @param {Number} needed Expected indentation character count
7879
* @returns {Function} function to be executed by the fixer
7980
* @private
8081
*/
81-
function getFixerFunction(node, needed) {
82+
function getFixerFunction(rangeToReplace, needed) {
8283
return function(fixer) {
8384
var indent = Array(needed + 1).join(indentChar);
84-
return fixer.replaceTextRange(
85-
[node.start - node.loc.start.column, node.start],
86-
indent
87-
);
85+
return fixer.replaceTextRange(rangeToReplace, indent);
8886
};
8987
}
9088

@@ -93,46 +91,38 @@ module.exports = {
9391
* @param {ASTNode} node Node violating the indent rule
9492
* @param {Number} needed Expected indentation character count
9593
* @param {Number} gotten Indentation character count in the actual node/code
96-
* @param {Object} loc Error line and column location
94+
* @param {Array} rangeToReplace is used in the fixer.
95+
* Defaults to the indent of the start of the node
96+
* @param {Object} loc Error line and column location (defaults to node.loc
9797
*/
98-
function report(node, needed, gotten, loc) {
98+
function report(node, needed, gotten, rangeToReplace, loc) {
9999
var msgContext = {
100100
needed: needed,
101101
type: indentType,
102102
characters: needed === 1 ? 'character' : 'characters',
103103
gotten: gotten
104104
};
105+
rangeToReplace = rangeToReplace || [node.start - node.loc.start.column, node.start];
105106

106-
if (loc) {
107-
context.report({
108-
node: node,
109-
loc: loc,
110-
message: MESSAGE,
111-
data: msgContext,
112-
fix: getFixerFunction(node, needed)
113-
});
114-
} else {
115-
context.report({
116-
node: node,
117-
message: MESSAGE,
118-
data: msgContext,
119-
fix: getFixerFunction(node, needed)
120-
});
121-
}
107+
context.report({
108+
node: node,
109+
loc: loc || node.loc,
110+
message: MESSAGE,
111+
data: msgContext,
112+
fix: getFixerFunction(rangeToReplace, needed)
113+
});
122114
}
123115

124116
/**
125-
* Get node indent
126-
* @param {ASTNode} node Node to examine
127-
* @param {Boolean} byLastLine get indent of node's last line
128-
* @param {Boolean} excludeCommas skip comma on start of line
129-
* @return {Number} Indent
117+
* Get the indentation (of the proper indentType) that exists in the source
118+
* @param {String} src the source string
119+
* @param {Boolean} byLastLine whether the line checked should be the last
120+
* Defaults to the first line
121+
* @param {Boolean} excludeCommas whether to skip commas in the check
122+
* Defaults to false
123+
* @return {Number} the indentation of the indentType that exists on the line
130124
*/
131-
function getNodeIndent(node, byLastLine, excludeCommas) {
132-
byLastLine = byLastLine || false;
133-
excludeCommas = excludeCommas || false;
134-
135-
var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
125+
function getIndentFromString(src, byLastLine, excludeCommas) {
136126
var lines = src.split('\n');
137127
if (byLastLine) {
138128
src = lines[lines.length - 1];
@@ -154,7 +144,24 @@ module.exports = {
154144
}
155145

156146
/**
157-
* Checks node is the first in its own start line. By default it looks by start line.
147+
* Get node indent
148+
* @param {ASTNode} node Node to examine
149+
* @param {Boolean} byLastLine get indent of node's last line
150+
* @param {Boolean} excludeCommas skip comma on start of line
151+
* @return {Number} Indent
152+
*/
153+
function getNodeIndent(node, byLastLine, excludeCommas) {
154+
byLastLine = byLastLine || false;
155+
excludeCommas = excludeCommas || false;
156+
157+
var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
158+
159+
return getIndentFromString(src, byLastLine, excludeCommas);
160+
}
161+
162+
/**
163+
* Checks if the node is the first in its own start line. By default it looks by start line.
164+
* One exception is closing tags with preceeding whitespace
158165
* @param {ASTNode} node The node to check
159166
* @return {Boolean} true if its the first in the its start line
160167
*/
@@ -165,8 +172,9 @@ module.exports = {
165172
} while (token.type === 'JSXText' && /^\s*$/.test(token.value));
166173
var startLine = node.loc.start.line;
167174
var endLine = token ? token.loc.end.line : -1;
175+
var whitespaceOnly = token ? /\n\s*$/.test(token.value) : false;
168176

169-
return startLine !== endLine;
177+
return startLine !== endLine || whitespaceOnly;
170178
}
171179

172180
/**
@@ -218,41 +226,78 @@ module.exports = {
218226
}
219227
}
220228

229+
/**
230+
* Checks the end of the tag (>) to determine whether it's on its own line
231+
* If so, it verifies the indentation is correct and reports if it is not
232+
* @param {ASTNode} node The node to check
233+
* @param {Number} startIndent The indentation of the start of the tag
234+
*/
235+
function checkTagEndIndent(node, startIndent) {
236+
var source = sourceCode.getText(node);
237+
var isTagEndOnOwnLine = /\n\s*\/?>$/.exec(source);
238+
if (isTagEndOnOwnLine) {
239+
var endIndent = getIndentFromString(source, true, false);
240+
if (endIndent !== startIndent) {
241+
var rangeToReplace = [node.end - node.loc.end.column, node.end - 1];
242+
report(node, startIndent, endIndent, rangeToReplace);
243+
}
244+
}
245+
}
246+
247+
/**
248+
* Gets what the JSXOpeningElement's indentation should be
249+
* @param {ASTNode} node The JSXOpeningElement
250+
* @return {Number} the number of indentation characters it should have
251+
*/
252+
function getOpeningElementIndent(node) {
253+
var prevToken = sourceCode.getTokenBefore(node);
254+
if (!prevToken) {
255+
return 0;
256+
}
257+
// Use the parent in a list or an array
258+
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
259+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
260+
prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
261+
// Use the first non-punctuator token in a conditional expression
262+
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
263+
do {
264+
prevToken = sourceCode.getTokenBefore(prevToken);
265+
} while (prevToken.type === 'Punctuator');
266+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
267+
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
268+
prevToken = prevToken.parent;
269+
}
270+
}
271+
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
272+
273+
var parentElementIndent = getNodeIndent(prevToken);
274+
if (prevToken.type === 'JSXElement') {
275+
parentElementIndent = getOpeningElementIndent(prevToken.openingElement);
276+
}
277+
278+
var indent = (
279+
prevToken.loc.start.line === node.loc.start.line ||
280+
isRightInLogicalExp(node) ||
281+
isAlternateInConditionalExp(node)
282+
) ? 0 : indentSize;
283+
return parentElementIndent + indent;
284+
}
285+
221286
return {
222287
JSXOpeningElement: function(node) {
223288
var prevToken = sourceCode.getTokenBefore(node);
224289
if (!prevToken) {
225290
return;
226291
}
227-
// Use the parent in a list or an array
228-
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
229-
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
230-
prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
231-
// Use the first non-punctuator token in a conditional expression
232-
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
233-
do {
234-
prevToken = sourceCode.getTokenBefore(prevToken);
235-
} while (prevToken.type === 'Punctuator');
236-
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
237-
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
238-
prevToken = prevToken.parent;
239-
}
240-
}
241-
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
242-
243-
var parentElementIndent = getNodeIndent(prevToken);
244-
var indent = (
245-
prevToken.loc.start.line === node.loc.start.line ||
246-
isRightInLogicalExp(node) ||
247-
isAlternateInConditionalExp(node)
248-
) ? 0 : indentSize;
249-
checkNodesIndent(node, parentElementIndent + indent);
292+
var startIndent = getOpeningElementIndent(node);
293+
checkNodesIndent(node, startIndent);
294+
checkTagEndIndent(node, startIndent);
250295
},
251296
JSXClosingElement: function(node) {
252297
if (!node.parent) {
253298
return;
254299
}
255-
var peerElementIndent = getNodeIndent(node.parent.openingElement);
300+
var peerElementIndent = getOpeningElementIndent(node.parent.openingElement);
256301
checkNodesIndent(node, peerElementIndent);
257302
},
258303
JSXExpressionContainer: function(node) {
@@ -261,6 +306,34 @@ module.exports = {
261306
}
262307
var parentNodeIndent = getNodeIndent(node.parent);
263308
checkNodesIndent(node, parentNodeIndent + indentSize);
309+
},
310+
Literal: function(node) {
311+
if (!node.parent || (node.parent.type !== 'JSXElement' && node.parent.type !== 'JSXExpressionContainer')) {
312+
return;
313+
}
314+
var parentElementIndent = getOpeningElementIndent(node.parent.openingElement);
315+
var expectedIndent = parentElementIndent + indentSize;
316+
var source = sourceCode.getText(node);
317+
var lines = source.split('\n');
318+
var currentIndex = 0;
319+
lines.forEach(function(line, lineNumber) {
320+
if (line.trim()) {
321+
var lineIndent = getIndentFromString(line);
322+
if (lineIndent !== expectedIndent) {
323+
var lineStart = source.indexOf(line, currentIndex);
324+
var lineIndentStart = line.search(/\S/);
325+
var lineIndentEnd = lineStart + lineIndentStart;
326+
var rangeToReplace = [node.start + lineStart, node.start + lineIndentEnd];
327+
var locLine = lineNumber + node.loc.start.line;
328+
var loc = {
329+
start: {line: locLine, column: lineIndentStart},
330+
end: {line: locLine, column: lineIndentEnd}
331+
};
332+
report(node, expectedIndent, lineIndent, rangeToReplace, loc);
333+
}
334+
}
335+
currentIndex += line.length;
336+
});
264337
}
265338
};
266339

tests/lib/rules/jsx-indent.js

+50-26
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ ruleTester.run('jsx-indent', rule, {
5151
].join('\n'),
5252
options: [0],
5353
parserOptions: parserOptions
54-
}, {
55-
code: [
56-
' <App>',
57-
'<Foo />',
58-
' </App>'
59-
].join('\n'),
60-
options: [-2],
61-
parserOptions: parserOptions
54+
// }, {
55+
// should we put effort in making this work?
56+
// who in their right mind would do this?
57+
// code: [
58+
// ' <App>',
59+
// '<Foo />',
60+
// ' </App>'
61+
// ].join('\n'),
62+
// options: [-2],
63+
// parserOptions: parserOptions
6264
}, {
6365
code: [
6466
'<App>',
@@ -211,17 +213,6 @@ ruleTester.run('jsx-indent', rule, {
211213
'</div>'
212214
].join('\n'),
213215
parserOptions: parserOptions
214-
}, {
215-
// Literals indentation is not touched
216-
code: [
217-
'<div>',
218-
'bar <div>',
219-
' bar',
220-
' bar {foo}',
221-
'bar </div>',
222-
'</div>'
223-
].join('\n'),
224-
parserOptions: parserOptions
225216
}, {
226217
// Multiline ternary
227218
// (colon at the end of the first expression)
@@ -459,6 +450,39 @@ ruleTester.run('jsx-indent', rule, {
459450
options: ['tab'],
460451
parserOptions: parserOptions,
461452
errors: [{message: 'Expected indentation of 1 tab character but found 0.'}]
453+
}, {
454+
code: [
455+
'function MyComponent(props) {',
456+
'\treturn (',
457+
' <div',
458+
'\t\t\tclassName="foo-bar"',
459+
'\t\t\tid="thing"',
460+
' >',
461+
' Hello world!',
462+
' </div>',
463+
'\t)',
464+
'}'
465+
].join('\n'),
466+
output: [
467+
'function MyComponent(props) {',
468+
'\treturn (',
469+
'\t\t<div',
470+
'\t\t\tclassName="foo-bar"',
471+
'\t\t\tid="thing"',
472+
'\t\t>',
473+
'\t\t\tHello world!',
474+
'\t\t</div>',
475+
'\t)',
476+
'}'
477+
].join('\n'),
478+
options: ['tab'],
479+
parserOptions: parserOptions,
480+
errors: [
481+
{message: 'Expected indentation of 2 tab characters but found 0.'},
482+
{message: 'Expected indentation of 2 tab characters but found 0.'},
483+
{message: 'Expected indentation of 3 tab characters but found 0.'},
484+
{message: 'Expected indentation of 2 tab characters but found 0.'}
485+
]
462486
}, {
463487
code: [
464488
'function App() {',
@@ -505,22 +529,22 @@ ruleTester.run('jsx-indent', rule, {
505529
' );',
506530
'}'
507531
].join('\n'),
508-
// The detection logic only thinks <App> is indented wrong, not the other
509-
// two lines following. I *think* because it incorrectly uses <App>'s indention
510-
// as the baseline for the next two, instead of the realizing the entire three
511-
// lines are wrong together. See #608
512-
/* output: [
532+
output: [
513533
'function App() {',
514534
' return (',
515535
' <App>',
516536
' <Foo />',
517537
' </App>',
518538
' );',
519539
'}'
520-
].join('\n'), */
540+
].join('\n'),
521541
options: [2],
522542
parserOptions: parserOptions,
523-
errors: [{message: 'Expected indentation of 4 space characters but found 0.'}]
543+
errors: [
544+
{message: 'Expected indentation of 4 space characters but found 0.'},
545+
{message: 'Expected indentation of 6 space characters but found 2.'},
546+
{message: 'Expected indentation of 4 space characters but found 0.'}
547+
]
524548
}, {
525549
code: [
526550
'<App>',

0 commit comments

Comments
 (0)