Skip to content

Commit 6e5f688

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

File tree

3 files changed

+264
-86
lines changed

3 files changed

+264
-86
lines changed

docs/rules/jsx-indent.md

+62
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,68 @@ The following patterns are not warnings:
7777
</App>
7878
```
7979

80+
#### indentLogicalExpressions
81+
82+
```js
83+
...
84+
"react/jsx-indent": [<enabled>, 'tab'|<number>, {indentLogicalExpressions: true}]
85+
...
86+
```
87+
88+
By default this is set to false. When enabled, an additional indentation is required when the JSX is the right of a LogicalExpression
89+
90+
The following patterns are considered warnings:
91+
92+
```jsx
93+
// 2 spaces indentation with indentLogicalExpressions as false
94+
// [2, 2, {indentLogicalExpressions: false}]
95+
<App>
96+
{
97+
condition &&
98+
<Container>
99+
<Child></Child>
100+
</Container>
101+
}
102+
</App>
103+
104+
// 2 spaces indentation with indentLogicalExpressions as true
105+
// [2, 2, {indentLogicalExpressions: true}]
106+
<App>
107+
{
108+
condition &&
109+
<Container>
110+
<Child></Child>
111+
</Container>
112+
}
113+
</App>
114+
```
115+
116+
The following patterns are not warnings:
117+
118+
```jsx
119+
// 2 spaces indentation with indentLogicalExpressions as true
120+
// [2, 2, {indentLogicalExpressions: true}]
121+
<App>
122+
{
123+
condition &&
124+
<Container>
125+
<Child></Child>
126+
</Container>
127+
}
128+
</App>
129+
130+
// 2 spaces indentation with indentLogicalExpressions as false
131+
// [2, 2, {indentLogicalExpressions: false}]
132+
<App>
133+
{
134+
condition &&
135+
<Container>
136+
<Child></Child>
137+
</Container>
138+
}
139+
</App>
140+
```
141+
80142
## When not to use
81143

82144
If you are not using JSX then you can disable this rule.

lib/rules/jsx-indent.js

+149-60
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ module.exports = {
4646
}, {
4747
type: 'integer'
4848
}]
49+
}, {
50+
type: 'object',
51+
properties: {
52+
indentLogicalExpressions: {
53+
type: 'boolean'
54+
}
55+
},
56+
additionalProperties: false
4957
}]
5058
},
5159

@@ -56,6 +64,7 @@ module.exports = {
5664
var extraColumnStart = 0;
5765
var indentType = 'space';
5866
var indentSize = 4;
67+
var indentLogicalExpressions = false;
5968

6069
var sourceCode = context.getSourceCode();
6170

@@ -67,24 +76,25 @@ module.exports = {
6776
indentSize = context.options[0];
6877
indentType = 'space';
6978
}
79+
if (context.options[1]) {
80+
indentLogicalExpressions = context.options[1].indentLogicalExpressions || false;
81+
}
7082
}
7183

7284
var indentChar = indentType === 'space' ? ' ' : '\t';
7385

7486
/**
7587
* Responsible for fixing the indentation issue fix
76-
* @param {ASTNode} node Node violating the indent rule
88+
* @param {Boolean} rangeToReplace is used to specify the range
89+
* to replace with the correct indentation.
7790
* @param {Number} needed Expected indentation character count
7891
* @returns {Function} function to be executed by the fixer
7992
* @private
8093
*/
81-
function getFixerFunction(node, needed) {
94+
function getFixerFunction(rangeToReplace, needed) {
8295
return function(fixer) {
8396
var indent = Array(needed + 1).join(indentChar);
84-
return fixer.replaceTextRange(
85-
[node.start - node.loc.start.column, node.start],
86-
indent
87-
);
97+
return fixer.replaceTextRange(rangeToReplace, indent);
8898
};
8999
}
90100

@@ -93,46 +103,38 @@ module.exports = {
93103
* @param {ASTNode} node Node violating the indent rule
94104
* @param {Number} needed Expected indentation character count
95105
* @param {Number} gotten Indentation character count in the actual node/code
96-
* @param {Object} loc Error line and column location
106+
* @param {Array} rangeToReplace is used in the fixer.
107+
* Defaults to the indent of the start of the node
108+
* @param {Object} loc Error line and column location (defaults to node.loc
97109
*/
98-
function report(node, needed, gotten, loc) {
110+
function report(node, needed, gotten, rangeToReplace, loc) {
99111
var msgContext = {
100112
needed: needed,
101113
type: indentType,
102114
characters: needed === 1 ? 'character' : 'characters',
103115
gotten: gotten
104116
};
117+
rangeToReplace = rangeToReplace || [node.start - node.loc.start.column, node.start];
105118

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-
}
119+
context.report({
120+
node: node,
121+
loc: loc || node.loc,
122+
message: MESSAGE,
123+
data: msgContext,
124+
fix: getFixerFunction(rangeToReplace, needed)
125+
});
122126
}
123127

124128
/**
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
129+
* Get the indentation (of the proper indentType) that exists in the source
130+
* @param {String} src the source string
131+
* @param {Boolean} byLastLine whether the line checked should be the last
132+
* Defaults to the first line
133+
* @param {Boolean} excludeCommas whether to skip commas in the check
134+
* Defaults to false
135+
* @return {Number} the indentation of the indentType that exists on the line
130136
*/
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);
137+
function getIndentFromString(src, byLastLine, excludeCommas) {
136138
var lines = src.split('\n');
137139
if (byLastLine) {
138140
src = lines[lines.length - 1];
@@ -154,7 +156,24 @@ module.exports = {
154156
}
155157

156158
/**
157-
* Checks node is the first in its own start line. By default it looks by start line.
159+
* Get node indent
160+
* @param {ASTNode} node Node to examine
161+
* @param {Boolean} byLastLine get indent of node's last line
162+
* @param {Boolean} excludeCommas skip comma on start of line
163+
* @return {Number} Indent
164+
*/
165+
function getNodeIndent(node, byLastLine, excludeCommas) {
166+
byLastLine = byLastLine || false;
167+
excludeCommas = excludeCommas || false;
168+
169+
var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart);
170+
171+
return getIndentFromString(src, byLastLine, excludeCommas);
172+
}
173+
174+
/**
175+
* Checks if the node is the first in its own start line. By default it looks by start line.
176+
* One exception is closing tags with preceeding whitespace
158177
* @param {ASTNode} node The node to check
159178
* @return {Boolean} true if its the first in the its start line
160179
*/
@@ -165,8 +184,9 @@ module.exports = {
165184
} while (token.type === 'JSXText' && /^\s*$/.test(token.value));
166185
var startLine = node.loc.start.line;
167186
var endLine = token ? token.loc.end.line : -1;
187+
var whitespaceOnly = token ? /\n\s*$/.test(token.value) : false;
168188

169-
return startLine !== endLine;
189+
return startLine !== endLine || whitespaceOnly;
170190
}
171191

172192
/**
@@ -218,41 +238,82 @@ module.exports = {
218238
}
219239
}
220240

241+
/**
242+
* Checks the end of the tag (>) to determine whether it's on its own line
243+
* If so, it verifies the indentation is correct and reports if it is not
244+
* @param {ASTNode} node The node to check
245+
* @param {Number} startIndent The indentation of the start of the tag
246+
*/
247+
function checkTagEndIndent(node, startIndent) {
248+
var source = sourceCode.getText(node);
249+
var isTagEndOnOwnLine = /\n\s*\/?>$/.exec(source);
250+
if (isTagEndOnOwnLine) {
251+
var endIndent = getIndentFromString(source, true, false);
252+
if (endIndent !== startIndent) {
253+
var rangeToReplace = [node.end - node.loc.end.column, node.end - 1];
254+
report(node, startIndent, endIndent, rangeToReplace);
255+
}
256+
}
257+
}
258+
259+
/**
260+
* Gets what the JSXOpeningElement's indentation should be
261+
* @param {ASTNode} node The JSXOpeningElement
262+
* @return {Number} the number of indentation characters it should have
263+
*/
264+
function getOpeningElementIndent(node) {
265+
var prevToken = sourceCode.getTokenBefore(node);
266+
if (!prevToken) {
267+
return 0;
268+
}
269+
if (prevToken.type === 'JSXText' || prevToken.type === 'Punctuator' && prevToken.value === ',') {
270+
// Use the parent in a list or an array
271+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
272+
prevToken = prevToken.type === 'Literal' ? prevToken.parent : prevToken;
273+
} else if (prevToken.type === 'Punctuator' && prevToken.value === ':') {
274+
// Use the first non-punctuator token in a conditional expression
275+
do {
276+
prevToken = sourceCode.getTokenBefore(prevToken);
277+
} while (prevToken.type === 'Punctuator');
278+
prevToken = sourceCode.getNodeByRangeIndex(prevToken.start);
279+
while (prevToken.parent && prevToken.parent.type !== 'ConditionalExpression') {
280+
prevToken = prevToken.parent;
281+
}
282+
}
283+
prevToken = prevToken.type === 'JSXExpressionContainer' ? prevToken.expression : prevToken;
284+
285+
var parentElementIndent = getNodeIndent(prevToken);
286+
if (prevToken.type === 'JSXElement') {
287+
parentElementIndent = getOpeningElementIndent(prevToken.openingElement);
288+
}
289+
290+
if (isRightInLogicalExp(node) && indentLogicalExpressions) {
291+
parentElementIndent += indentSize;
292+
}
293+
294+
var indent = (
295+
prevToken.loc.start.line === node.loc.start.line ||
296+
isRightInLogicalExp(node) ||
297+
isAlternateInConditionalExp(node)
298+
) ? 0 : indentSize;
299+
return parentElementIndent + indent;
300+
}
301+
221302
return {
222303
JSXOpeningElement: function(node) {
223304
var prevToken = sourceCode.getTokenBefore(node);
224305
if (!prevToken) {
225306
return;
226307
}
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);
308+
var startIndent = getOpeningElementIndent(node);
309+
checkNodesIndent(node, startIndent);
310+
checkTagEndIndent(node, startIndent);
250311
},
251312
JSXClosingElement: function(node) {
252313
if (!node.parent) {
253314
return;
254315
}
255-
var peerElementIndent = getNodeIndent(node.parent.openingElement);
316+
var peerElementIndent = getOpeningElementIndent(node.parent.openingElement);
256317
checkNodesIndent(node, peerElementIndent);
257318
},
258319
JSXExpressionContainer: function(node) {
@@ -261,6 +322,34 @@ module.exports = {
261322
}
262323
var parentNodeIndent = getNodeIndent(node.parent);
263324
checkNodesIndent(node, parentNodeIndent + indentSize);
325+
},
326+
Literal: function(node) {
327+
if (!node.parent || node.parent.type !== 'JSXElement') {
328+
return;
329+
}
330+
var parentElementIndent = getOpeningElementIndent(node.parent.openingElement);
331+
var expectedIndent = parentElementIndent + indentSize;
332+
var source = sourceCode.getText(node);
333+
var lines = source.split('\n');
334+
var currentIndex = 0;
335+
lines.forEach(function(line, lineNumber) {
336+
if (line.trim()) {
337+
var lineIndent = getIndentFromString(line);
338+
if (lineIndent !== expectedIndent) {
339+
var lineStart = source.indexOf(line, currentIndex);
340+
var lineIndentStart = line.search(/\S/);
341+
var lineIndentEnd = lineStart + lineIndentStart;
342+
var rangeToReplace = [node.start + lineStart, node.start + lineIndentEnd];
343+
var locLine = lineNumber + node.loc.start.line;
344+
var loc = {
345+
start: {line: locLine, column: lineIndentStart},
346+
end: {line: locLine, column: lineIndentEnd}
347+
};
348+
report(node, expectedIndent, lineIndent, rangeToReplace, loc);
349+
}
350+
}
351+
currentIndex += line.length;
352+
});
264353
}
265354
};
266355

0 commit comments

Comments
 (0)