@@ -46,6 +46,14 @@ module.exports = {
46
46
} , {
47
47
type : 'integer'
48
48
} ]
49
+ } , {
50
+ type : 'object' ,
51
+ properties : {
52
+ indentLogicalExpressions : {
53
+ type : 'boolean'
54
+ }
55
+ } ,
56
+ additionalProperties : false
49
57
} ]
50
58
} ,
51
59
@@ -56,6 +64,7 @@ module.exports = {
56
64
var extraColumnStart = 0 ;
57
65
var indentType = 'space' ;
58
66
var indentSize = 4 ;
67
+ var indentLogicalExpressions = false ;
59
68
60
69
var sourceCode = context . getSourceCode ( ) ;
61
70
@@ -67,24 +76,25 @@ module.exports = {
67
76
indentSize = context . options [ 0 ] ;
68
77
indentType = 'space' ;
69
78
}
79
+ if ( context . options [ 1 ] ) {
80
+ indentLogicalExpressions = context . options [ 1 ] . indentLogicalExpressions || false ;
81
+ }
70
82
}
71
83
72
84
var indentChar = indentType === 'space' ? ' ' : '\t' ;
73
85
74
86
/**
75
87
* 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.
77
90
* @param {Number } needed Expected indentation character count
78
91
* @returns {Function } function to be executed by the fixer
79
92
* @private
80
93
*/
81
- function getFixerFunction ( node , needed ) {
94
+ function getFixerFunction ( rangeToReplace , needed ) {
82
95
return function ( fixer ) {
83
96
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 ) ;
88
98
} ;
89
99
}
90
100
@@ -93,46 +103,38 @@ module.exports = {
93
103
* @param {ASTNode } node Node violating the indent rule
94
104
* @param {Number } needed Expected indentation character count
95
105
* @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
97
109
*/
98
- function report ( node , needed , gotten , loc ) {
110
+ function report ( node , needed , gotten , rangeToReplace , loc ) {
99
111
var msgContext = {
100
112
needed : needed ,
101
113
type : indentType ,
102
114
characters : needed === 1 ? 'character' : 'characters' ,
103
115
gotten : gotten
104
116
} ;
117
+ rangeToReplace = rangeToReplace || [ node . start - node . loc . start . column , node . start ] ;
105
118
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
+ } ) ;
122
126
}
123
127
124
128
/**
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
130
136
*/
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 ) {
136
138
var lines = src . split ( '\n' ) ;
137
139
if ( byLastLine ) {
138
140
src = lines [ lines . length - 1 ] ;
@@ -154,7 +156,24 @@ module.exports = {
154
156
}
155
157
156
158
/**
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
158
177
* @param {ASTNode } node The node to check
159
178
* @return {Boolean } true if its the first in the its start line
160
179
*/
@@ -165,8 +184,9 @@ module.exports = {
165
184
} while ( token . type === 'JSXText' && / ^ \s * $ / . test ( token . value ) ) ;
166
185
var startLine = node . loc . start . line ;
167
186
var endLine = token ? token . loc . end . line : - 1 ;
187
+ var whitespaceOnly = token ? / \n \s * $ / . test ( token . value ) : false ;
168
188
169
- return startLine !== endLine ;
189
+ return startLine !== endLine || whitespaceOnly ;
170
190
}
171
191
172
192
/**
@@ -218,41 +238,82 @@ module.exports = {
218
238
}
219
239
}
220
240
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
+
221
302
return {
222
303
JSXOpeningElement : function ( node ) {
223
304
var prevToken = sourceCode . getTokenBefore ( node ) ;
224
305
if ( ! prevToken ) {
225
306
return ;
226
307
}
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 ) ;
250
311
} ,
251
312
JSXClosingElement : function ( node ) {
252
313
if ( ! node . parent ) {
253
314
return ;
254
315
}
255
- var peerElementIndent = getNodeIndent ( node . parent . openingElement ) ;
316
+ var peerElementIndent = getOpeningElementIndent ( node . parent . openingElement ) ;
256
317
checkNodesIndent ( node , peerElementIndent ) ;
257
318
} ,
258
319
JSXExpressionContainer : function ( node ) {
@@ -261,6 +322,34 @@ module.exports = {
261
322
}
262
323
var parentNodeIndent = getNodeIndent ( node . parent ) ;
263
324
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
+ } ) ;
264
353
}
265
354
} ;
266
355
0 commit comments