Skip to content

Commit

Permalink
Re-add importOrderCaseSensitive option (#184)
Browse files Browse the repository at this point in the history
Implements a custom natural sort algorithm which allows for numeric
natural sorting while also sorting all uppercase letters before all
lowercase letters, which is the desired behavior here.

For example, when `importOrderCaseSensitive` is false (the default):

```js
import ExampleComponent from './ExampleComponent';
import ExamplesList from './ExamplesList';
import ExampleWidget from './ExampleWidget';
```

Compared with `"importOrderCaseSensitive": true`:

```js
import ExampleComponent from './ExampleComponent';
import ExampleWidget from './ExampleWidget';
import ExamplesList from './ExamplesList';
```

Closes #151

---------

Co-authored-by: Ian VanSchooten <[email protected]>
  • Loading branch information
acnebs and IanVS authored Oct 8, 2024
1 parent fef70a6 commit e28fa0e
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 34 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This project is based on [@trivago/prettier-plugin-sort-imports](https://github.
- [7. Enable/disable plugin or use different order in certain folders or files](#7-enabledisable-plugin-or-use-different-order-in-certain-folders-or-files)
- [`importOrderTypeScriptVersion`](#importordertypescriptversion)
- [`importOrderParserPlugins`](#importorderparserplugins)
- [`importOrderCaseSensitive`](#importordercasesensitive)
- [Prevent imports from being sorted](#prevent-imports-from-being-sorted)
- [Comments](#comments)
- [FAQ / Troubleshooting](#faq--troubleshooting)
Expand Down Expand Up @@ -139,6 +140,7 @@ module.exports = {
importOrder: ['^@core/(.*)$', '', '^@server/(.*)$', '', '^@ui/(.*)$', '', '^[./]'],
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false,
};
```

Expand Down Expand Up @@ -393,6 +395,33 @@ with options as a JSON string of the plugin array:
"importOrderParserPlugins": []
```

#### `importOrderCaseSensitive`

**type**: `boolean`

**default value**: `false`

A boolean value to enable case-sensitivity in the sorting algorithm
used to order imports within each match group.

For example, when false (or not specified):

```javascript
import {CatComponent, catFilter, DogComponent, dogFilter} from './animals';
import ExampleComponent from './ExampleComponent';
import ExamplesList from './ExamplesList';
import ExampleWidget from './ExampleWidget';
```

compared with `"importOrderCaseSensitive": true`:

```javascript
import ExampleComponent from './ExampleComponent';
import ExampleWidget from './ExampleWidget';
import ExamplesList from './ExamplesList';
import {CatComponent, DogComponent, catFilter, dogFilter} from './animals';
```

### Prevent imports from being sorted

This plugin supports standard prettier ignore comments. By default, side-effect imports (like
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export const options: Record<
description:
'Version of TypeScript in use in the project. Determines some output syntax when using TypeScript.',
},
importOrderCaseSensitive: {
type: 'boolean',
category: 'Global',
default: false,
description: 'Provide a case sensitivity boolean flag',
},
};

export const parsers = {
Expand Down
129 changes: 109 additions & 20 deletions src/natural-sort/__tests__/natural-sort.spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,115 @@
import { expect, test } from 'vitest';
import { describe, expect, test } from 'vitest';

import { naturalSort } from '..';
import { naturalSort, naturalSortCaseSensitive } from '..';

test('should sort normal things alphabetically', () => {
expect(
['a', 'h', 'b', 'i', 'c', 'd', 'j', 'e', 'k', 'f', 'g'].sort((a, b) =>
naturalSort(a, b),
),
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
});
describe('naturalSort', () => {
test('should sort normal things alphabetically', () => {
expect(
['a', 'h', 'b', 'i', 'c', 'd', 'j', 'e', 'k', 'f', 'g'].sort(
(a, b) => naturalSort(a, b),
),
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
});

test('should ignore capitalization differences', () => {
expect(
['./ExampleComponent', './ExamplesList', './ExampleWidget'].sort(
(a, b) => naturalSort(a, b),
),
).toEqual(['./ExampleComponent', './ExamplesList', './ExampleWidget']);
});

test('should ignore capitalization differences', () => {
// We have no option to cause case-sensitive sorting, so this is the "default" case!
expect(
['./ExampleView', './ExamplesList'].sort((a, b) => naturalSort(a, b)),
).toEqual(['./ExamplesList', './ExampleView']);
test('should sort things numerically', () => {
expect(
[
'a2',
'a3',
'a10',
'a1',
'a11',
'a9',
'a1b',
'file000b',
'file000a',
'file00a',
'file00z',
].sort(naturalSort),
).toEqual([
'a1',
'a1b',
'a2',
'a3',
'a9',
'a10',
'a11',
'file000a',
'file00a',
'file000b',
'file00z',
]);
});
});

test('should sort things numerically', () => {
expect(
['a2', 'a3', 'a10', 'a1', 'a11', 'a9'].sort((a, b) =>
naturalSort(a, b),
),
).toEqual(['a1', 'a2', 'a3', 'a9', 'a10', 'a11']);
describe('naturalSortCaseSensitive', () => {
test('should not ignore capitalization differences', () => {
expect(
['./ExampleComponent', './ExamplesList', './ExampleWidget'].sort(
(a, b) => naturalSortCaseSensitive(a, b),
),
).toEqual(['./ExampleComponent', './ExampleWidget', './ExamplesList']);
});

test('should sort numerically and case-sensitively', () => {
expect(
[
'file1',
'File10',
'AbA',
'file10',
'files10',
'file1z',
'file10ab',
'file2s',
'a',
'Ab',
'file20',
'file22',
'file11',
'file2',
'File20',
'file000b',
'file000a',
'file00a',
'file00z',
'aaa',
'AAA',
'bBb',
'BBB',
].sort(naturalSortCaseSensitive),
).toEqual([
'AAA',
'Ab',
'AbA',
'BBB',
'File10',
'File20',
'a',
'aaa',
'bBb',
'file000a',
'file00a',
'file000b',
'file00z',
'file1',
'file1z',
'file2',
'file2s',
'file10',
'file10ab',
'file11',
'file20',
'file22',
'files10',
]);
});
});
39 changes: 37 additions & 2 deletions src/natural-sort/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
export function naturalSort(a: string, b: string): number {
const left = typeof a === 'string' ? a : String(a);

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#syntax
const sortOptions: Intl.CollatorOptions = {
sensitivity: 'base',
numeric: true,
caseFirst: 'lower',
};

return left.localeCompare(b, 'en', sortOptions);
}

/**
* Using a custom comparison function here, as `String.localeCompare` does not
* support sorting characters with all uppercase letters before lowercase
* letters, which is the desired behavior for a case-sensitive import sort. When
* `sensitivity` is set to `base`, `String.localeCompare` sorts alphabetically
* and then by case, but we want to sort by case first (then alphabetical).
*/
const numericRegex = /^\d+/;
export function naturalSortCaseSensitive(a: string, b: string) {
let aIndex = 0;
let bIndex = 0;
while (aIndex < Math.max(a.length, b.length)) {
// check if we've encountered a number and compare appropriately if so
const aNumericMatch = a.slice(aIndex).match(numericRegex);
const bNumericMatch = b.slice(bIndex).match(numericRegex);
if (aNumericMatch && !bNumericMatch) return -1;
if (!aNumericMatch && bNumericMatch) return 1;
if (aNumericMatch && bNumericMatch) {
const aNumber = parseInt(aNumericMatch[0]);
const bNumber = parseInt(bNumericMatch[0]);
if (aNumber > bNumber) return 1;
if (aNumber < bNumber) return -1;
aIndex += aNumericMatch[0].length;
bIndex += bNumericMatch[0].length;
}
// otherwise just compare characters directly
const aChar = a[aIndex];
const bChar = b[bIndex];
if (aChar && !bChar) return 1;
if (!aChar && bChar) return -1;
if (aChar !== bChar) return aChar.charCodeAt(0) - bChar.charCodeAt(0);
aIndex++;
bIndex++;
}
return 0;
}
10 changes: 8 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export interface PrettierOptions
/** Subset of options that need to be normalized, or affect normalization */
export type NormalizableOptions = Pick<
PrettierOptions,
'importOrder' | 'importOrderParserPlugins' | 'importOrderTypeScriptVersion'
| 'importOrder'
| 'importOrderParserPlugins'
| 'importOrderTypeScriptVersion'
| 'importOrderCaseSensitive'
> &
// filepath can be undefined when running prettier via the api on text input
Pick<Partial<PrettierOptions>, 'filepath'>;
Expand Down Expand Up @@ -63,6 +66,7 @@ export type ImportRelated = ImportOrLine | SomeSpecifier;
export interface ExtendedOptions {
importOrder: PrettierOptions['importOrder'];
importOrderCombineTypeAndValueImports: boolean;
importOrderCaseSensitive: boolean;
hasAnyCustomGroupSeparatorsInImportOrder: boolean;
provideGapAfterTopOfFileComments: boolean;
plugins: ParserPlugin[];
Expand All @@ -79,7 +83,9 @@ export type GetSortedNodes = (

export type GetSortedNodesByImportOrder = (
nodes: ImportDeclaration[],
options: Pick<ExtendedOptions, 'importOrder'>,
options: Pick<ExtendedOptions, 'importOrder'> & {
importOrderCaseSensitive?: boolean;
},
) => ImportOrLine[];

export type GetChunkTypeOfNode = (node: ImportDeclaration) => ChunkType;
Expand Down
17 changes: 17 additions & 0 deletions src/utils/__tests__/get-sorted-import-specifiers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,20 @@ test('should group type imports after value imports - flow', () => {
'TypeB',
]);
});

test('should sort case-sensitively', () => {
const code = `import { ExampleComponent, ExamplesList, ExampleWidget } from '@components/e';`;
const [importNode] = getImportNodes(code);
const sortedImportSpecifiers = getSortedImportSpecifiers(importNode, {
importOrderCaseSensitive: true,
});
const specifiersList = getSortedNodesModulesNames(
sortedImportSpecifiers.specifiers,
);

expect(specifiersList).toEqual([
'ExampleComponent',
'ExampleWidget',
'ExamplesList',
]);
});
25 changes: 25 additions & 0 deletions src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,28 @@ test('it does not add multiple custom import separators', () => {
'./local',
]);
});

test('it should sort nodes case-sensitively', () => {
const result = getImportNodes(code);
const sorted = getSortedNodesByImportOrder(result, {
importOrder: testingOnly.normalizeImportOrderOption(['^[./]']),
importOrderCaseSensitive: true,
}) as ImportDeclaration[];
expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([
'node:fs/promises',
'node:url',
'path',
'BY',
'Ba',
'XY',
'Xa',
'a',
'c',
'g',
'k',
't',
'x',
'z',
'./local',
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultOptions = examineAndNormalizePluginOptions({
// First separator for top-of-file comments, second to separate side-effect and ignored chunks, for easier test readability
importOrder: testingOnly.normalizeImportOrderOption(['', '']),
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false,
importOrderParserPlugins: [],
filepath: __filename,
});
Expand Down
Loading

0 comments on commit e28fa0e

Please sign in to comment.