1- import coreRule from "eslint/lib/rules/sort-keys"
2- import { createRule , defineWrapperListener } from "../utils"
1+ /**
2+ * Taken with https://github.com/eslint/eslint/blob/master/lib/rules/sort-keys.js
3+ */
4+ import naturalCompare from "natural-compare"
5+ import { createRule } from "../utils"
6+ import { isCommaToken } from "eslint-utils"
7+ import type { JSONProperty , JSONObjectExpression } from "../parser/ast"
8+ import { getStaticJSONValue } from "../utils/ast"
9+
10+ //------------------------------------------------------------------------------
11+ // Helpers
12+ //------------------------------------------------------------------------------
13+
14+ /**
15+ * Gets the property name of the given `Property` node.
16+ */
17+ function getPropertyName ( node : JSONProperty ) : string {
18+ const prop = node . key
19+ if ( prop . type === "JSONIdentifier" ) {
20+ return prop . name
21+ }
22+ return String ( getStaticJSONValue ( prop ) )
23+ }
24+
25+ /**
26+ * Build function which check that the given 2 names are in specific order.
27+ */
28+ function buildValidator ( order : Option , insensitive : boolean , natural : boolean ) {
29+ let compare = natural
30+ ? ( [ a , b ] : string [ ] ) => naturalCompare ( a , b ) <= 0
31+ : ( [ a , b ] : string [ ] ) => a <= b
32+ if ( insensitive ) {
33+ const baseCompare = compare
34+ compare = ( [ a , b ] : string [ ] ) =>
35+ baseCompare ( [ a . toLowerCase ( ) , b . toLowerCase ( ) ] )
36+ }
37+ if ( order === "desc" ) {
38+ const baseCompare = compare
39+ compare = ( args : string [ ] ) => baseCompare ( args . reverse ( ) )
40+ }
41+ return ( a : string , b : string ) => compare ( [ a , b ] )
42+ }
43+
44+ const allowOptions = [ "asc" , "desc" ] as const
45+ type Option = typeof allowOptions [ number ]
46+
47+ //------------------------------------------------------------------------------
48+ // Rule Definition
49+ //------------------------------------------------------------------------------
350
451export default createRule ( "sort-keys" , {
552 meta : {
@@ -8,12 +55,135 @@ export default createRule("sort-keys", {
855 recommended : null ,
956 extensionRule : true ,
1057 } ,
11- fixable : coreRule . meta ?. fixable ,
12- schema : coreRule . meta ?. schema ! ,
13- messages : coreRule . meta ?. messages ! ,
14- type : coreRule . meta ?. type ! ,
58+ fixable : "code" ,
59+ schema : [
60+ {
61+ enum : allowOptions ,
62+ } ,
63+ {
64+ type : "object" ,
65+ properties : {
66+ caseSensitive : {
67+ type : "boolean" ,
68+ default : true ,
69+ } ,
70+ natural : {
71+ type : "boolean" ,
72+ default : false ,
73+ } ,
74+ minKeys : {
75+ type : "integer" ,
76+ minimum : 2 ,
77+ default : 2 ,
78+ } ,
79+ } ,
80+ additionalProperties : false ,
81+ } ,
82+ ] ,
83+ messages : {
84+ sortKeys :
85+ "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'." ,
86+ } ,
87+ type : "suggestion" ,
1588 } ,
1689 create ( context ) {
17- return defineWrapperListener ( coreRule , context , context . options )
90+ // Parse options.
91+ const order : Option = context . options [ 0 ] || "asc"
92+ const options = context . options [ 1 ]
93+ const insensitive : boolean = options && options . caseSensitive === false
94+ const natural : boolean = options && options . natural
95+ const minKeys : number = options && options . minKeys
96+ const isValidOrder = buildValidator ( order , insensitive , natural )
97+ type Stack = {
98+ upper : Stack | null
99+ prevList : { name : string ; node : JSONProperty } [ ]
100+ numKeys : number
101+ }
102+ let stack : Stack = { upper : null , prevList : [ ] , numKeys : 0 }
103+
104+ return {
105+ JSONObjectExpression ( node : JSONObjectExpression ) {
106+ stack = {
107+ upper : stack ,
108+ prevList : [ ] ,
109+ numKeys : node . properties . length ,
110+ }
111+ } ,
112+
113+ "JSONObjectExpression:exit" ( ) {
114+ stack = stack . upper !
115+ } ,
116+
117+ JSONProperty ( node : JSONProperty ) {
118+ const prevList = stack . prevList
119+ const numKeys = stack . numKeys
120+ const thisName = getPropertyName ( node )
121+
122+ stack . prevList = [
123+ {
124+ name : thisName ,
125+ node,
126+ } ,
127+ ...prevList ,
128+ ]
129+ if ( prevList . length === 0 || numKeys < minKeys ) {
130+ return
131+ }
132+ const prevName = prevList [ 0 ] . name
133+ if ( ! isValidOrder ( prevName , thisName ) ) {
134+ context . report ( {
135+ loc : node . key . loc ,
136+ messageId : "sortKeys" ,
137+ data : {
138+ thisName,
139+ prevName,
140+ order,
141+ insensitive : insensitive ? "insensitive " : "" ,
142+ natural : natural ? "natural " : "" ,
143+ } ,
144+ * fix ( fixer ) {
145+ const sourceCode = context . getSourceCode ( )
146+ let moveTarget = prevList [ 0 ] . node
147+ for ( const prev of prevList ) {
148+ if ( isValidOrder ( prev . name , thisName ) ) {
149+ break
150+ } else {
151+ moveTarget = prev . node
152+ }
153+ }
154+
155+ const beforeToken = sourceCode . getTokenBefore (
156+ node as never ,
157+ ) !
158+ const afterToken = sourceCode . getTokenAfter (
159+ node as never ,
160+ ) !
161+ const hasAfterComma = isCommaToken ( afterToken )
162+ const codeStart = beforeToken . range [ 1 ] // to include comments
163+ const codeEnd = hasAfterComma
164+ ? afterToken . range [ 1 ] // |/**/ key: value,|
165+ : node . range [ 1 ] // |/**/ key: value|
166+ const removeStart = hasAfterComma
167+ ? codeStart // |/**/ key: value,|
168+ : beforeToken . range [ 0 ] // |,/**/ key: value|
169+
170+ const insertCode =
171+ sourceCode . text . slice ( codeStart , codeEnd ) +
172+ ( hasAfterComma ? "" : "," )
173+
174+ const insertTarget = sourceCode . getTokenBefore (
175+ moveTarget as never ,
176+ ) !
177+ yield fixer . insertTextAfterRange (
178+ insertTarget . range ,
179+ insertCode ,
180+ )
181+
182+ yield fixer . removeRange ( [ removeStart , codeEnd ] )
183+ } ,
184+ } )
185+ }
186+ } ,
187+ }
18188 } ,
19189} )
0 commit comments