Skip to content

Commit d95aca8

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

File tree

2 files changed

+121
-60
lines changed

2 files changed

+121
-60
lines changed

lib/rules/jsx-indent.js

+90-59
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,18 @@ module.exports = {
7575
* Responsible for fixing the indentation issue fix
7676
* @param {ASTNode} node Node violating the indent rule
7777
* @param {Number} needed Expected indentation character count
78+
* @param {Boolean} isEndOfTag is used to indent the right thing
7879
* @returns {Function} function to be executed by the fixer
7980
* @private
8081
*/
81-
function getFixerFunction(node, needed) {
82+
function getFixerFunction(node, needed, isEndOfTag) {
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+
var rangeToReplace = [node.start - node.loc.start.column, node.start];
86+
if (isEndOfTag) {
87+
rangeToReplace = [node.end - node.loc.end.column, node.end - 1];
88+
}
89+
return fixer.replaceTextRange(rangeToReplace, indent);
8890
};
8991
}
9092

@@ -93,46 +95,32 @@ module.exports = {
9395
* @param {ASTNode} node Node violating the indent rule
9496
* @param {Number} needed Expected indentation character count
9597
* @param {Number} gotten Indentation character count in the actual node/code
96-
* @param {Object} loc Error line and column location
98+
* @param {Boolean} isEndOfTag which is used in the fixer function
9799
*/
98-
function report(node, needed, gotten, loc) {
100+
function report(node, needed, gotten, isEndOfTag) {
99101
var msgContext = {
100102
needed: needed,
101103
type: indentType,
102104
characters: needed === 1 ? 'character' : 'characters',
103105
gotten: gotten
104106
};
105107

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-
}
108+
context.report({
109+
node: node,
110+
message: MESSAGE,
111+
data: msgContext,
112+
fix: getFixerFunction(node, needed, isEndOfTag)
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} the source string
119+
* @param {Boolean} whether the line checked should be the last (defaults to the first line)
120+
* @param {Boolean} whether to skip commas in the check (defaults to false)
121+
* @return {Number} the indentation of the indentType that exists on the line
130122
*/
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);
123+
function getIndentFromString(src, byLastLine, excludeCommas) {
136124
var lines = src.split('\n');
137125
if (byLastLine) {
138126
src = lines[lines.length - 1];
@@ -154,7 +142,24 @@ module.exports = {
154142
}
155143

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

169-
return startLine !== endLine;
175+
return startLine !== endLine || whitespaceOnly;
170176
}
171177

172178
/**
@@ -218,41 +224,66 @@ module.exports = {
218224
}
219225
}
220226

227+
/**
228+
* Checks the end of the tag (>) to determine whether it's on its own line
229+
* If so, it verifies the indentation is correct and reports if it is not
230+
* @param {[type]} node [description]
231+
* @param {[type]} startIndent [description]
232+
* @return {[type]} [description]
233+
*/
234+
function checkTagEndIndent(node, startIndent) {
235+
var source = sourceCode.getText(node);
236+
var isTagEndOnOwnLine = /\n\s*\/?>$/.exec(source);
237+
if (isTagEndOnOwnLine) {
238+
var endIndent = getIndentFromString(source, true, false);
239+
if (endIndent !== startIndent) {
240+
report(node, startIndent, endIndent, true);
241+
}
242+
}
243+
}
244+
245+
function getOpeningElementIndent(node) {
246+
var prevToken = sourceCode.getTokenBefore(node);
247+
// Use the parent in a list or an array
248+
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
249+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
250+
prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
251+
// Use the first non-punctuator token in a conditional expression
252+
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
253+
do {
254+
prevToken = sourceCode.getTokenBefore(prevToken);
255+
} while (prevToken.type === 'Punctuator');
256+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
257+
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
258+
prevToken = prevToken.parent;
259+
}
260+
}
261+
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
262+
263+
var parentElementIndent = getNodeIndent(prevToken);
264+
var indent = (
265+
prevToken.loc.start.line === node.loc.start.line ||
266+
isRightInLogicalExp(node) ||
267+
isAlternateInConditionalExp(node)
268+
) ? 0 : indentSize;
269+
return parentElementIndent + indent;
270+
}
271+
221272
return {
222273
JSXOpeningElement: function(node) {
223274
var prevToken = sourceCode.getTokenBefore(node);
224275
if (!prevToken) {
225276
return;
226277
}
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);
278+
var startIndent = getOpeningElementIndent(node);
279+
checkNodesIndent(node, startIndent);
280+
checkTagEndIndent(node, startIndent);
250281
},
251282
JSXClosingElement: function(node) {
252283
if (!node.parent) {
253284
return;
254285
}
255-
var peerElementIndent = getNodeIndent(node.parent.openingElement);
286+
var peerElementIndent = getOpeningElementIndent(node.parent.openingElement);
256287
checkNodesIndent(node, peerElementIndent);
257288
},
258289
JSXExpressionContainer: function(node) {

tests/lib/rules/jsx-indent.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,34 @@ ruleTester.run('jsx-indent', rule, {
459459
options: ['tab'],
460460
parserOptions: parserOptions,
461461
errors: [{message: 'Expected indentation of 1 tab character but found 0.'}]
462+
}, {
463+
code: [
464+
'function MyComponent(props) {',
465+
'\treturn (',
466+
' <div',
467+
'\t\t\tclassName="foo-bar"',
468+
'\t\t\tid="thing"',
469+
' >',
470+
' Hello world!',
471+
' </div>',
472+
'\t)',
473+
'}'
474+
].join('\n'),
475+
output: [
476+
'function MyComponent(props) {',
477+
'\treturn (',
478+
'\t\t<div',
479+
'\t\t\tclassName="foo-bar"',
480+
'\t\t\tid="thing"',
481+
'\t\t>',
482+
'\t\t\tHello world!',
483+
'\t\t</div>',
484+
'\t)',
485+
'}'
486+
].join('\n'),
487+
options: ['tab'],
488+
parserOptions: parserOptions,
489+
errors: [{message: 'Expected indentation of 2 tab characters but found 0.'}]
462490
}, {
463491
code: [
464492
'function App() {',
@@ -730,7 +758,9 @@ ruleTester.run('jsx-indent', rule, {
730758
].join('\n'),
731759
parserOptions: parserOptions,
732760
errors: [
733-
{message: 'Expected indentation of 4 space characters but found 0.'}
761+
{message: 'Expected indentation of 2 tab characters but found 0.'},
762+
{message: 'Expected indentation of 2 tab characters but found 0.'},
763+
{message: 'Expected indentation of 2 tab characters but found 0.'}
734764
]
735765
}, {
736766
// Multiline ternary

0 commit comments

Comments
 (0)