@@ -20,6 +20,7 @@ type CompatibleWithESLintOptions =
2020 caseSensitive ?: boolean ;
2121 natural ?: boolean ;
2222 minKeys ?: number ;
23+ allowLineSeparatedGroups ?: boolean ;
2324 }
2425 ] ;
2526type PatternOption = {
@@ -35,6 +36,7 @@ type PatternOption = {
3536 }
3637 ) [ ] ;
3738 minKeys ?: number ;
39+ allowLineSeparatedGroups ?: boolean ;
3840} ;
3941type OrderObject = {
4042 type ?: OrderTypeOption ;
@@ -45,6 +47,7 @@ type ParsedOption = {
4547 isTargetObject : ( node : JSONObjectData ) => boolean ;
4648 ignore : ( data : JSONPropertyData ) => boolean ;
4749 isValidOrder : Validator ;
50+ allowLineSeparatedGroups : boolean ;
4851 orderText : string ;
4952} ;
5053type Validator = ( a : JSONPropertyData , b : JSONPropertyData ) => boolean ;
@@ -86,6 +89,11 @@ class JSONPropertyData {
8689 public get name ( ) {
8790 return ( this . cachedName ??= getPropertyName ( this . node ) ) ;
8891 }
92+
93+ public getPrev ( ) : JSONPropertyData | null {
94+ const prevIndex = this . index - 1 ;
95+ return prevIndex >= 0 ? this . object . properties [ prevIndex ] : null ;
96+ }
8997}
9098class JSONObjectData {
9199 public readonly node : AST . JSONObjectExpression ;
@@ -101,6 +109,36 @@ class JSONObjectData {
101109 ( e , index ) => new JSONPropertyData ( this , e , index )
102110 ) ) ;
103111 }
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+ }
104142}
105143
106144/**
@@ -153,6 +191,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
153191 const insensitive = obj . caseSensitive === false ;
154192 const natural = Boolean ( obj . natural ) ;
155193 const minKeys : number = obj . minKeys ?? 2 ;
194+ const allowLineSeparatedGroups = obj . allowLineSeparatedGroups || false ;
156195 return [
157196 {
158197 isTargetObject : ( node ) => node . properties . length >= minKeys ,
@@ -161,6 +200,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
161200 orderText : `${ natural ? "natural " : "" } ${
162201 insensitive ? "insensitive " : ""
163202 } ${ type } ending`,
203+ allowLineSeparatedGroups,
164204 } ,
165205 ] ;
166206 }
@@ -170,6 +210,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
170210 const pathPattern = new RegExp ( opt . pathPattern ) ;
171211 const hasProperties = opt . hasProperties ?? [ ] ;
172212 const minKeys : number = opt . minKeys ?? 2 ;
213+ const allowLineSeparatedGroups = opt . allowLineSeparatedGroups || false ;
173214 if ( ! Array . isArray ( order ) ) {
174215 const type : OrderTypeOption = order . type ?? "asc" ;
175216 const insensitive = order . caseSensitive === false ;
@@ -182,6 +223,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
182223 orderText : `${ natural ? "natural " : "" } ${
183224 insensitive ? "insensitive " : ""
184225 } ${ type } ending`,
226+ allowLineSeparatedGroups,
185227 } ;
186228 }
187229 const parsedOrder : {
@@ -227,6 +269,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
227269 return false ;
228270 } ,
229271 orderText : "specified" ,
272+ allowLineSeparatedGroups,
230273 } ;
231274
232275 /**
@@ -242,29 +285,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
242285 return false ;
243286 }
244287 }
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 ( ) ) ;
268289 }
269290 } ) ;
270291}
@@ -339,6 +360,9 @@ export default createRule("sort-keys", {
339360 type : "integer" ,
340361 minimum : 2 ,
341362 } ,
363+ allowLineSeparatedGroups : {
364+ type : "boolean" ,
365+ } ,
342366 } ,
343367 required : [ "pathPattern" , "order" ] ,
344368 additionalProperties : false ,
@@ -365,6 +389,9 @@ export default createRule("sort-keys", {
365389 type : "integer" ,
366390 minimum : 2 ,
367391 } ,
392+ allowLineSeparatedGroups : {
393+ type : "boolean" ,
394+ } ,
368395 } ,
369396 additionalProperties : false ,
370397 } ,
@@ -387,17 +414,30 @@ export default createRule("sort-keys", {
387414 // Parse options.
388415 const parsedOptions = parseOptions ( context . options ) ;
389416
417+ const sourceCode = context . getSourceCode ( ) ;
418+
390419 /**
391420 * Verify for property
392421 */
393422 function verifyProperty ( data : JSONPropertyData , option : ParsedOption ) {
394423 if ( option . ignore ( data ) ) {
395424 return ;
396425 }
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+ }
401441
402442 if ( prevList . length === 0 ) {
403443 return ;
@@ -413,7 +453,6 @@ export default createRule("sort-keys", {
413453 orderText : option . orderText ,
414454 } ,
415455 * fix ( fixer ) {
416- const sourceCode = context . getSourceCode ( ) ;
417456 let moveTarget = prevList [ 0 ] ;
418457 for ( const prev of prevList ) {
419458 if ( option . isValidOrder ( prev , data ) ) {
@@ -441,14 +480,46 @@ export default createRule("sort-keys", {
441480 const insertTarget = sourceCode . getTokenBefore (
442481 moveTarget . node as never
443482 ) ! ;
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 ) ;
445495
446496 yield fixer . removeRange ( [ removeStart , codeEnd ] ) ;
447497 } ,
448498 } ) ;
449499 }
450500 }
451501
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+
452523 return {
453524 JSONObjectExpression ( node : AST . JSONObjectExpression ) {
454525 const data = new JSONObjectData ( node ) ;
0 commit comments