@danilqa/eslint-plugin-ts-pattern 0.0.1 → 0.0.2
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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @danilqa/eslint-plugin-ts-pattern
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Warns when you branch on a string-literal union type with `if`/`else`, and points you at [`ts-pattern`](https://github.com/gvergnaud/ts-pattern)'s exhaustive `match` instead.
|
|
4
4
|
|
|
5
5
|
## Problem
|
|
6
6
|
|
|
@@ -12,13 +12,18 @@ interface Payment {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function describe(payment: Payment) {
|
|
15
|
+
// Case 1.
|
|
16
|
+
// When "State" later grows a "refunded" variant, the compiler does not flag this "if". The branch silently misses
|
|
17
|
+
// the new case — and so does every other `if` block scattered across the codebase.
|
|
15
18
|
if (payment.state === 'failed') return 'a'
|
|
16
|
-
|
|
19
|
+
|
|
20
|
+
// Case 2.
|
|
21
|
+
// We implicitly convert union to "boolean" type instead of covering all cases now and in the future. Added 'refunded'?
|
|
22
|
+
// It will be implicitly mached to "b" and we won't notice.
|
|
23
|
+
return payment.state === 'failed' ? 'a' : 'b'
|
|
17
24
|
}
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
When `State` later grows a `'refunded'` variant, the compiler does not flag this `if`. The branch silently misses the new case — and so does every other `if` block scattered across the codebase.
|
|
21
|
-
|
|
22
27
|
## Solution
|
|
23
28
|
|
|
24
29
|
```ts
|
|
@@ -49,7 +54,7 @@ yarn add -D @danilqa/eslint-plugin-ts-pattern
|
|
|
49
54
|
pnpm add -D @danilqa/eslint-plugin-ts-pattern
|
|
50
55
|
```
|
|
51
56
|
|
|
52
|
-
## Usage
|
|
57
|
+
## Usage
|
|
53
58
|
|
|
54
59
|
```js
|
|
55
60
|
import tsPattern from '@danilqa/eslint-plugin-ts-pattern'
|
|
@@ -68,6 +73,24 @@ export default [
|
|
|
68
73
|
|
|
69
74
|
## Rules
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
```ts
|
|
77
|
+
type State = 'failed' | 'success' | 'pending'
|
|
78
|
+
let state: State
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Case | Example | Fires |
|
|
82
|
+
| --------------------------------------------------- | ------------------------------------- | :---: |
|
|
83
|
+
| String-literal union, `===` with literal | `if (state === 'failed') {}` | ✅ |
|
|
84
|
+
| String-literal union, `!==` with literal | `if (state !== 'failed') {}` | ✅ |
|
|
85
|
+
| Literal on the left side | `if ('failed' === state) {}` | ✅ |
|
|
86
|
+
| Ternary on a string-literal union | `state === 'failed' ? 1 : 0` | ✅ |
|
|
87
|
+
| Member access into a union property | `if (payment.state === 'failed') {}` | ✅ |
|
|
88
|
+
| Optional chain on non-nullable receiver | `if (payment?.state === 'failed') {}` | ✅ |
|
|
89
|
+
| Optional / nullable property (`State \| undefined`) | `if (payment.state === 'failed') {}` | ✅ |
|
|
90
|
+
| Plain `string` operand | `if (s === 'hi') {}` | ❌ |
|
|
91
|
+
| Single-member literal type (`'only'`) | `if (x === 'only') {}` | ❌ |
|
|
92
|
+
| Number- or boolean-literal union (`1 \| 2`) | `if (n === 1) {}` | ❌ |
|
|
93
|
+
| Mixed-type union (`'a' \| number`) | `if (m === 'a') {}` | ❌ |
|
|
94
|
+
| Loose equality (`==` / `!=`) | `if (s == 'failed') {}` | ❌ |
|
|
95
|
+
| Both operands are non-literal | `if (a === b) {}` | ❌ |
|
|
96
|
+
| `switch` statement | `switch (s) { case 'failed': … }` | ❌ |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-match-on-union.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-match-on-union.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"prefer-match-on-union.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-match-on-union.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAkCtD,eAAO,MAAM,kBAAkB;;CA2C7B,CAAA"}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
2
|
import { createRule } from '../utils/create-rule.js';
|
|
3
|
-
function
|
|
3
|
+
function isNullish(type, checker) {
|
|
4
|
+
const printed = checker.typeToString(type);
|
|
5
|
+
return printed === 'null' || printed === 'undefined';
|
|
6
|
+
}
|
|
7
|
+
function isStringLiteralUnion(type, checker) {
|
|
4
8
|
if (!type.isUnion())
|
|
5
9
|
return false;
|
|
6
10
|
const constituents = type.types;
|
|
7
|
-
|
|
11
|
+
const literals = constituents.filter((t) => t.isStringLiteral());
|
|
12
|
+
if (literals.length < 2)
|
|
8
13
|
return false;
|
|
9
|
-
return constituents.every((t) => t.isStringLiteral());
|
|
14
|
+
return constituents.every((t) => t.isStringLiteral() || isNullish(t, checker));
|
|
10
15
|
}
|
|
11
16
|
function getNonLiteralOperand(node) {
|
|
12
17
|
const { left, right } = node;
|
|
@@ -29,7 +34,7 @@ export const preferMatchOnUnion = createRule({
|
|
|
29
34
|
},
|
|
30
35
|
schema: [],
|
|
31
36
|
messages: {
|
|
32
|
-
preferMatch: 'Avoid `===`/`!==` checks on string-literal union types. Use `match(value).with(...).exhaustive()` from ts-pattern so missing cases are caught at compile time.',
|
|
37
|
+
preferMatch: 'Avoid `===`/`!==` checks on string-literal union types. Use `match(value).with(...).exhaustive()` from ts-pattern so missing cases are caught at compile time. Use .otherwise() for dynamic backend types. Read more: https://github.com/Danilqa/eslint-plugin-ts-pattern',
|
|
33
38
|
},
|
|
34
39
|
},
|
|
35
40
|
defaultOptions: [],
|
|
@@ -46,7 +51,7 @@ export const preferMatchOnUnion = createRule({
|
|
|
46
51
|
return;
|
|
47
52
|
const tsNode = services.esTreeNodeToTSNodeMap.get(target);
|
|
48
53
|
const type = checker.getTypeAtLocation(tsNode);
|
|
49
|
-
if (!isStringLiteralUnion(type))
|
|
54
|
+
if (!isStringLiteralUnion(type, checker))
|
|
50
55
|
return;
|
|
51
56
|
context.report({ node: test, messageId: 'preferMatch' });
|
|
52
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-match-on-union.js","sourceRoot":"","sources":["../../src/rules/prefer-match-on-union.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGtD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAIjD,SAAS,oBAAoB,CAAC,IAAa;
|
|
1
|
+
{"version":3,"file":"prefer-match-on-union.js","sourceRoot":"","sources":["../../src/rules/prefer-match-on-union.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danilqa/eslint-plugin-ts-pattern",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "ESLint plugin: warn when `if` is used on string-literal union types instead of ts-pattern's exhaustive `match`",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"eslint",
|
|
8
|
+
"eslint-plugin",
|
|
9
|
+
"eslintplugin",
|
|
10
|
+
"ts-pattern",
|
|
11
|
+
"pattern-matching",
|
|
12
|
+
"pattern-match",
|
|
13
|
+
"match",
|
|
14
|
+
"exhaustive",
|
|
15
|
+
"exhaustiveness",
|
|
16
|
+
"discriminated-union",
|
|
17
|
+
"union-types",
|
|
18
|
+
"string-literal-union",
|
|
19
|
+
"typescript",
|
|
20
|
+
"ts",
|
|
21
|
+
"type-safety",
|
|
22
|
+
"linter",
|
|
23
|
+
"lint"
|
|
24
|
+
],
|
|
6
25
|
"publishConfig": {
|
|
7
26
|
"access": "public"
|
|
8
27
|
},
|
|
@@ -22,19 +41,6 @@
|
|
|
22
41
|
"engines": {
|
|
23
42
|
"node": ">=20"
|
|
24
43
|
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths",
|
|
27
|
-
"typecheck": "tsc --noEmit",
|
|
28
|
-
"test": "node --import tsx --test 'tests/**/*.test.ts'",
|
|
29
|
-
"test:watch": "node --import tsx --test --watch 'tests/**/*.test.ts'",
|
|
30
|
-
"lint": "eslint .",
|
|
31
|
-
"lint:fix": "eslint . --fix",
|
|
32
|
-
"format": "prettier --write .",
|
|
33
|
-
"format:check": "prettier --check .",
|
|
34
|
-
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test",
|
|
35
|
-
"prepare": "husky",
|
|
36
|
-
"prepublishOnly": "pnpm check && pnpm build"
|
|
37
|
-
},
|
|
38
44
|
"peerDependencies": {
|
|
39
45
|
"eslint": ">=9",
|
|
40
46
|
"typescript": ">=5"
|
|
@@ -53,5 +59,15 @@
|
|
|
53
59
|
"typescript": "6.0.3",
|
|
54
60
|
"typescript-eslint": "8.59.2"
|
|
55
61
|
},
|
|
56
|
-
"
|
|
57
|
-
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "node --import tsx --test 'tests/**/*.test.ts'",
|
|
66
|
+
"test:watch": "node --import tsx --test --watch 'tests/**/*.test.ts'",
|
|
67
|
+
"lint": "eslint .",
|
|
68
|
+
"lint:fix": "eslint . --fix",
|
|
69
|
+
"format": "prettier --write .",
|
|
70
|
+
"format:check": "prettier --check .",
|
|
71
|
+
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test"
|
|
72
|
+
}
|
|
73
|
+
}
|