Files
bytecii 07238bf64f fix(ts): sort strings by code point, not UTF-16 code unit (#370)
JavaScript orders strings by UTF-16 code unit. An astral character
(U+10000 and up) is a surrogate pair in 0xD800-0xDFFF, so it sorts before
every BMP name from U+E000 up, while Python's `sorted` puts it after.
`ls` on a directory holding a U+E000 name and an emoji listed them in
opposite orders across the two trees. GNU agrees with Python here, in
both C and C.UTF-8: UTF-8 byte order *is* code-point order, so
TypeScript was the only one of the three disagreeing.

Adds utils/sort.ts (compareCodePoints, sortedByCodePoints) and routes
every string sort through it. Three classes of site, only the first of
which was obvious:

- 131 bare `.sort()` calls across 94 files.
- ~40 explicit `a < b ? -1 : a > b ? 1 : 0` comparators, which are the
  same code-unit comparison wearing a comparator's clothes.
  core/gdrive/readdir.ts even carried the comment "Codepoint compare, not
  localeCompare: python sorts by codepoint" directly above one.
- 3 localeCompare calls, whose Python twins are all plain code-point
  sorts. ICU collation reorders ASCII too, so those diverged on far more
  than astral names.

An integ pin on `ls` and `find` is what caught generic/ls.ts, which the
bare-`.sort()` sweep could not see because it already passed a
comparator. eslint now rejects a zero-argument `.sort()` outright (it is
also wrong for numbers and tuples, which it compares as strings) so the
default cannot come back; typecheck caught the four sites where the
sweep had reached numbers and bigints.

Two latent bugs fixed on the way: git/reset.ts sorted [path, letter]
tuples by their `toString()`, ordering `a+b` before `a` because ',' has
a higher code point than '+'; sort_helper.ts's compareLines is the
`sort` command itself, so the C-locale byte order it should have had is
now what it computes.

Closes #370

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 06:46:39 -07:00

78 lines
2.7 KiB
JavaScript

import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import prettierConfig from 'eslint-config-prettier'
export default tseslint.config(
{
ignores: [
'**/dist/**',
'**/node_modules/**',
'**/*.d.ts',
'**/*.config.ts',
'**/*.config.js',
'**/*.setup.ts',
'**/scripts/**',
'**/generated/**',
],
},
js.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
{
// Commands read flags only through FlagView, which is constructed
// with the command's spec and throws on a name the spec does not
// declare. Reaching into the bag directly reads a renamed or
// misspelled flag as false, which no test catches. Python's twin is
// `tests/commands/test_no_raw_flag_reads.py`, which walks the whole
// `mirage/commands` tree, so this covers the same ground.
files: ['packages/*/src/commands/builtin/**/*.ts'],
ignores: ['packages/*/src/commands/builtin/**/*.test.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector:
"MemberExpression[object.type='MemberExpression'][object.object.name='opts'][object.property.name='flags']",
message:
'Read flags through FlagView (new FlagView(opts.flags, specOf(name))), not opts.flags directly.',
},
],
},
},
{
// JavaScript's default comparator orders by UTF-16 code unit, so an
// astral filename (U+10000 and up, stored as a surrogate pair in
// 0xD800-0xDFFF) sorts before every BMP name from U+E000 up, while
// Python's `sorted` puts it after -- issue #370. It is also wrong for
// numbers and tuples, which it compares as strings. Pass an explicit
// comparator: `compareCodePoints` for anything a user sees the order
// of. Tests are exempt, as they are for the flag rule above: a test
// sorts its own expectation, so it stays self-consistent either way.
files: ['packages/*/src/**/*.ts'],
ignores: ['packages/*/src/**/*.test.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.property.name='sort'][arguments.length=0]",
message:
'Pass a comparator to .sort(); the default orders by UTF-16 code unit and diverges from Python on astral characters (#370). Use compareCodePoints for strings.',
},
],
},
},
prettierConfig,
)