@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
- An ESLint plugin that 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.
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 (flat config)
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
- | Rule | Description |
72
- | ----------------------- | ------------------------------------------------------------------------ |
73
- | `prefer-match-on-union` | Warn on `if (x === 'literal')` when `x` has a string-literal union type. |
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;AA4BtD,eAAO,MAAM,kBAAkB;;CA2C7B,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 isStringLiteralUnion(type) {
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
- if (constituents.length < 2)
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;IACzC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;QAAE,OAAO,KAAK,CAAA;IACjC,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAA;IAC/B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACzC,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,SAAS,oBAAoB,CAC3B,IAA+B;IAE/B,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO,IAAI,CAAA;IAClD,MAAM,eAAe,GACnB,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAA;IAC3D,MAAM,gBAAgB,GACpB,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAA;IAC7D,IAAI,eAAe,IAAI,CAAC,gBAAgB;QAAE,OAAO,KAAK,CAAA;IACtD,IAAI,gBAAgB,IAAI,CAAC,eAAe;QAAE,OAAO,IAAI,CAAA;IACrD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,UAAU,CAAiB;IAC3D,IAAI,EAAE,uBAAuB;IAC7B,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,qIAAqI;SACxI;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,WAAW,EACT,gKAAgK;SACnK;KACF;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,QAAQ,GAAG,WAAW,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;QACvD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAA;QAEjD,SAAS,KAAK,CAAC,IAAyB;YACtC,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB;gBAAE,OAAM;YAC5C,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK;gBAAE,OAAM;YAE9D,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;YACzC,IAAI,CAAC,MAAM;gBAAE,OAAM;YAEnB,MAAM,MAAM,GAAG,QAAQ,CAAC,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACzD,MAAM,IAAI,GAAG,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAA;YAE9C,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC;gBAAE,OAAM;YAEvC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAA;QAC1D,CAAC;QAED,OAAO;YACL,WAAW,CAAC,IAAI;gBACd,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClB,CAAC;YACD,qBAAqB,CAAC,IAAI;gBACxB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClB,CAAC;SACF,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
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.1",
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
- "packageManager": "pnpm@11.0.9"
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
+ }