1+ import type { TSESTree } from '@typescript-eslint/types'
2+
13import type { SortingNode } from '../typings'
24
35import { createEslintRule } from '../utils/create-eslint-rule'
@@ -20,6 +22,7 @@ type Group<T extends string[]> = 'multiline' | 'unknown' | T[number]
2022
2123type Options < T extends string [ ] > = [
2224 Partial < {
25+ groupKind : 'required-first' | 'optional-first' | 'mixed'
2326 type : 'alphabetical' | 'line-length' | 'natural'
2427 groups : ( Group < T > [ ] | Group < T > ) [ ]
2528 partitionByNewLine : boolean
@@ -62,6 +65,11 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
6265 'Allows to use spaces to separate the nodes into logical groups.' ,
6366 type : 'boolean' ,
6467 } ,
68+ groupKind : {
69+ description : 'Specifies top-level groups.' ,
70+ type : 'string' ,
71+ enum : [ 'mixed' , 'required-first' , 'optional-first' ] ,
72+ } ,
6573 groups : {
6674 description : 'Specifies the order of the groups.' ,
6775 type : 'array' ,
@@ -111,6 +119,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
111119 order : 'asc' ,
112120 ignoreCase : true ,
113121 partitionByNewLine : false ,
122+ groupKind : 'mixed' ,
114123 groups : [ ] ,
115124 customGroups : { } ,
116125 } ,
@@ -121,6 +130,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
121130 let options = complete ( context . options . at ( 0 ) , {
122131 partitionByNewLine : false ,
123132 type : 'alphabetical' ,
133+ groupKind : 'mixed' ,
124134 ignoreCase : true ,
125135 customGroups : { } ,
126136 order : 'asc' ,
@@ -129,89 +139,133 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
129139
130140 let sourceCode = getSourceCode ( context )
131141
132- let formattedMembers : SortingNode [ ] [ ] = node . members . reduce (
133- ( accumulator : SortingNode [ ] [ ] , member ) => {
134- let name : string
135- let raw = sourceCode . text . slice (
136- member . range . at ( 0 ) ,
137- member . range . at ( 1 ) ,
138- )
139- let lastMember = accumulator . at ( - 1 ) ?. at ( - 1 )
142+ let formattedMembers : SortingNode < TSESTree . TypeElement > [ ] [ ] =
143+ node . members . reduce (
144+ ( accumulator : SortingNode < TSESTree . TypeElement > [ ] [ ] , member ) => {
145+ let name : string
146+ let raw = sourceCode . text . slice (
147+ member . range . at ( 0 ) ,
148+ member . range . at ( 1 ) ,
149+ )
150+ let lastMember = accumulator . at ( - 1 ) ?. at ( - 1 )
151+
152+ let { getGroup, defineGroup, setCustomGroups } = useGroups (
153+ options . groups ,
154+ )
140155
141- let { getGroup, defineGroup, setCustomGroups } = useGroups (
142- options . groups ,
143- )
156+ let formatName = ( value : string ) : string =>
157+ value . replace ( / ( , | ; ) $ / , '' )
144158
145- let formatName = ( value : string ) : string =>
146- value . replace ( / ( , | ; ) $ / , '' )
159+ if ( member . type === 'TSPropertySignature' ) {
160+ if ( member . key . type === 'Identifier' ) {
161+ ; ( { name } = member . key )
162+ } else if ( member . key . type === 'Literal' ) {
163+ name = `${ member . key . value } `
164+ } else {
165+ name = sourceCode . text . slice (
166+ member . range . at ( 0 ) ,
167+ member . typeAnnotation ?. range . at ( 0 ) ,
168+ )
169+ }
170+ } else if ( member . type === 'TSIndexSignature' ) {
171+ let endIndex : number =
172+ member . typeAnnotation ?. range . at ( 0 ) ?? member . range . at ( 1 ) !
147173
148- if ( member . type === 'TSPropertySignature' ) {
149- if ( member . key . type === 'Identifier' ) {
150- ; ( { name } = member . key )
151- } else if ( member . key . type === 'Literal' ) {
152- name = `${ member . key . value } `
174+ name = formatName (
175+ sourceCode . text . slice ( member . range . at ( 0 ) , endIndex ) ,
176+ )
153177 } else {
154- name = sourceCode . text . slice (
155- member . range . at ( 0 ) ,
156- member . typeAnnotation ?. range . at ( 0 ) ,
178+ name = formatName (
179+ sourceCode . text . slice ( member . range . at ( 0 ) , member . range . at ( 1 ) ) ,
157180 )
158181 }
159- } else if ( member . type === 'TSIndexSignature' ) {
160- let endIndex : number =
161- member . typeAnnotation ?. range . at ( 0 ) ?? member . range . at ( 1 ) !
162-
163- name = formatName (
164- sourceCode . text . slice ( member . range . at ( 0 ) , endIndex ) ,
165- )
166- } else {
167- name = formatName (
168- sourceCode . text . slice ( member . range . at ( 0 ) , member . range . at ( 1 ) ) ,
169- )
170- }
171182
172- setCustomGroups ( options . customGroups , name )
183+ setCustomGroups ( options . customGroups , name )
173184
174- if ( member . loc . start . line !== member . loc . end . line ) {
175- defineGroup ( 'multiline' )
176- }
185+ if ( member . loc . start . line !== member . loc . end . line ) {
186+ defineGroup ( 'multiline' )
187+ }
177188
178- let endsWithComma = raw . endsWith ( ';' ) || raw . endsWith ( ',' )
179- let endSize = endsWithComma ? 1 : 0
189+ let endsWithComma = raw . endsWith ( ';' ) || raw . endsWith ( ',' )
190+ let endSize = endsWithComma ? 1 : 0
180191
181- let memberSortingNode = {
182- size : rangeToDiff ( member . range ) - endSize ,
183- node : member ,
184- name,
185- }
192+ let memberSortingNode = {
193+ size : rangeToDiff ( member . range ) - endSize ,
194+ node : member ,
195+ name,
196+ }
186197
187- if (
188- options . partitionByNewLine &&
189- lastMember &&
190- getLinesBetween ( sourceCode , lastMember , memberSortingNode )
191- ) {
192- accumulator . push ( [ ] )
193- }
198+ if (
199+ options . partitionByNewLine &&
200+ lastMember &&
201+ getLinesBetween ( sourceCode , lastMember , memberSortingNode )
202+ ) {
203+ accumulator . push ( [ ] )
204+ }
194205
195- accumulator . at ( - 1 ) ?. push ( {
196- ...memberSortingNode ,
197- group : getGroup ( ) ,
198- } )
206+ accumulator . at ( - 1 ) ?. push ( {
207+ ...memberSortingNode ,
208+ group : getGroup ( ) ,
209+ } )
199210
200- return accumulator
201- } ,
202- [ [ ] ] ,
203- )
211+ return accumulator
212+ } ,
213+ [ [ ] ] ,
214+ )
204215
205216 for ( let nodes of formattedMembers ) {
206217 pairwise ( nodes , ( left , right ) => {
207218 let leftNum = getGroupNumber ( options . groups , left )
208219 let rightNum = getGroupNumber ( options . groups , right )
209220
221+ let getIsOptionalValue = ( nodeValue : TSESTree . TypeElement ) => {
222+ if (
223+ nodeValue . type === 'TSCallSignatureDeclaration' ||
224+ nodeValue . type === 'TSConstructSignatureDeclaration' ||
225+ nodeValue . type === 'TSIndexSignature'
226+ ) {
227+ return false
228+ }
229+ return nodeValue . optional
230+ }
231+
232+ let isLeftOptional = getIsOptionalValue ( left . node )
233+ let isRightOptional = getIsOptionalValue ( right . node )
234+
235+ let compareValue
210236 if (
211- leftNum > rightNum ||
212- ( leftNum === rightNum &&
213- isPositive ( compare ( left , right , options ) ) )
237+ options . groupKind === 'optional-first' &&
238+ isLeftOptional &&
239+ ! isRightOptional
240+ ) {
241+ compareValue = false
242+ } else if (
243+ options . groupKind === 'optional-first' &&
244+ ! isLeftOptional &&
245+ isRightOptional
246+ ) {
247+ compareValue = true
248+ } else if (
249+ options . groupKind === 'required-first' &&
250+ ! isLeftOptional &&
251+ isRightOptional
252+ ) {
253+ compareValue = false
254+ } else if (
255+ options . groupKind === 'required-first' &&
256+ isLeftOptional &&
257+ ! isRightOptional
214258 ) {
259+ compareValue = true
260+ } else if ( leftNum > rightNum ) {
261+ compareValue = true
262+ } else if ( leftNum === rightNum ) {
263+ compareValue = isPositive ( compare ( left , right , options ) )
264+ } else {
265+ compareValue = false
266+ }
267+
268+ if ( compareValue ) {
215269 context . report ( {
216270 messageId : 'unexpectedObjectTypesOrder' ,
217271 data : {
@@ -220,29 +274,55 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
220274 } ,
221275 node : right . node ,
222276 fix : fixer => {
223- let grouped : {
224- [ key : string ] : SortingNode [ ]
225- } = { }
277+ let groupedByKind
278+ if ( options . groupKind !== 'mixed' ) {
279+ groupedByKind = nodes . reduce <
280+ SortingNode < TSESTree . TypeElement > [ ] [ ]
281+ > (
282+ ( accumulator , currentNode ) => {
283+ let requiredIndex =
284+ options . groupKind === 'required-first' ? 0 : 1
285+ let optionalIndex =
286+ options . groupKind === 'required-first' ? 1 : 0
226287
227- for ( let currentNode of nodes ) {
228- let groupNum = getGroupNumber ( options . groups , currentNode )
229-
230- if ( ! ( groupNum in grouped ) ) {
231- grouped [ groupNum ] = [ currentNode ]
232- } else {
233- grouped [ groupNum ] = sortNodes (
234- [ ... grouped [ groupNum ] , currentNode ] ,
235- options ,
236- )
237- }
288+ if ( getIsOptionalValue ( currentNode . node ) ) {
289+ accumulator [ optionalIndex ] . push ( currentNode )
290+ } else {
291+ accumulator [ requiredIndex ] . push ( currentNode )
292+ }
293+ return accumulator
294+ } ,
295+ [ [ ] , [ ] ] ,
296+ )
297+ } else {
298+ groupedByKind = [ nodes ]
238299 }
239300
240301 let sortedNodes : SortingNode [ ] = [ ]
241302
242- for ( let group of Object . keys ( grouped ) . sort (
243- ( a , b ) => Number ( a ) - Number ( b ) ,
244- ) ) {
245- sortedNodes . push ( ...sortNodes ( grouped [ group ] , options ) )
303+ for ( let nodesByKind of groupedByKind ) {
304+ let grouped : {
305+ [ key : string ] : SortingNode [ ]
306+ } = { }
307+
308+ for ( let currentNode of nodesByKind ) {
309+ let groupNum = getGroupNumber ( options . groups , currentNode )
310+
311+ if ( ! ( groupNum in grouped ) ) {
312+ grouped [ groupNum ] = [ currentNode ]
313+ } else {
314+ grouped [ groupNum ] = sortNodes (
315+ [ ...grouped [ groupNum ] , currentNode ] ,
316+ options ,
317+ )
318+ }
319+ }
320+
321+ for ( let group of Object . keys ( grouped ) . sort (
322+ ( a , b ) => Number ( a ) - Number ( b ) ,
323+ ) ) {
324+ sortedNodes . push ( ...sortNodes ( grouped [ group ] , options ) )
325+ }
246326 }
247327
248328 return makeFixes ( fixer , nodes , sortedNodes , sourceCode )
0 commit comments