biome-typescript-best-practices-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/README.md +243 -0
- package/package.json +45 -0
- package/typescript.grit +131 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# biome-typescript-best-practices-plugin
|
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# biome-typescript-best-practices-plugin
|
|
2
|
+
|
|
3
|
+
A [Biome](https://biomejs.dev) plugin (written in [GritQL](https://biomejs.dev/blog/gritql-biome)) that
|
|
4
|
+
enforces TypeScript best practices **not covered by Biome's recommended linter** — catching the patterns
|
|
5
|
+
that quietly bypass the type checker, walk the prototype chain, or sort your numbers wrong.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// flagged
|
|
9
|
+
const el = getEl() as HTMLInputElement; // `as` bypasses the type checker
|
|
10
|
+
if ("id" in obj) {} // `in` walks the prototype chain
|
|
11
|
+
items.reduce((acc, x) => acc, {}); // `{}` accumulator leaks prototype keys
|
|
12
|
+
enum Color { Red, Green } // enum emits runtime code
|
|
13
|
+
delete obj[key]; // dynamic delete deoptimises shape
|
|
14
|
+
[3, 20, 100].sort(); // lexicographic → [100, 20, 3]
|
|
15
|
+
|
|
16
|
+
// safe
|
|
17
|
+
const el = getEl(); // narrow with a type guard, or `satisfies`
|
|
18
|
+
if (Object.hasOwn(obj, "id")) {}
|
|
19
|
+
items.reduce((acc, x) => acc.set(x.k, x.v), new Map());
|
|
20
|
+
const Color = { Red: "red", Green: "green" } as const;
|
|
21
|
+
delete obj.prop; // static key is fine
|
|
22
|
+
[3, 20, 100].sort((a, b) => a - b);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
| Rule | Flags | Why |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `ts/no-as-cast` | `expr as T` type assertions (except `as const`) | `as` silences the type checker and can mask real type errors. |
|
|
30
|
+
| `ts/no-in-operator` | the `in` operator and `for...in` | `in` walks the prototype chain, matching inherited/polluted keys. |
|
|
31
|
+
| `ts/no-empty-object-accumulator` | `reduce(…, {})` / `reduceRight(…, {})` | A `{}` accumulator carries `Object.prototype`, so dynamic keys like `"__proto__"` leak in. |
|
|
32
|
+
| `ts/no-enum` | `enum` and `const enum` declarations | Enums emit runtime code with surprising semantics; `const enum` breaks `isolatedModules`. |
|
|
33
|
+
| `ts/no-dynamic-delete` | `delete obj[expr]` with a computed, non-literal key | Deleting a dynamic key deoptimises the object shape and usually means a `Map` was wanted. |
|
|
34
|
+
| `ts/require-array-sort-compare` | `.sort()` / `.toSorted()` with no comparator | Default sort is by UTF-16 code unit, so numbers come out in the wrong order. |
|
|
35
|
+
|
|
36
|
+
All rules report a diagnostic only (severity `warn`, category `plugin`); none apply an auto-fix, because the
|
|
37
|
+
correct repair is context-specific — the plugin flags the hazard and leaves the fix to you.
|
|
38
|
+
|
|
39
|
+
These rules are intentionally **not** duplicates of Biome's recommended set (which already covers
|
|
40
|
+
`noExplicitAny`, `noNonNullAssertion`, `noDoubleEquals`, and similar). They fill gaps that otherwise need
|
|
41
|
+
`typescript-eslint`.
|
|
42
|
+
|
|
43
|
+
### ts/no-as-cast
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// flagged
|
|
47
|
+
const el = getEl() as HTMLInputElement;
|
|
48
|
+
const rec = (obj as Record<string, unknown>).key;
|
|
49
|
+
const twice = value as unknown as Target; // double assertion — flagged twice
|
|
50
|
+
|
|
51
|
+
// safe
|
|
52
|
+
const val = input as const; // `as const` narrows a literal, allowed
|
|
53
|
+
const ok = value satisfies Target; // `satisfies` validates without asserting
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A type assertion tells the compiler "trust me" and switches off the very check you installed TypeScript
|
|
57
|
+
for. Only `as const` (literal narrowing) is exempt — every other `as T` is flagged. A double assertion
|
|
58
|
+
(`x as unknown as T`) is two `TsAsExpression` nodes, so it reports twice. Inspired by
|
|
59
|
+
[`biome-plugin-no-type-assertion`](https://github.com/albertodeago/biome-plugin-no-type-assertion).
|
|
60
|
+
|
|
61
|
+
### ts/no-in-operator
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// flagged
|
|
65
|
+
if ("id" in obj) {}
|
|
66
|
+
for (const k in obj) {}
|
|
67
|
+
|
|
68
|
+
// safe
|
|
69
|
+
if (Object.hasOwn(obj, "id")) {}
|
|
70
|
+
for (const k of Object.keys(obj)) {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `in` operator and `for...in` both consult the prototype chain, so inherited or prototype-polluted keys
|
|
74
|
+
match. `Object.hasOwn` is an own-property check; `Object.keys`/`Object.entries` with `for...of` iterate only
|
|
75
|
+
own enumerable keys. Inspired by
|
|
76
|
+
[felixarntz/biome's `no-in-operator`](https://github.com/felixarntz/biome/blob/main/rules/no-in-operator.grit).
|
|
77
|
+
|
|
78
|
+
### ts/no-empty-object-accumulator
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// flagged
|
|
82
|
+
items.reduce((acc, x) => { acc[x.k] = x.v; return acc; }, {});
|
|
83
|
+
items.reduceRight((acc, x) => acc, {});
|
|
84
|
+
|
|
85
|
+
// safe
|
|
86
|
+
items.reduce((acc, x) => acc.set(x.k, x.v), new Map());
|
|
87
|
+
items.reduce((acc, x) => acc + x, 0);
|
|
88
|
+
items.reduce((acc, x) => acc, { total: 0 }); // seeded object, not empty
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Only an **empty** `{}` seed is flagged — that is the shape used for dynamic-key aggregation, where a key like
|
|
92
|
+
`"__proto__"` or `"constructor"` can collide with `Object.prototype`. A `new Map()` or `Object.create(null)`
|
|
93
|
+
has no such surface. Inspired by
|
|
94
|
+
[felixarntz/biome's `no-empty-object-accumulator`](https://github.com/felixarntz/biome/blob/main/rules/no-empty-object-accumulator.grit).
|
|
95
|
+
|
|
96
|
+
### ts/no-enum
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// flagged
|
|
100
|
+
enum Color { Red, Green }
|
|
101
|
+
const enum Dir { Up, Down }
|
|
102
|
+
|
|
103
|
+
// safe
|
|
104
|
+
const Color = { Red: "red", Green: "green" } as const;
|
|
105
|
+
type Color = (typeof Color)[keyof typeof Color];
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`enum` is one of the few TypeScript features that emits runtime code, and it has surprising semantics (numeric
|
|
109
|
+
enums are bidirectional maps; a value can be assigned any number). `const enum` is erased but breaks under
|
|
110
|
+
`isolatedModules` / Babel / esbuild. A `const` object with `as const` plus a derived union is fully erasable
|
|
111
|
+
and behaves predictably.
|
|
112
|
+
|
|
113
|
+
### ts/no-dynamic-delete
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// flagged
|
|
117
|
+
delete obj[key];
|
|
118
|
+
delete cache[getId()];
|
|
119
|
+
delete registry[user.id];
|
|
120
|
+
|
|
121
|
+
// safe
|
|
122
|
+
delete obj.prop; // static member
|
|
123
|
+
delete obj["literal"]; // literal key
|
|
124
|
+
delete arr[0]; // literal index
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Deleting a computed, non-literal key forces the engine to change the object's hidden shape, deoptimising it,
|
|
128
|
+
and usually signals that a `Map` (with `.delete(key)`) was the right structure — or that the field should be
|
|
129
|
+
modelled as optional (`v?: T`). Static members and literal keys are left alone.
|
|
130
|
+
|
|
131
|
+
### ts/require-array-sort-compare
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// flagged
|
|
135
|
+
[3, 20, 100].sort(); // → [100, 20, 3]
|
|
136
|
+
numbers.toSorted();
|
|
137
|
+
|
|
138
|
+
// safe
|
|
139
|
+
[3, 20, 100].sort((a, b) => a - b);
|
|
140
|
+
strings.sort(); // NOTE: also flagged — see limitations
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`Array#sort` and `Array#toSorted` with no comparator coerce elements to strings and compare by UTF-16 code
|
|
144
|
+
unit, so `[3, 20, 100]` sorts to `[100, 20, 3]`. Passing an explicit comparator makes the order intentional
|
|
145
|
+
and correct.
|
|
146
|
+
|
|
147
|
+
## Limitations
|
|
148
|
+
|
|
149
|
+
The plugin matches **structure, not types** — it keys off method and operator shapes, not the static type of
|
|
150
|
+
the receiver. Practical consequences:
|
|
151
|
+
|
|
152
|
+
- `ts/require-array-sort-compare` flags every argument-less `.sort()` / `.toSorted()`, including on
|
|
153
|
+
`string[]` where the default order is fine. Add a comparator (`(a, b) => a.localeCompare(b)`) or suppress
|
|
154
|
+
the line if the lexicographic default is intended.
|
|
155
|
+
- `ts/no-empty-object-accumulator` matches any `.reduce(fn, {})` regardless of receiver, and
|
|
156
|
+
`ts/no-dynamic-delete` matches any `delete x[expr]`.
|
|
157
|
+
- Biome's GritQL plugins cannot yet take per-rule configuration, so the matches are intentionally broad. Scope
|
|
158
|
+
the plugin with Biome's `includes` / `overrides` if false positives are a problem, or disable an individual
|
|
159
|
+
rule by editing your copy of the `.grit` file.
|
|
160
|
+
|
|
161
|
+
## Usage
|
|
162
|
+
|
|
163
|
+
Install the plugin as a dev dependency:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
npm install -D biome-typescript-best-practices-plugin
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Reference it from your Biome configuration:
|
|
170
|
+
|
|
171
|
+
```jsonc
|
|
172
|
+
{
|
|
173
|
+
"plugins": ["biome-typescript-best-practices-plugin/typescript.grit"],
|
|
174
|
+
"linter": {
|
|
175
|
+
"rules": { "recommended": true }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Then run the linter:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
npx @biomejs/biome lint <files>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Requires Biome **2.0+** (GritQL plugins landed in v2.0). Developed and tested against Biome 2.5.
|
|
187
|
+
|
|
188
|
+
> Using it directly from this repo instead? Set `"plugins": ["./typescript.grit"]` and point the path at the
|
|
189
|
+
> checked-out file.
|
|
190
|
+
|
|
191
|
+
## Try it
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
npm install
|
|
195
|
+
npx @biomejs/biome lint example.ts
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Tests
|
|
199
|
+
|
|
200
|
+
Snapshot tests live in [tests/](tests/). Each case is a pair: `tests/fixtures/<name>.ts` (the source to lint)
|
|
201
|
+
and `<name>.expected.json` (the diagnostics it should produce, as an order-independent array of
|
|
202
|
+
`{ "line": <number>, "rule": "<slug>" }`). The runner ([scripts/run-tests.mjs](scripts/run-tests.mjs)) runs
|
|
203
|
+
`biome lint --reporter=json` on each fixture with only the plugin enabled and compares the extracted
|
|
204
|
+
diagnostics against the expectation.
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npm test
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Every rule has a flagged fixture and a safe counterpart, covering the exempt cases (`as const`,
|
|
211
|
+
`Object.hasOwn`, seeded/`Map` accumulators, `const`-object enums, literal-key deletes, and comparator sorts).
|
|
212
|
+
|
|
213
|
+
## How it works
|
|
214
|
+
|
|
215
|
+
The plugin is one Biome GritQL file, [typescript.grit](typescript.grit).
|
|
216
|
+
|
|
217
|
+
- `no-as-cast` matches `TsAsExpression(ty = $type)` and excludes `$type <: TsReferenceType(name = \`const\`)`
|
|
218
|
+
so `as const` passes.
|
|
219
|
+
- `no-in-operator` matches `JsInExpression` and `JsForInStatement`.
|
|
220
|
+
- `no-empty-object-accumulator` matches a `reduce`/`reduceRight` call whose second argument is a
|
|
221
|
+
`JsObjectExpression` with an empty member list (`$members <: []`).
|
|
222
|
+
- `no-enum` matches `TsEnumDeclaration` (covers both `enum` and `const enum`).
|
|
223
|
+
- `no-dynamic-delete` matches `delete $target` where `$target` is a `JsComputedMemberExpression` whose key is
|
|
224
|
+
not a string or number literal.
|
|
225
|
+
- `require-array-sort-compare` matches a `sort`/`toSorted` call with an empty argument list (`$args <: []`).
|
|
226
|
+
|
|
227
|
+
## Releasing
|
|
228
|
+
|
|
229
|
+
Versions and the changelog are managed with [Changesets](https://github.com/changesets/changesets).
|
|
230
|
+
|
|
231
|
+
1. Add a changeset describing a change: `npx changeset`.
|
|
232
|
+
2. Commit the changeset to your branch.
|
|
233
|
+
3. On merge to `main`, the [Release workflow](.github/workflows/release.yml) opens a "Version Packages" pull
|
|
234
|
+
request that bumps the version and updates `CHANGELOG.md`.
|
|
235
|
+
4. Merge that PR and the workflow publishes the new version to npm.
|
|
236
|
+
|
|
237
|
+
The workflow needs an `NPM_TOKEN` secret in the repo. CI runs the test suite on every push and pull request
|
|
238
|
+
([.github/workflows/ci.yml](.github/workflows/ci.yml)).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
Inspired by [`biome-plugin-no-type-assertion`](https://github.com/albertodeago/biome-plugin-no-type-assertion)
|
|
243
|
+
and the GritQL rules in [felixarntz/biome](https://github.com/felixarntz/biome).
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "biome-typescript-best-practices-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Biome plugin that enforces TypeScript best practices — no `as` assertions, no `in` operator, no empty-object reduce accumulators, no enums, no dynamic delete, and safe array sorting.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "Ivan Stepanian <iv.stpn@gmail.com>",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"biome",
|
|
10
|
+
"biome-plugin",
|
|
11
|
+
"gritql",
|
|
12
|
+
"lint",
|
|
13
|
+
"typescript",
|
|
14
|
+
"best-practices",
|
|
15
|
+
"type-safety"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/iv-stpn/biome-typescript-best-practices-plugin.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/iv-stpn/biome-typescript-best-practices-plugin#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/iv-stpn/biome-typescript-best-practices-plugin/issues"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"typescript.grit",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node scripts/run-tests.mjs",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"version": "changeset version",
|
|
34
|
+
"release": "changeset publish"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "2.5.2",
|
|
41
|
+
"@changesets/cli": "2.31.0",
|
|
42
|
+
"@types/node": "^26",
|
|
43
|
+
"typescript": "^6"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/typescript.grit
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js
|
|
3
|
+
|
|
4
|
+
// biome-typescript-best-practices-plugin: TypeScript safety & clarity rules.
|
|
5
|
+
//
|
|
6
|
+
// A set of best-practice checks that are NOT part of Biome's default/recommended
|
|
7
|
+
// linter but that push TypeScript towards safer, clearer code. Every rule reports
|
|
8
|
+
// a diagnostic only (no auto-fix): the correct rewrite is context-specific, so the
|
|
9
|
+
// plugin flags the hazard and leaves the fix to the author.
|
|
10
|
+
//
|
|
11
|
+
// Rules:
|
|
12
|
+
// ts/no-as-cast — `expr as T` type assertions (bypasses the checker).
|
|
13
|
+
// ts/no-in-operator — the `in` operator and `for...in` (prototype-chain traps).
|
|
14
|
+
// ts/no-empty-object-accumulator — `reduce(..., {})` with a plain-object accumulator.
|
|
15
|
+
// ts/no-enum — `enum` / `const enum` declarations.
|
|
16
|
+
// ts/no-dynamic-delete — `delete obj[expr]` with a computed, non-literal key.
|
|
17
|
+
// ts/require-array-sort-compare — `.sort()` / `.toSorted()` with no comparator.
|
|
18
|
+
//
|
|
19
|
+
// Inspired by biome-plugin-no-type-assertion and felixarntz/biome's GritQL rules.
|
|
20
|
+
|
|
21
|
+
// A `.sort` / `.toSorted` member name.
|
|
22
|
+
pattern sort_method_name() {
|
|
23
|
+
or { `sort`, `toSorted` }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
or {
|
|
27
|
+
// Rule: ts/no-as-cast
|
|
28
|
+
// Bad: const el = document.getElementById("x") as HTMLInputElement;
|
|
29
|
+
// Good: use a type guard, a runtime check, or `satisfies` for validation.
|
|
30
|
+
// `as` assertions silence the type checker and can mask real bugs. `as const`
|
|
31
|
+
// is a legitimate literal-narrowing form and is left alone.
|
|
32
|
+
TsAsExpression(ty = $type) as $node where {
|
|
33
|
+
not $type <: TsReferenceType(name = `const`),
|
|
34
|
+
register_diagnostic(
|
|
35
|
+
span = $node,
|
|
36
|
+
message = "[ts/no-as-cast] Avoid `as` type assertions — they bypass the type checker and can hide real type errors. Use a type guard, a runtime check, or `satisfies` to validate a value. (`as const` is allowed.)",
|
|
37
|
+
severity = "warn"
|
|
38
|
+
)
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Rule: ts/no-in-operator
|
|
42
|
+
// Bad: if ("id" in obj) { ... } for (const k in obj) { ... }
|
|
43
|
+
// Good: if (Object.hasOwn(obj, "id")) for (const k of Object.keys(obj))
|
|
44
|
+
// `in` walks the prototype chain, so inherited/polluted keys match too, and it
|
|
45
|
+
// gives no type narrowing you can rely on across shapes.
|
|
46
|
+
or {
|
|
47
|
+
JsInExpression() as $node where {
|
|
48
|
+
register_diagnostic(
|
|
49
|
+
span = $node,
|
|
50
|
+
message = "[ts/no-in-operator] Avoid the `in` operator — it walks the prototype chain and matches inherited keys. Use Object.hasOwn(obj, key) for an own-property check.",
|
|
51
|
+
severity = "warn"
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
JsForInStatement() as $node where {
|
|
55
|
+
register_diagnostic(
|
|
56
|
+
span = $node,
|
|
57
|
+
message = "[ts/no-in-operator] Avoid `for...in` — it iterates inherited enumerable keys. Use `for (const k of Object.keys(obj))` (or Object.entries) instead.",
|
|
58
|
+
severity = "warn"
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Rule: ts/no-empty-object-accumulator
|
|
64
|
+
// Bad: items.reduce((acc, x) => { acc[x.k] = x.v; return acc; }, {})
|
|
65
|
+
// Good: new Map(items.map((x) => [x.k, x.v])) or Object.create(null)
|
|
66
|
+
// A plain `{}` accumulator carries Object.prototype, so dynamic keys like
|
|
67
|
+
// "__proto__" / "constructor" leak into aggregation. A Map (or a null-proto
|
|
68
|
+
// object) has no such surface.
|
|
69
|
+
JsCallExpression(
|
|
70
|
+
callee = JsStaticMemberExpression(member = $method),
|
|
71
|
+
arguments = JsCallArguments(args = $args)
|
|
72
|
+
) as $call where {
|
|
73
|
+
$method <: or { `reduce`, `reduceRight` },
|
|
74
|
+
$args <: [$_, $init],
|
|
75
|
+
$init <: JsObjectExpression(members = $members),
|
|
76
|
+
$members <: [],
|
|
77
|
+
register_diagnostic(
|
|
78
|
+
span = $call,
|
|
79
|
+
message = "[ts/no-empty-object-accumulator] Avoid a `{}` accumulator in reduce — its prototype chain lets keys like \"__proto__\" leak into dynamic-key aggregation. Use `new Map()` or `Object.create(null)`.",
|
|
80
|
+
severity = "warn"
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Rule: ts/no-enum
|
|
85
|
+
// Bad: enum Color { Red, Green } const enum Dir { Up, Down }
|
|
86
|
+
// Good: const Color = { Red: "red", Green: "green" } as const;
|
|
87
|
+
// type Color = (typeof Color)[keyof typeof Color];
|
|
88
|
+
// `enum` emits runtime code, has surprising numeric/string semantics, and
|
|
89
|
+
// `const enum` breaks under isolatedModules. A `const` object + union type is
|
|
90
|
+
// safer and erasable.
|
|
91
|
+
TsEnumDeclaration() as $node where {
|
|
92
|
+
register_diagnostic(
|
|
93
|
+
span = $node,
|
|
94
|
+
message = "[ts/no-enum] Avoid `enum` — it emits runtime code and has surprising semantics (and `const enum` breaks isolatedModules). Prefer a `const` object with `as const` plus a derived union type.",
|
|
95
|
+
severity = "warn"
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Rule: ts/no-dynamic-delete
|
|
100
|
+
// Bad: delete obj[key]; delete obj[getKey()];
|
|
101
|
+
// Good: model the field as optional (`v?: T`) or use a Map and `.delete(key)`.
|
|
102
|
+
// Deleting a computed, non-literal key deoptimises the object's shape and often
|
|
103
|
+
// signals that a Map was the right structure. Literal keys are left alone.
|
|
104
|
+
`delete $target` as $node where {
|
|
105
|
+
$target <: JsComputedMemberExpression(member = $key),
|
|
106
|
+
not $key <: or { JsStringLiteralExpression(), JsNumberLiteralExpression() },
|
|
107
|
+
register_diagnostic(
|
|
108
|
+
span = $node,
|
|
109
|
+
message = "[ts/no-dynamic-delete] Avoid `delete` with a computed key — it deoptimises the object shape. Model the property as optional, or use a Map with `.delete(key)`.",
|
|
110
|
+
severity = "warn"
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Rule: ts/require-array-sort-compare
|
|
115
|
+
// Bad: [3, 20, 100].sort() // → [100, 20, 3] (lexicographic!)
|
|
116
|
+
// Good: [3, 20, 100].sort((a, b) => a - b)
|
|
117
|
+
// `Array#sort` / `toSorted` with no comparator sorts by UTF-16 code unit, so
|
|
118
|
+
// numbers come out in the wrong order. Always pass an explicit comparator.
|
|
119
|
+
JsCallExpression(
|
|
120
|
+
callee = JsStaticMemberExpression(member = $method),
|
|
121
|
+
arguments = JsCallArguments(args = $args)
|
|
122
|
+
) as $call where {
|
|
123
|
+
$method <: sort_method_name(),
|
|
124
|
+
$args <: [],
|
|
125
|
+
register_diagnostic(
|
|
126
|
+
span = $call,
|
|
127
|
+
message = "[ts/require-array-sort-compare] `.sort()` / `.toSorted()` with no comparator sorts lexicographically, so numbers sort wrong (e.g. [3,20,100] → [100,20,3]). Pass a compare function, e.g. .sort((a, b) => a - b).",
|
|
128
|
+
severity = "warn"
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|