eslint-plugin-ts-data-forge 0.8.5 → 0.9.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.
Files changed (135) hide show
  1. package/README.md +20 -19
  2. package/dist/plugin.d.mts +2 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +16 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -34
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +53 -58
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  22. package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
  23. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  25. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  26. package/dist/rules/prefer-arr-generate.mjs +588 -0
  27. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  28. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  29. package/dist/rules/prefer-arr-is-array.mjs +7 -5
  30. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  31. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  32. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +9 -6
  33. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  34. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  35. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +9 -6
  36. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  37. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -6
  39. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -6
  42. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  44. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -6
  45. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  46. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  47. package/dist/rules/prefer-arr-scan.mjs +8 -11
  48. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  49. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  50. package/dist/rules/prefer-arr-sum.mjs +8 -6
  51. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  52. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  53. package/dist/rules/prefer-arr-uniq.mjs +12 -166
  54. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  55. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  56. package/dist/rules/prefer-as-int.mjs +6 -8
  57. package/dist/rules/prefer-as-int.mjs.map +1 -1
  58. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  59. package/dist/rules/prefer-canonical-array-slicing.mjs +57 -34
  60. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  61. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  62. package/dist/rules/prefer-canonical-length-cast.mjs +3 -3
  63. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  64. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  65. package/dist/rules/prefer-canonical-length-guard.mjs +4 -4
  66. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  67. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  68. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +3 -2
  69. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  70. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  71. package/dist/rules/prefer-is-non-null-object.mjs +113 -43
  72. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  73. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  74. package/dist/rules/prefer-is-record-and-has-key.mjs +105 -48
  75. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  77. package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
  78. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  80. package/dist/rules/prefer-num-safe-parse-int.mjs +6 -6
  81. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +9 -12
  84. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  86. package/dist/rules/prefer-range-for-loop.mjs +104 -23
  87. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  88. package/dist/rules/rules.d.mts +1 -0
  89. package/dist/rules/rules.d.mts.map +1 -1
  90. package/dist/rules/rules.mjs +2 -0
  91. package/dist/rules/rules.mjs.map +1 -1
  92. package/package.json +11 -10
  93. package/src/plugin.mts +1 -0
  94. package/src/rule-types.mts +17 -1
  95. package/src/rules/ast-utils.mts +73 -80
  96. package/src/rules/constants.mts +12 -0
  97. package/src/rules/mutable-result.mts +204 -0
  98. package/src/rules/no-unnecessary-type-guard.mts +14 -10
  99. package/src/rules/prefer-arr-generate.mts +918 -0
  100. package/src/rules/prefer-arr-generate.test.mts +967 -0
  101. package/src/rules/prefer-arr-is-array.mts +14 -7
  102. package/src/rules/prefer-arr-is-array.test.mts +14 -0
  103. package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
  104. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  105. package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
  106. package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
  107. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  108. package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
  109. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  110. package/src/rules/prefer-arr-is-non-empty.mts +16 -13
  111. package/src/rules/prefer-arr-scan.mts +24 -19
  112. package/src/rules/prefer-arr-sum.mts +18 -14
  113. package/src/rules/prefer-arr-uniq.mts +25 -234
  114. package/src/rules/prefer-as-int.mts +19 -12
  115. package/src/rules/prefer-canonical-array-slicing.mts +79 -52
  116. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  117. package/src/rules/prefer-canonical-length-cast.mts +7 -3
  118. package/src/rules/prefer-canonical-length-guard.mts +9 -4
  119. package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
  120. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
  121. package/src/rules/prefer-is-non-null-object.mts +171 -64
  122. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  123. package/src/rules/prefer-is-record-and-has-key.mts +159 -74
  124. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  125. package/src/rules/prefer-num-safe-parse-float.mts +15 -10
  126. package/src/rules/prefer-num-safe-parse-int.mts +15 -13
  127. package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
  128. package/src/rules/prefer-range-for-loop.mts +157 -31
  129. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  130. package/src/rules/rules.mts +2 -0
  131. package/dist/rules/import-utils.d.mts +0 -30
  132. package/dist/rules/import-utils.d.mts.map +0 -1
  133. package/dist/rules/import-utils.mjs +0 -98
  134. package/dist/rules/import-utils.mjs.map +0 -1
  135. package/src/rules/import-utils.mts +0 -175
@@ -675,6 +675,61 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
675
675
  { messageId: 'preferCanonicalArraySlicing' },
676
676
  ],
677
677
  },
678
+ {
679
+ name: 'a filter callback whose body is wrapped',
680
+ code: dedent`
681
+ declare const xs: readonly number[];
682
+ const ys = xs.filter((_, i) => (i >= 1) satisfies boolean);
683
+ `,
684
+ output: dedent`
685
+ import { Arr } from 'ts-data-forge';
686
+ declare const xs: readonly number[];
687
+ const ys = Arr.tail(xs);
688
+ `,
689
+ errors: [{ messageId: 'preferCanonicalArraySlicing' }],
690
+ },
691
+ {
692
+ name: 'a wrapped `xs.length - N`, and a wrapped `.length` or receiver inside it',
693
+ code: dedent`
694
+ declare const xs: readonly number[];
695
+ const a = xs.slice(0, (xs.length - 1) as number);
696
+ const b = xs.slice(0, (xs as readonly number[]).length - 1);
697
+ const c = xs.filter((_, i) => i < (xs.length satisfies number) - 2);
698
+ const d = xs.toSpliced(0, xs!.length - 2);
699
+ `,
700
+ output: dedent`
701
+ import { Arr } from 'ts-data-forge';
702
+ declare const xs: readonly number[];
703
+ const a = Arr.butLast(xs);
704
+ const b = Arr.butLast(xs);
705
+ const c = Arr.skipLast(xs, 2);
706
+ const d = Arr.takeLast(xs, 2);
707
+ `,
708
+ errors: [
709
+ { messageId: 'preferCanonicalArraySlicing' },
710
+ { messageId: 'preferCanonicalArraySlicing' },
711
+ { messageId: 'preferCanonicalArraySlicing' },
712
+ { messageId: 'preferCanonicalArraySlicing' },
713
+ ],
714
+ },
715
+ {
716
+ name: 'a wrapped receiver compared with its own `.length`',
717
+ code: dedent`
718
+ declare const xs: readonly number[];
719
+ const a = (xs satisfies readonly number[]).slice(0, xs.length - 1);
720
+ const b = xs.toSpliced((xs.length as number), 0, 1);
721
+ `,
722
+ output: dedent`
723
+ import { Arr } from 'ts-data-forge';
724
+ declare const xs: readonly number[];
725
+ const a = Arr.butLast(xs satisfies readonly number[]);
726
+ const b = Arr.toPushed(xs, 1);
727
+ `,
728
+ errors: [
729
+ { messageId: 'preferCanonicalArraySlicing' },
730
+ { messageId: 'preferCanonicalArraySlicing' },
731
+ ],
732
+ },
678
733
  ],
679
734
  });
680
735
  });
@@ -4,8 +4,12 @@ import {
4
4
  type TSESLint,
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
- import { skipTypeWrappers } from './ast-utils.mjs';
8
- import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
7
+ import {
8
+ getImportDeclarations,
9
+ getImportedLocalName,
10
+ skipTypeWrappers,
11
+ } from 'eslint-rule-forge';
12
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
9
13
 
10
14
  type Options = readonly [];
11
15
 
@@ -143,7 +147,7 @@ export const preferCanonicalLengthCast: TSESLint.RuleModule<
143
147
  const sourceCode = context.sourceCode;
144
148
 
145
149
  const arrLocalName = getImportedLocalName(
146
- getTsDataForgeImport(sourceCode.ast),
150
+ getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
147
151
  'Arr',
148
152
  );
149
153
 
@@ -3,9 +3,14 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
+ import {
7
+ getArgumentText,
8
+ getImportDeclarations,
9
+ getImportedLocalName,
10
+ skipTypeWrappers,
11
+ } from 'eslint-rule-forge';
6
12
  import type { MutableRecord, ReadonlyRecord } from 'ts-type-forge';
7
- import { skipTypeWrappers, toArgumentText } from './ast-utils.mjs';
8
- import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
13
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
9
14
  import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
10
15
  import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
11
16
  import { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';
@@ -117,7 +122,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
117
122
  const sourceCode = context.sourceCode;
118
123
 
119
124
  const arrLocalName = getImportedLocalName(
120
- getTsDataForgeImport(sourceCode.ast),
125
+ getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
121
126
  'Arr',
122
127
  );
123
128
 
@@ -154,7 +159,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
154
159
  return;
155
160
  }
156
161
 
157
- const arrayText = toArgumentText(array, sourceCode);
162
+ const arrayText = getArgumentText(sourceCode, array);
158
163
 
159
164
  context.report({
160
165
  node,
@@ -3,8 +3,9 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
+ import { buildCalleeResolver, getImportDeclarations } from 'eslint-rule-forge';
6
7
  import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
7
- import { buildCalleeResolver, getTsDataForgeImport } from './import-utils.mjs';
8
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
8
9
 
9
10
  type Options = readonly [];
10
11
 
@@ -121,7 +122,7 @@ export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
121
122
  const sourceCode = context.sourceCode;
122
123
 
123
124
  const resolveGuard = buildCalleeResolver(
124
- getTsDataForgeImport(sourceCode.ast),
125
+ getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
125
126
  );
126
127
 
127
128
  return {
@@ -49,6 +49,22 @@ describe('prefer-comparison-over-nullish-guard', () => {
49
49
  },
50
50
  ],
51
51
  invalid: [
52
+ {
53
+ name: 'the guard imported by a second ts-data-forge declaration',
54
+ code: dedent`
55
+ import type { Optional } from 'ts-data-forge';
56
+ import { isUndefined } from 'ts-data-forge';
57
+ declare const x: string | undefined;
58
+ const y = isUndefined(x);
59
+ `,
60
+ output: dedent`
61
+ import type { Optional } from 'ts-data-forge';
62
+ import { isUndefined } from 'ts-data-forge';
63
+ declare const x: string | undefined;
64
+ const y = x === undefined;
65
+ `,
66
+ errors: [{ messageId: 'preferComparison' }],
67
+ },
52
68
  {
53
69
  name: 'isNotUndefined with explicit argument',
54
70
  code: dedent`
@@ -3,12 +3,15 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { skipTypeWrappers } from './ast-utils.mjs';
7
6
  import {
8
- buildImportFixes,
7
+ buildNamedImportFix,
8
+ getImportDeclarations,
9
9
  getNamedImports,
10
- getTsDataForgeImport,
11
- } from './import-utils.mjs';
10
+ getRangeWithParens,
11
+ parenthesesDepth,
12
+ skipTypeWrappers,
13
+ } from 'eslint-rule-forge';
14
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
12
15
 
13
16
  type Options = readonly [];
14
17
 
@@ -19,7 +22,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
19
22
  type: 'suggestion',
20
23
  docs: {
21
24
  description:
22
- 'Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-data-forge.',
25
+ 'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-data-forge.',
23
26
  },
24
27
  fixable: 'code',
25
28
  schema: [],
@@ -34,51 +37,55 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
34
37
 
35
38
  const program = sourceCode.ast;
36
39
 
37
- const tsDataForgeImport = getTsDataForgeImport(program);
40
+ const tsDataForgeImports = getImportDeclarations(
41
+ program,
42
+ TS_DATA_FORGE_MODULE,
43
+ );
38
44
 
39
- const mut_nodesToFix: {
40
- node: TSESTree.LogicalExpression;
41
- identifierName: string;
42
- }[] = [];
45
+ const mut_pairsToFix: NonNullObjectPair[] = [];
43
46
 
44
47
  return {
45
48
  LogicalExpression: (node) => {
46
- const identifierName = getNonNullObjectIdentifierName(node);
47
-
48
- if (identifierName === undefined) {
49
+ // A chain is read once, from its outermost `&&`.
50
+ if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
49
51
  return;
50
52
  }
51
53
 
52
- mut_nodesToFix.push({ node, identifierName });
54
+ mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
53
55
  },
54
56
  'Program:exit': () => {
55
- // Check if isNonNullObject is already imported
56
57
  const hasIsNonNullObjectImport =
57
- getNamedImports(tsDataForgeImport).includes('isNonNullObject');
58
+ getNamedImports(tsDataForgeImports).includes('isNonNullObject');
58
59
 
59
- // Note: We add import only for the first node to avoid conflicts when
60
- // multiple fixes try to insert at the same position. This means if the
61
- // first node is disabled via eslint-disable comment, no import will be
62
- // added.
60
+ // The import fix lives on the first report only:
61
+ // `insertTextBefore(program, …)` produces overlapping ranges
62
+ // otherwise and only one of them would survive a pass. A first report
63
+ // silenced by an `eslint-disable` comment therefore takes the import
64
+ // with it.
63
65
  for (const [
64
66
  index,
65
- { node, identifierName },
66
- ] of mut_nodesToFix.entries()) {
67
+ { range, identifierName },
68
+ ] of mut_pairsToFix.entries()) {
67
69
  context.report({
68
- node,
70
+ loc: {
71
+ start: sourceCode.getLocFromIndex(range[0]),
72
+ end: sourceCode.getLocFromIndex(range[1]),
73
+ },
69
74
  messageId: 'useIsNonNullObject',
70
75
  fix: (fixer) => {
71
76
  const replacement = `isNonNullObject(${identifierName})`;
72
77
 
73
- // Add import only for the first node and only if not already imported
74
78
  const importFixes =
75
79
  index === 0 && !hasIsNonNullObjectImport
76
- ? buildImportFixes(fixer, program, tsDataForgeImport, [
80
+ ? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
77
81
  'isNonNullObject',
78
82
  ])
79
83
  : [];
80
84
 
81
- return [...importFixes, fixer.replaceText(node, replacement)];
85
+ return [
86
+ ...importFixes,
87
+ fixer.replaceTextRange(range, replacement),
88
+ ];
82
89
  },
83
90
  });
84
91
  }
@@ -89,39 +96,123 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
89
96
  } as const;
90
97
 
91
98
  /**
92
- * The name of the value checked by `typeof u === 'object' && u !== null`, or
93
- * `undefined`. Every operand is read through type wrappers:
94
- * `(u satisfies unknown) !== null` and `u! !== null` check the same `u`.
99
+ * A `typeof u === 'object'` and a `u !== null` next to each other in an `&&`
100
+ * chain, as the source range from the first to the second (parentheses
101
+ * included) and the name of `u`.
95
102
  */
96
- const getNonNullObjectIdentifierName = (
97
- // AST nodes hold mutable child arrays, so they are not deeply readonly.
103
+ type NonNullObjectPair = Readonly<{
104
+ range: TSESTree.Range;
105
+ identifierName: string;
106
+ }>;
107
+
108
+ /**
109
+ * Whether `node` is an operand of an enclosing `&&` chain: a `&&` written
110
+ * directly under another one, without parentheses.
111
+ */
112
+ const isChainOperand = (
113
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
114
+ sourceCode: TSESLint.SourceCode,
98
115
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
99
116
  node: TSESTree.LogicalExpression,
100
- ): string | undefined => {
101
- if (node.operator !== '&&') {
102
- return undefined;
103
- }
117
+ ): boolean =>
118
+ node.parent.type === AST_NODE_TYPES.LogicalExpression &&
119
+ node.parent.operator === '&&' &&
120
+ parenthesesDepth(sourceCode, node) === 0;
104
121
 
105
- const leftIdentifierName = getTypeofObjectIdentifierName(node.left);
122
+ /**
123
+ * Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
124
+ * two halves may come in either order, and the pair may have operands before
125
+ * and after it: `a && u !== null && typeof u === 'object' && b`. They have to
126
+ * be next to each other, so that the pair is one range of the source.
127
+ */
128
+ const findNonNullObjectPairs = (
129
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
130
+ sourceCode: TSESLint.SourceCode,
131
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
132
+ node: TSESTree.LogicalExpression,
133
+ ): readonly NonNullObjectPair[] => {
134
+ const operands = chainOperands(sourceCode, node);
135
+
136
+ const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
137
+ const first = operands[index];
138
+
139
+ const second = operands[index + 1];
106
140
 
107
- const rightIdentifierName = getNonNullCheckIdentifierName(node.right);
141
+ if (first === undefined || second === undefined) {
142
+ return [];
143
+ }
108
144
 
109
- return leftIdentifierName === undefined ||
110
- rightIdentifierName === undefined ||
111
- leftIdentifierName !== rightIdentifierName
112
- ? undefined
113
- : leftIdentifierName;
145
+ const identifierName =
146
+ pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
147
+
148
+ return identifierName === undefined
149
+ ? pairsFrom(index + 1)
150
+ : [
151
+ {
152
+ range: [
153
+ getRangeWithParens(sourceCode, first)[0],
154
+ getRangeWithParens(sourceCode, second)[1],
155
+ ],
156
+ identifierName,
157
+ },
158
+ ...pairsFrom(index + 2),
159
+ ];
160
+ };
161
+
162
+ return pairsFrom(0);
114
163
  };
115
164
 
116
- const getTypeofObjectIdentifierName = (
165
+ /**
166
+ * The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
167
+ * is one operand: its halves cannot be replaced without its parentheses.
168
+ */
169
+ const chainOperands = (
170
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
171
+ sourceCode: TSESLint.SourceCode,
172
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
173
+ node: TSESTree.LogicalExpression,
174
+ ): readonly TSESTree.Expression[] =>
175
+ [node.left, node.right].flatMap((operand) =>
176
+ operand.type === AST_NODE_TYPES.LogicalExpression &&
177
+ operand.operator === '&&' &&
178
+ parenthesesDepth(sourceCode, operand) === 0
179
+ ? chainOperands(sourceCode, operand)
180
+ : [operand],
181
+ );
182
+
183
+ // Every operand below is read through its type wrappers: `typeof (u satisfies
184
+ // unknown) === 'object' && u! !== null` is the same check.
185
+
186
+ /**
187
+ * The name of `u` when `typeofCheck` is `typeof u === 'object'` and
188
+ * `nullCheck` is `u !== null` for the same `u`.
189
+ */
190
+ const pairIdentifierName = (
191
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
192
+ typeofCheck: TSESTree.Expression,
117
193
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
118
- node: TSESTree.Expression,
194
+ nullCheck: TSESTree.Expression,
119
195
  ): string | undefined => {
120
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
121
- return undefined;
122
- }
196
+ const identifierName = getTypeofObjectIdentifierName(
197
+ skipTypeWrappers(typeofCheck),
198
+ );
123
199
 
124
- if (node.operator !== '===') {
200
+ return identifierName !== undefined &&
201
+ identifierName ===
202
+ getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
203
+ ? identifierName
204
+ : undefined;
205
+ };
206
+
207
+ /** `u` of `typeof u === 'object'`, in either operand order. */
208
+ const getTypeofObjectIdentifierName = (
209
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
210
+ node: TSESTree.Node,
211
+ ): string | undefined => {
212
+ if (
213
+ node.type !== AST_NODE_TYPES.BinaryExpression ||
214
+ node.operator !== '==='
215
+ ) {
125
216
  return undefined;
126
217
  }
127
218
 
@@ -129,31 +220,39 @@ const getTypeofObjectIdentifierName = (
129
220
 
130
221
  const right = skipTypeWrappers(node.right);
131
222
 
223
+ const typeofSide = isLiteralOf(right, 'object')
224
+ ? left
225
+ : isLiteralOf(left, 'object')
226
+ ? right
227
+ : undefined;
228
+
132
229
  if (
133
- left.type !== AST_NODE_TYPES.UnaryExpression ||
134
- left.operator !== 'typeof' ||
135
- right.type !== AST_NODE_TYPES.Literal ||
136
- right.value !== 'object'
230
+ typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
231
+ typeofSide.operator !== 'typeof'
137
232
  ) {
138
233
  return undefined;
139
234
  }
140
235
 
141
- const argument = skipTypeWrappers(left.argument);
236
+ const argument = skipTypeWrappers(typeofSide.argument);
142
237
 
143
238
  return argument.type === AST_NODE_TYPES.Identifier
144
239
  ? argument.name
145
240
  : undefined;
146
241
  };
147
242
 
243
+ /**
244
+ * `u` of `u !== null`, in either operand order. `u != null` also excludes
245
+ * `undefined`, which `typeof u === 'object'` has already excluded, so it is
246
+ * the same check here.
247
+ */
148
248
  const getNonNullCheckIdentifierName = (
149
249
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
150
- node: TSESTree.Expression,
250
+ node: TSESTree.Node,
151
251
  ): string | undefined => {
152
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
153
- return undefined;
154
- }
155
-
156
- if (node.operator !== '!==') {
252
+ if (
253
+ node.type !== AST_NODE_TYPES.BinaryExpression ||
254
+ (node.operator !== '!==' && node.operator !== '!=')
255
+ ) {
157
256
  return undefined;
158
257
  }
159
258
 
@@ -161,9 +260,17 @@ const getNonNullCheckIdentifierName = (
161
260
 
162
261
  const right = skipTypeWrappers(node.right);
163
262
 
164
- return left.type !== AST_NODE_TYPES.Identifier ||
165
- right.type !== AST_NODE_TYPES.Literal ||
166
- right.value !== null
167
- ? undefined
168
- : left.name;
263
+ const checked = isLiteralOf(right, null)
264
+ ? left
265
+ : isLiteralOf(left, null)
266
+ ? right
267
+ : undefined;
268
+
269
+ return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
169
270
  };
271
+
272
+ const isLiteralOf = (
273
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
274
+ node: TSESTree.Node,
275
+ value: string | null,
276
+ ): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
@@ -193,6 +193,144 @@ describe('prefer-is-non-null-object through type wrappers', () => {
193
193
  { messageId: 'useIsNonNullObject' },
194
194
  ],
195
195
  },
196
+ {
197
+ name: 'both halves wrapped as a whole',
198
+ code: dedent`
199
+ import { isNonNullObject } from 'ts-data-forge';
200
+ declare const u: unknown;
201
+ const ok =
202
+ ((typeof u === 'object') satisfies boolean) &&
203
+ ((u !== null) as boolean);
204
+ `,
205
+ output: dedent`
206
+ import { isNonNullObject } from 'ts-data-forge';
207
+ declare const u: unknown;
208
+ const ok =
209
+ isNonNullObject(u);
210
+ `,
211
+ errors: [{ messageId: 'useIsNonNullObject' }],
212
+ },
196
213
  ],
197
214
  });
198
215
  }, 20000);
216
+
217
+ describe('prefer-is-non-null-object in other forms', () => {
218
+ tester.run('prefer-is-non-null-object', preferIsNonNullObject, {
219
+ valid: [
220
+ {
221
+ name: 'the two halves joined by `||`',
222
+ code: dedent`
223
+ declare const u: unknown;
224
+ const ok = typeof u === 'object' || u !== null;
225
+ `,
226
+ },
227
+ {
228
+ name: 'the two halves not next to each other in the chain',
229
+ code: dedent`
230
+ declare const u: unknown;
231
+ declare const a: boolean;
232
+ const ok = typeof u === 'object' && a && u !== null;
233
+ `,
234
+ },
235
+ {
236
+ name: 'an undefined check is not a null check',
237
+ code: dedent`
238
+ declare const u: unknown;
239
+ const ok = u !== undefined && typeof u === 'object';
240
+ `,
241
+ },
242
+ {
243
+ name: 'the halves split by parentheses',
244
+ code: dedent`
245
+ declare const u: unknown;
246
+ declare const b: boolean;
247
+ const ok = typeof u === 'object' && (u !== null && b);
248
+ `,
249
+ },
250
+ ],
251
+ invalid: [
252
+ {
253
+ name: 'the halves in reverse order',
254
+ code: dedent`
255
+ declare const u: unknown;
256
+ const ok = u !== null && typeof u === 'object';
257
+ `,
258
+ output: dedent`
259
+ import { isNonNullObject } from 'ts-data-forge';
260
+ declare const u: unknown;
261
+ const ok = isNonNullObject(u);
262
+ `,
263
+ errors: [{ messageId: 'useIsNonNullObject' }],
264
+ },
265
+ {
266
+ name: 'each comparison with its operands swapped',
267
+ code: dedent`
268
+ declare const u: unknown;
269
+ const ok = 'object' === typeof u && null !== u;
270
+ `,
271
+ output: dedent`
272
+ import { isNonNullObject } from 'ts-data-forge';
273
+ declare const u: unknown;
274
+ const ok = isNonNullObject(u);
275
+ `,
276
+ errors: [{ messageId: 'useIsNonNullObject' }],
277
+ },
278
+ {
279
+ name: '`!= null`, which typeof has already made the same check',
280
+ code: dedent`
281
+ declare const u: unknown;
282
+ const ok = typeof u === 'object' && u != null;
283
+ `,
284
+ output: dedent`
285
+ import { isNonNullObject } from 'ts-data-forge';
286
+ declare const u: unknown;
287
+ const ok = isNonNullObject(u);
288
+ `,
289
+ errors: [{ messageId: 'useIsNonNullObject' }],
290
+ },
291
+ {
292
+ name: 'the pair at the start, the end and the middle of a longer chain',
293
+ code: dedent`
294
+ declare const u: unknown;
295
+ declare const a: boolean;
296
+ declare const b: boolean;
297
+ const x = typeof u === 'object' && u !== null && 'k' in u;
298
+ const y = a && typeof u === 'object' && u !== null;
299
+ const z = a && u !== null && (typeof u === 'object') && b;
300
+ `,
301
+ output: dedent`
302
+ import { isNonNullObject } from 'ts-data-forge';
303
+ declare const u: unknown;
304
+ declare const a: boolean;
305
+ declare const b: boolean;
306
+ const x = isNonNullObject(u) && 'k' in u;
307
+ const y = a && isNonNullObject(u);
308
+ const z = a && isNonNullObject(u) && b;
309
+ `,
310
+ errors: [
311
+ { messageId: 'useIsNonNullObject' },
312
+ { messageId: 'useIsNonNullObject' },
313
+ { messageId: 'useIsNonNullObject' },
314
+ ],
315
+ },
316
+ {
317
+ name: 'two pairs in one chain',
318
+ code: dedent`
319
+ declare const u: unknown;
320
+ declare const v: unknown;
321
+ const ok = typeof u === 'object' && u !== null && v !== null && typeof v === 'object';
322
+ `,
323
+ output: dedent`
324
+ import { isNonNullObject } from 'ts-data-forge';
325
+ declare const u: unknown;
326
+ declare const v: unknown;
327
+ const ok = isNonNullObject(u) && isNonNullObject(v);
328
+ `,
329
+ errors: [
330
+ { messageId: 'useIsNonNullObject' },
331
+ { messageId: 'useIsNonNullObject' },
332
+ ],
333
+ },
334
+ ],
335
+ });
336
+ });