Repository: azat-io/eslint-plugin-perfectionist
Pull request #427 (2024-12-20 20:33:05)

fix: `custom` sort type: give maximum priority to the `void` character
Fixes the issue mentioned in this [comment](https://github.com/azat-io/eslint-plugin-perfectionist/issues/424#issuecomment-2557345446). 

### Description

Today, `custom` sort doesn't handle void (empty) characters well: `a` will not always come before `aa`.
`localeCompare`'s sorting behavior is to put `a` first, and in general to give the maximum priority to the `void` character. I think it's better to mimic this by default.

### Is this a breaking change?

No, I think it's a **bug**: I believe users would expect the empty character to take maximum priority over any other character by default, like it is the case when locale-comparing strings, so this shouldn't be considered a breaking change.

### Can we customize it so the `void` character has a different priority?

- This is not easy:
  - We require users to pass an `alphabet`: a list of unique ordered characters.
    - Users can pass the alphabet as an array. In this case, it's possible: `alphabet: ['a', 'b', '']`.
    - Users can pass the alphabet as a string: this is not possible in that case, as you can't represent the `void` character inside a string, so this would require another option.
- I don't think there is a clear use case for that yet, so let's not implement that for now.

### What is the purpose of this pull request?

- [x] Bug fix



Patch:
diff --git a/utils/compare.ts b/utils/compare.ts
index 13aba43..ed43d9e 100644
--- a/utils/compare.ts
+++ b/utils/compare.ts
@@ -122,9 +122,9 @@ let getCustomSortingFunction = <T extends SortingNode>(
   return (aNode: T, bNode: T) => {
     let aValue = formatString(nodeValueGetter(aNode))
     let bValue = formatString(nodeValueGetter(bNode))
+    let minLength = Math.min(aValue.length, bValue.length)
     // Iterate character by character
-    // eslint-disable-next-line unicorn/no-for-loop
-    for (let i = 0; i < aValue.length; i++) {
+    for (let i = 0; i < minLength; i++) {
       let aCharacter = aValue[i]
       let bCharacter = bValue[i]
       let indexOfA = indexByCharacters.get(aCharacter)
@@ -135,7 +135,10 @@ let getCustomSortingFunction = <T extends SortingNode>(
         return convertBooleanToSign(indexOfA - indexOfB > 0)
       }
     }
-    return 0
+    if (aValue.length === bValue.length) {
+      return 0
+    }
+    return convertBooleanToSign(aValue.length - bValue.length > 0)
   }
 }
 


Test patch:
diff --git a/test/compare.test.ts b/test/compare.test.ts
index e2c9445..f34d74b 100644
--- a/test/compare.test.ts
+++ b/test/compare.test.ts
@@ -249,6 +249,21 @@ describe('compare', () => {
         }),
       ).toBe(0)
     })
+
+    it('gives maximum priority to void', () => {
+      expect(
+        compare(createTestNode({ name: 'a' }), createTestNode({ name: '' }), {
+          ...compareOptions,
+          alphabet: 'a',
+        }),
+      ).toBe(1)
+      expect(
+        compare(createTestNode({ name: '' }), createTestNode({ name: 'a' }), {
+          ...compareOptions,
+          alphabet: 'a',
+        }),
+      ).toBe(-1)
+    })
   })
 
   let createTestNode = ({ name }: { name: string }): SortingNode =>