@@ -20,6 +20,7 @@ type CompatibleWithESLintOptions =
20
20
caseSensitive ?: boolean ;
21
21
natural ?: boolean ;
22
22
minKeys ?: number ;
23
+ allowLineSeparatedGroups ?: boolean ;
23
24
}
24
25
] ;
25
26
type PatternOption = {
@@ -35,6 +36,7 @@ type PatternOption = {
35
36
}
36
37
) [ ] ;
37
38
minKeys ?: number ;
39
+ allowLineSeparatedGroups ?: boolean ;
38
40
} ;
39
41
type OrderObject = {
40
42
type ?: OrderTypeOption ;
@@ -45,6 +47,7 @@ type ParsedOption = {
45
47
isTargetObject : ( node : JSONObjectData ) => boolean ;
46
48
ignore : ( data : JSONPropertyData ) => boolean ;
47
49
isValidOrder : Validator ;
50
+ allowLineSeparatedGroups : boolean ;
48
51
orderText : string ;
49
52
} ;
50
53
type Validator = ( a : JSONPropertyData , b : JSONPropertyData ) => boolean ;
@@ -86,6 +89,11 @@ class JSONPropertyData {
86
89
public get name ( ) {
87
90
return ( this . cachedName ??= getPropertyName ( this . node ) ) ;
88
91
}
92
+
93
+ public getPrev ( ) : JSONPropertyData | null {
94
+ const prevIndex = this . index - 1 ;
95
+ return prevIndex >= 0 ? this . object . properties [ prevIndex ] : null ;
96
+ }
89
97
}
90
98
class JSONObjectData {
91
99
public readonly node : AST . JSONObjectExpression ;
@@ -101,6 +109,36 @@ class JSONObjectData {
101
109
( e , index ) => new JSONPropertyData ( this , e , index )
102
110
) ) ;
103
111
}
112
+
113
+ public getPath ( ) : string {
114
+ let path = "" ;
115
+ let curr : AST . JSONExpression = this . node ;
116
+ let p : AST . JSONNode | null = curr . parent ;
117
+ while ( p ) {
118
+ if ( p . type === "JSONProperty" ) {
119
+ const name = getPropertyName ( p ) ;
120
+ if ( / ^ [ $ _ a - z ] [ \w $ ] * $ / iu. test ( name ) ) {
121
+ path = `.${ name } ${ path } ` ;
122
+ } else {
123
+ path = `[${ JSON . stringify ( name ) } ]${ path } ` ;
124
+ }
125
+ curr = p . parent ;
126
+ } else if ( p . type === "JSONArrayExpression" ) {
127
+ const index = p . elements . indexOf ( curr ) ;
128
+ path = `[${ index } ]${ path } ` ;
129
+ curr = p ;
130
+ } else if ( p . type === "JSONExpressionStatement" ) {
131
+ break ;
132
+ } else {
133
+ curr = p ;
134
+ }
135
+ p = curr . parent ;
136
+ }
137
+ if ( path . startsWith ( "." ) ) {
138
+ path = path . slice ( 1 ) ;
139
+ }
140
+ return path ;
141
+ }
104
142
}
105
143
106
144
/**
@@ -153,6 +191,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
153
191
const insensitive = obj . caseSensitive === false ;
154
192
const natural = Boolean ( obj . natural ) ;
155
193
const minKeys : number = obj . minKeys ?? 2 ;
194
+ const allowLineSeparatedGroups = obj . allowLineSeparatedGroups || false ;
156
195
return [
157
196
{
158
197
isTargetObject : ( node ) => node . properties . length >= minKeys ,
@@ -161,6 +200,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
161
200
orderText : `${ natural ? "natural " : "" } ${
162
201
insensitive ? "insensitive " : ""
163
202
} ${ type } ending`,
203
+ allowLineSeparatedGroups,
164
204
} ,
165
205
] ;
166
206
}
@@ -170,6 +210,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
170
210
const pathPattern = new RegExp ( opt . pathPattern ) ;
171
211
const hasProperties = opt . hasProperties ?? [ ] ;
172
212
const minKeys : number = opt . minKeys ?? 2 ;
213
+ const allowLineSeparatedGroups = opt . allowLineSeparatedGroups || false ;
173
214
if ( ! Array . isArray ( order ) ) {
174
215
const type : OrderTypeOption = order . type ?? "asc" ;
175
216
const insensitive = order . caseSensitive === false ;
@@ -182,6 +223,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
182
223
orderText : `${ natural ? "natural " : "" } ${
183
224
insensitive ? "insensitive " : ""
184
225
} ${ type } ending`,
226
+ allowLineSeparatedGroups,
185
227
} ;
186
228
}
187
229
const parsedOrder : {
@@ -227,6 +269,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
227
269
return false ;
228
270
} ,
229
271
orderText : "specified" ,
272
+ allowLineSeparatedGroups,
230
273
} ;
231
274
232
275
/**
@@ -242,29 +285,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
242
285
return false ;
243
286
}
244
287
}
245
-
246
- let path = "" ;
247
- let curr : AST . JSONNode = data . node ;
248
- let p : AST . JSONNode | null = curr . parent ;
249
- while ( p ) {
250
- if ( p . type === "JSONProperty" ) {
251
- const name = getPropertyName ( p ) ;
252
- if ( / ^ [ $ _ a - z ] [ \w $ ] * $ / iu. test ( name ) ) {
253
- path = `.${ name } ${ path } ` ;
254
- } else {
255
- path = `[${ JSON . stringify ( name ) } ]${ path } ` ;
256
- }
257
- } else if ( p . type === "JSONArrayExpression" ) {
258
- const index = p . elements . indexOf ( curr as never ) ;
259
- path = `[${ index } ]${ path } ` ;
260
- }
261
- curr = p ;
262
- p = curr . parent ;
263
- }
264
- if ( path . startsWith ( "." ) ) {
265
- path = path . slice ( 1 ) ;
266
- }
267
- return pathPattern . test ( path ) ;
288
+ return pathPattern . test ( data . getPath ( ) ) ;
268
289
}
269
290
} ) ;
270
291
}
@@ -339,6 +360,9 @@ export default createRule("sort-keys", {
339
360
type : "integer" ,
340
361
minimum : 2 ,
341
362
} ,
363
+ allowLineSeparatedGroups : {
364
+ type : "boolean" ,
365
+ } ,
342
366
} ,
343
367
required : [ "pathPattern" , "order" ] ,
344
368
additionalProperties : false ,
@@ -365,6 +389,9 @@ export default createRule("sort-keys", {
365
389
type : "integer" ,
366
390
minimum : 2 ,
367
391
} ,
392
+ allowLineSeparatedGroups : {
393
+ type : "boolean" ,
394
+ } ,
368
395
} ,
369
396
additionalProperties : false ,
370
397
} ,
@@ -387,17 +414,30 @@ export default createRule("sort-keys", {
387
414
// Parse options.
388
415
const parsedOptions = parseOptions ( context . options ) ;
389
416
417
+ const sourceCode = context . getSourceCode ( ) ;
418
+
390
419
/**
391
420
* Verify for property
392
421
*/
393
422
function verifyProperty ( data : JSONPropertyData , option : ParsedOption ) {
394
423
if ( option . ignore ( data ) ) {
395
424
return ;
396
425
}
397
- const prevList = data . object . properties
398
- . slice ( 0 , data . index )
399
- . reverse ( )
400
- . filter ( ( d ) => ! option . ignore ( d ) ) ;
426
+ const prevList : JSONPropertyData [ ] = [ ] ;
427
+ let currTarget = data ;
428
+ let prevTarget ;
429
+ while ( ( prevTarget = currTarget . getPrev ( ) ) ) {
430
+ if ( option . allowLineSeparatedGroups ) {
431
+ if ( hasBlankLine ( prevTarget , currTarget ) ) {
432
+ break ;
433
+ }
434
+ }
435
+
436
+ if ( ! option . ignore ( prevTarget ) ) {
437
+ prevList . push ( prevTarget ) ;
438
+ }
439
+ currTarget = prevTarget ;
440
+ }
401
441
402
442
if ( prevList . length === 0 ) {
403
443
return ;
@@ -413,7 +453,6 @@ export default createRule("sort-keys", {
413
453
orderText : option . orderText ,
414
454
} ,
415
455
* fix ( fixer ) {
416
- const sourceCode = context . getSourceCode ( ) ;
417
456
let moveTarget = prevList [ 0 ] ;
418
457
for ( const prev of prevList ) {
419
458
if ( option . isValidOrder ( prev , data ) ) {
@@ -441,14 +480,46 @@ export default createRule("sort-keys", {
441
480
const insertTarget = sourceCode . getTokenBefore (
442
481
moveTarget . node as never
443
482
) ! ;
444
- yield fixer . insertTextAfterRange ( insertTarget . range , insertCode ) ;
483
+ let insertRange = insertTarget . range ;
484
+ const insertNext = sourceCode . getTokenAfter ( insertTarget , {
485
+ includeComments : true ,
486
+ } ) ! ;
487
+ if ( insertNext . loc ! . start . line - insertTarget . loc . end . line > 1 ) {
488
+ const offset = sourceCode . getIndexFromLoc ( {
489
+ line : insertNext . loc ! . start . line - 1 ,
490
+ column : 0 ,
491
+ } ) ;
492
+ insertRange = [ offset , offset ] ;
493
+ }
494
+ yield fixer . insertTextAfterRange ( insertRange , insertCode ) ;
445
495
446
496
yield fixer . removeRange ( [ removeStart , codeEnd ] ) ;
447
497
} ,
448
498
} ) ;
449
499
}
450
500
}
451
501
502
+ /**
503
+ * Checks whether the given two properties have a blank line between them.
504
+ */
505
+ function hasBlankLine ( prev : JSONPropertyData , next : JSONPropertyData ) {
506
+ const tokenOrNodes = [
507
+ ...sourceCode . getTokensBetween ( prev . node as never , next . node as never , {
508
+ includeComments : true ,
509
+ } ) ,
510
+ next . node ,
511
+ ] ;
512
+ let prevLoc = prev . node . loc ;
513
+ for ( const t of tokenOrNodes ) {
514
+ const loc = t . loc ! ;
515
+ if ( loc . start . line - prevLoc . end . line > 1 ) {
516
+ return true ;
517
+ }
518
+ prevLoc = loc ;
519
+ }
520
+ return false ;
521
+ }
522
+
452
523
return {
453
524
JSONObjectExpression ( node : AST . JSONObjectExpression ) {
454
525
const data = new JSONObjectData ( node ) ;
0 commit comments