07238bf64f
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>
78 lines
2.7 KiB
JavaScript
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,
|
|
)
|