eslint-plugin-ts-std-forge 0.1.9 → 0.1.10

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.
@@ -1,7 +1,7 @@
1
1
  import type { Linter } from 'eslint';
2
2
  import type { ReadonlyRecord } from 'ts-type-forge';
3
3
  /**
4
- * @description Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-std-forge.
4
+ * @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.
5
5
  *
6
6
  * ```md
7
7
  * | key | value |
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-non-null-object.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AASlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,oBAAoB,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA0EjE,CAAC"}
1
+ {"version":3,"file":"prefer-is-non-null-object.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAWlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,oBAAoB,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4EjE,CAAC"}
@@ -1,11 +1,11 @@
1
1
  import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
- import { buildNamedImportFix, getImportDeclarations, getNamedImports, skipTypeWrappers, } from 'eslint-rule-forge';
2
+ import { buildNamedImportFix, getImportDeclarations, getNamedImports, getRangeWithParens, parenthesesDepth, skipTypeWrappers, } from 'eslint-rule-forge';
3
3
  import { TS_STD_FORGE_MODULE } from './constants.mjs';
4
4
  export const preferIsNonNullObject = {
5
5
  meta: {
6
6
  type: 'suggestion',
7
7
  docs: {
8
- description: 'Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-std-forge.',
8
+ description: 'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.',
9
9
  },
10
10
  fixable: 'code',
11
11
  schema: [],
@@ -17,14 +17,14 @@ export const preferIsNonNullObject = {
17
17
  const sourceCode = context.sourceCode;
18
18
  const program = sourceCode.ast;
19
19
  const tsStdForgeImports = getImportDeclarations(program, TS_STD_FORGE_MODULE);
20
- const mut_nodesToFix = [];
20
+ const mut_pairsToFix = [];
21
21
  return {
22
22
  LogicalExpression: (node) => {
23
- const identifierName = getNonNullObjectIdentifierName(node);
24
- if (identifierName === undefined) {
23
+
24
+ if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
25
25
  return;
26
26
  }
27
- mut_nodesToFix.push({ node, identifierName });
27
+ mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
28
28
  },
29
29
  'Program:exit': () => {
30
30
  const hasIsNonNullObjectImport = getNamedImports(tsStdForgeImports).includes('isNonNullObject');
@@ -33,9 +33,12 @@ export const preferIsNonNullObject = {
33
33
 
34
34
 
35
35
 
36
- for (const [index, { node, identifierName },] of mut_nodesToFix.entries()) {
36
+ for (const [index, { range, identifierName },] of mut_pairsToFix.entries()) {
37
37
  context.report({
38
- node,
38
+ loc: {
39
+ start: sourceCode.getLocFromIndex(range[0]),
40
+ end: sourceCode.getLocFromIndex(range[1]),
41
+ },
39
42
  messageId: 'useIsNonNullObject',
40
43
  fix: (fixer) => {
41
44
  const replacement = `isNonNullObject(${identifierName})`;
@@ -44,7 +47,10 @@ export const preferIsNonNullObject = {
44
47
  'isNonNullObject',
45
48
  ])
46
49
  : [];
47
- return [...importFixes, fixer.replaceText(node, replacement)];
50
+ return [
51
+ ...importFixes,
52
+ fixer.replaceTextRange(range, replacement),
53
+ ];
48
54
  },
49
55
  });
50
56
  }
@@ -55,57 +61,126 @@ export const preferIsNonNullObject = {
55
61
  };
56
62
 
57
63
 
58
- const getNonNullObjectIdentifierName = (
64
+
65
+
66
+ const isChainOperand = (
67
+
68
+ sourceCode,
69
+
70
+ node) => node.parent.type === AST_NODE_TYPES.LogicalExpression &&
71
+ node.parent.operator === '&&' &&
72
+ parenthesesDepth(sourceCode, node) === 0;
73
+
74
+
75
+
76
+
77
+
78
+
79
+ const findNonNullObjectPairs = (
80
+
81
+ sourceCode,
59
82
 
60
83
  node) => {
61
- if (node.operator !== '&&') {
62
- return undefined;
63
- }
64
- const leftIdentifierName = getTypeofObjectIdentifierName(skipTypeWrappers(node.left));
65
- const rightIdentifierName = getNonNullCheckIdentifierName(skipTypeWrappers(node.right));
66
- return leftIdentifierName === undefined ||
67
- rightIdentifierName === undefined ||
68
- leftIdentifierName !== rightIdentifierName
69
- ? undefined
70
- : leftIdentifierName;
84
+ const operands = chainOperands(sourceCode, node);
85
+ const pairsFrom = (index) => {
86
+ const first = operands[index];
87
+ const second = operands[index + 1];
88
+ if (first === undefined || second === undefined) {
89
+ return [];
90
+ }
91
+ const identifierName = pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
92
+ return identifierName === undefined
93
+ ? pairsFrom(index + 1)
94
+ : [
95
+ {
96
+ range: [
97
+ getRangeWithParens(sourceCode, first)[0],
98
+ getRangeWithParens(sourceCode, second)[1],
99
+ ],
100
+ identifierName,
101
+ },
102
+ ...pairsFrom(index + 2),
103
+ ];
104
+ };
105
+ return pairsFrom(0);
106
+ };
107
+
108
+
109
+
110
+
111
+ const chainOperands = (
112
+
113
+ sourceCode,
114
+
115
+ node) => [node.left, node.right].flatMap((operand) => operand.type === AST_NODE_TYPES.LogicalExpression &&
116
+ operand.operator === '&&' &&
117
+ parenthesesDepth(sourceCode, operand) === 0
118
+ ? chainOperands(sourceCode, operand)
119
+ : [operand]);
120
+
121
+
122
+
123
+
124
+
125
+
126
+ const pairIdentifierName = (
127
+
128
+ typeofCheck,
129
+
130
+ nullCheck) => {
131
+ const identifierName = getTypeofObjectIdentifierName(skipTypeWrappers(typeofCheck));
132
+ return identifierName !== undefined &&
133
+ identifierName ===
134
+ getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
135
+ ? identifierName
136
+ : undefined;
71
137
  };
138
+
72
139
  const getTypeofObjectIdentifierName = (
73
140
 
74
141
  node) => {
75
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
76
- return undefined;
77
- }
78
- if (node.operator !== '===') {
142
+ if (node.type !== AST_NODE_TYPES.BinaryExpression ||
143
+ node.operator !== '===') {
79
144
  return undefined;
80
145
  }
81
146
  const left = skipTypeWrappers(node.left);
82
147
  const right = skipTypeWrappers(node.right);
83
- if (left.type !== AST_NODE_TYPES.UnaryExpression ||
84
- left.operator !== 'typeof' ||
85
- right.type !== AST_NODE_TYPES.Literal ||
86
- right.value !== 'object') {
148
+ const typeofSide = isLiteralOf(right, 'object')
149
+ ? left
150
+ : isLiteralOf(left, 'object')
151
+ ? right
152
+ : undefined;
153
+ if (typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
154
+ typeofSide.operator !== 'typeof') {
87
155
  return undefined;
88
156
  }
89
- const argument = skipTypeWrappers(left.argument);
157
+ const argument = skipTypeWrappers(typeofSide.argument);
90
158
  return argument.type === AST_NODE_TYPES.Identifier
91
159
  ? argument.name
92
160
  : undefined;
93
161
  };
162
+
163
+
164
+
165
+
166
+
94
167
  const getNonNullCheckIdentifierName = (
95
168
 
96
169
  node) => {
97
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
98
- return undefined;
99
- }
100
- if (node.operator !== '!==') {
170
+ if (node.type !== AST_NODE_TYPES.BinaryExpression ||
171
+ (node.operator !== '!==' && node.operator !== '!=')) {
101
172
  return undefined;
102
173
  }
103
174
  const left = skipTypeWrappers(node.left);
104
175
  const right = skipTypeWrappers(node.right);
105
- return left.type !== AST_NODE_TYPES.Identifier ||
106
- right.type !== AST_NODE_TYPES.Literal ||
107
- right.value !== null
108
- ? undefined
109
- : left.name;
176
+ const checked = isLiteralOf(right, null)
177
+ ? left
178
+ : isLiteralOf(left, null)
179
+ ? right
180
+ : undefined;
181
+ return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
110
182
  };
183
+ const isLiteralOf = (
184
+
185
+ node, value) => node.type === AST_NODE_TYPES.Literal && node.value === value;
111
186
  //# sourceMappingURL=prefer-is-non-null-object.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-non-null-object.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-is-non-null-object.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAalC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAiLjE,CAAC"}
1
+ {"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAalC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4NjE,CAAC"}
@@ -33,6 +33,17 @@ export const preferIsRecordAndHasKey = {
33
33
  const isKnownRecord = (
34
34
 
35
35
  expression) => typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) => isAlreadyRecordType(checker, type));
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ const mayBeArray = (
44
+
45
+ expression) => sourceCode.parserServices?.program != null &&
46
+ !typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) => !mayBeArrayType(checker, type));
36
47
  const mut_nodesToFix = [];
37
48
  return {
38
49
 
@@ -55,7 +66,8 @@ export const preferIsRecordAndHasKey = {
55
66
  }
56
67
 
57
68
  if (objArg.type === AST_NODE_TYPES.SpreadElement ||
58
- keyArg.type === AST_NODE_TYPES.SpreadElement) {
69
+ keyArg.type === AST_NODE_TYPES.SpreadElement ||
70
+ mayBeArray(objArg)) {
59
71
  return;
60
72
  }
61
73
  mut_nodesToFix.push({
@@ -72,7 +84,8 @@ export const preferIsRecordAndHasKey = {
72
84
  const keyExpression = node.left;
73
85
  const objExpression = node.right;
74
86
 
75
- if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) {
87
+ if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
88
+ mayBeArray(objExpression)) {
76
89
  return;
77
90
  }
78
91
  mut_nodesToFix.push({
@@ -83,20 +96,29 @@ export const preferIsRecordAndHasKey = {
83
96
  },
84
97
  'Program:exit': () => {
85
98
  const namedImports = getNamedImports(tsStdForgeImports);
86
- const rewrites = mut_nodesToFix.map((entry) => ({
87
- ...entry,
88
-
89
-
90
- needsIsRecord: !isKnownRecord(entry.objExpression),
91
- }));
92
- const importsToAdd = [
93
- ...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&
94
- !namedImports.includes('isRecord')
95
- ? ['isRecord']
96
- : []),
97
- ...(namedImports.includes('hasKey') ? [] : ['hasKey']),
98
- ];
99
- for (const [index, { node, objExpression, keyExpression, needsIsRecord },] of rewrites.entries()) {
99
+ const rewrites = mut_nodesToFix.map((entry) => {
100
+
101
+
102
+ const needsIsRecord = !isKnownRecord(entry.objExpression);
103
+ return {
104
+ ...entry,
105
+ needsIsRecord,
106
+
107
+ fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
108
+ };
109
+ });
110
+ const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
111
+ const importsToAdd = SafeArray.isEmpty(fixableRewrites)
112
+ ? []
113
+ : [
114
+ ...(fixableRewrites.some(({ needsIsRecord }) => needsIsRecord) && !namedImports.includes('isRecord')
115
+ ? ['isRecord']
116
+ : []),
117
+ ...(namedImports.includes('hasKey') ? [] : ['hasKey']),
118
+ ];
119
+ const importCarrier = fixableRewrites[0];
120
+ for (const rewrite of rewrites) {
121
+ const { node, objExpression, keyExpression, needsIsRecord } = rewrite;
100
122
  const objText = getArgumentText(sourceCode, objExpression);
101
123
  const keyText = getArgumentText(sourceCode, keyExpression);
102
124
  const originalText = sourceCode.getText(node);
@@ -108,18 +130,24 @@ export const preferIsRecordAndHasKey = {
108
130
  objName: objText,
109
131
  keyName: keyText,
110
132
  },
111
- fix: (fixer) => {
112
- const replacement = needsIsRecord
113
- ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
114
- : `hasKey(${objText}, ${keyText})`;
133
+ fix: !rewrite.fixable
134
+ ? undefined
135
+ : (fixer) => {
136
+ const replacement = needsIsRecord
137
+ ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
138
+ : `hasKey(${objText}, ${keyText})`;
115
139
 
116
140
 
117
141
 
118
- const importFixes = index === 0 && importsToAdd.length > 0
119
- ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, importsToAdd)
120
- : [];
121
- return [...importFixes, fixer.replaceText(node, replacement)];
122
- },
142
+
143
+ const importFixes = rewrite === importCarrier && importsToAdd.length > 0
144
+ ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, importsToAdd)
145
+ : [];
146
+ return [
147
+ ...importFixes,
148
+ fixer.replaceText(node, replacement),
149
+ ];
150
+ },
123
151
  });
124
152
  }
125
153
  },
@@ -131,6 +159,44 @@ export const preferIsRecordAndHasKey = {
131
159
 
132
160
 
133
161
 
162
+ const isSideEffectFree = (
163
+
164
+ wrapped) => {
165
+ const node = skipTypeWrappers(wrapped);
166
+ return (node.type === AST_NODE_TYPES.Identifier ||
167
+ node.type === AST_NODE_TYPES.ThisExpression ||
168
+ (node.type === AST_NODE_TYPES.MemberExpression &&
169
+ !node.computed &&
170
+ isSideEffectFree(node.object)));
171
+ };
172
+
173
+
174
+
175
+
176
+
177
+ const mayBeArrayType = (
178
+
179
+ checker,
180
+
181
+ type) => {
182
+ if (type.isUnionOrIntersection()) {
183
+ return type.types.some((member) => mayBeArrayType(checker, member));
184
+ }
185
+ if ((type.flags &
186
+ (ts.TypeFlags.TypeParameter |
187
+ ts.TypeFlags.IndexedAccess |
188
+ ts.TypeFlags.Conditional |
189
+ ts.TypeFlags.Substitution)) !==
190
+ 0) {
191
+ const constraint = checker.getBaseConstraintOfType(type);
192
+ return constraint !== undefined && mayBeArrayType(checker, constraint);
193
+ }
194
+ return checker.isArrayType(type) || checker.isTupleType(type);
195
+ };
196
+
197
+
198
+
199
+
134
200
 
135
201
 
136
202
 
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-record-and-has-key.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-is-record-and-has-key.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-std-forge",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer code toward ts-std-forge idioms.",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@typescript-eslint/utils": "^8.71.1",
38
- "eslint-rule-forge": "^0.1.0",
38
+ "eslint-rule-forge": "^0.2.0",
39
39
  "ts-std-forge": "0.5.1",
40
40
  "ts-type-forge": "~9.2.2"
41
41
  },
@@ -43,9 +43,9 @@
43
43
  "@typescript-eslint/parser": "8.71.1",
44
44
  "@typescript-eslint/rule-tester": "8.71.1",
45
45
  "dedent": "1.7.2",
46
- "eslint-config-typed": "5.18.0",
47
- "eslint-plugin-ts-fortress": "1.3.3",
48
- "eslint-plugin-ts-type-forge": "0.8.8",
46
+ "eslint-config-typed": "5.19.0",
47
+ "eslint-plugin-ts-fortress": "1.3.4",
48
+ "eslint-plugin-ts-type-forge": "0.8.9",
49
49
  "prettier": "3.9.9",
50
50
  "prettier-plugin-organize-imports": "4.3.0",
51
51
  "prettier-plugin-packagejson": "3.0.2",
@@ -66,15 +66,15 @@
66
66
  "scripts": {
67
67
  "build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
68
68
  "check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
69
- "check:lint": "oxlint . && eslint .",
70
- "check:test": "pnpm run z:vitest run",
71
- "check:test:cov": "pnpm run z:vitest run --coverage",
72
- "check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
69
+ "check:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . && eslint .",
70
+ "check:test": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run",
71
+ "check:test:cov": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run --coverage",
72
+ "check:types": "node ../../tools/scripts/cmd/assert-deps-built.mts && node ../../node_modules/typescript-native/bin/tsc --noEmit",
73
73
  "doc": "pnpm run gen:readme",
74
74
  "fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
75
75
  "fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
76
76
  "fix:fmt:full": "pnpm run z:prettier .",
77
- "fix:lint": "oxlint . --fix && eslint . --fix",
77
+ "fix:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . --fix && eslint . --fix",
78
78
  "fmt": "pnpm run fix:fmt",
79
79
  "gen": "run-s gen:rule-types gen:readme",
80
80
  "gen:readme": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
@@ -9,7 +9,7 @@ import type { ReadonlyRecord } from 'ts-type-forge';
9
9
  import type { tsStdForgeRules } from './rules/index.mjs';
10
10
 
11
11
  /**
12
- * @description Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-std-forge.
12
+ * @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.
13
13
  *
14
14
  * ```md
15
15
  * | key | value |
@@ -7,6 +7,8 @@ import {
7
7
  buildNamedImportFix,
8
8
  getImportDeclarations,
9
9
  getNamedImports,
10
+ getRangeWithParens,
11
+ parenthesesDepth,
10
12
  skipTypeWrappers,
11
13
  } from 'eslint-rule-forge';
12
14
  import { TS_STD_FORGE_MODULE } from './constants.mjs';
@@ -20,7 +22,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
20
22
  type: 'suggestion',
21
23
  docs: {
22
24
  description:
23
- 'Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-std-forge.',
25
+ 'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.',
24
26
  },
25
27
  fixable: 'code',
26
28
  schema: [],
@@ -40,20 +42,16 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
40
42
  TS_STD_FORGE_MODULE,
41
43
  );
42
44
 
43
- const mut_nodesToFix: {
44
- node: TSESTree.LogicalExpression;
45
- identifierName: string;
46
- }[] = [];
45
+ const mut_pairsToFix: NonNullObjectPair[] = [];
47
46
 
48
47
  return {
49
48
  LogicalExpression: (node) => {
50
- const identifierName = getNonNullObjectIdentifierName(node);
51
-
52
- if (identifierName === undefined) {
49
+ // A chain is read once, from its outermost `&&`.
50
+ if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
53
51
  return;
54
52
  }
55
53
 
56
- mut_nodesToFix.push({ node, identifierName });
54
+ mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
57
55
  },
58
56
  'Program:exit': () => {
59
57
  const hasIsNonNullObjectImport =
@@ -66,10 +64,13 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
66
64
  // with it.
67
65
  for (const [
68
66
  index,
69
- { node, identifierName },
70
- ] of mut_nodesToFix.entries()) {
67
+ { range, identifierName },
68
+ ] of mut_pairsToFix.entries()) {
71
69
  context.report({
72
- node,
70
+ loc: {
71
+ start: sourceCode.getLocFromIndex(range[0]),
72
+ end: sourceCode.getLocFromIndex(range[1]),
73
+ },
73
74
  messageId: 'useIsNonNullObject',
74
75
  fix: (fixer) => {
75
76
  const replacement = `isNonNullObject(${identifierName})`;
@@ -81,7 +82,10 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
81
82
  ])
82
83
  : [];
83
84
 
84
- return [...importFixes, fixer.replaceText(node, replacement)];
85
+ return [
86
+ ...importFixes,
87
+ fixer.replaceTextRange(range, replacement),
88
+ ];
85
89
  },
86
90
  });
87
91
  }
@@ -91,41 +95,124 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
91
95
  defaultOptions: [],
92
96
  } as const;
93
97
 
94
- // Every operand below is read through its type wrappers: `typeof (u satisfies
95
- // unknown) === 'object' && u! !== null` is the same check.
98
+ /**
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`.
102
+ */
103
+ type NonNullObjectPair = Readonly<{
104
+ range: TSESTree.Range;
105
+ identifierName: string;
106
+ }>;
96
107
 
97
- const getNonNullObjectIdentifierName = (
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;
121
+
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];
140
+
141
+ if (first === undefined || second === undefined) {
142
+ return [];
143
+ }
144
+
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
+ };
104
161
 
105
- const leftIdentifierName = getTypeofObjectIdentifierName(
106
- skipTypeWrappers(node.left),
162
+ return pairsFrom(0);
163
+ };
164
+
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],
107
181
  );
108
182
 
109
- const rightIdentifierName = getNonNullCheckIdentifierName(
110
- skipTypeWrappers(node.right),
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,
193
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
194
+ nullCheck: TSESTree.Expression,
195
+ ): string | undefined => {
196
+ const identifierName = getTypeofObjectIdentifierName(
197
+ skipTypeWrappers(typeofCheck),
111
198
  );
112
199
 
113
- return leftIdentifierName === undefined ||
114
- rightIdentifierName === undefined ||
115
- leftIdentifierName !== rightIdentifierName
116
- ? undefined
117
- : leftIdentifierName;
200
+ return identifierName !== undefined &&
201
+ identifierName ===
202
+ getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
203
+ ? identifierName
204
+ : undefined;
118
205
  };
119
206
 
207
+ /** `u` of `typeof u === 'object'`, in either operand order. */
120
208
  const getTypeofObjectIdentifierName = (
121
209
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
122
210
  node: TSESTree.Node,
123
211
  ): string | undefined => {
124
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
125
- return undefined;
126
- }
127
-
128
- if (node.operator !== '===') {
212
+ if (
213
+ node.type !== AST_NODE_TYPES.BinaryExpression ||
214
+ node.operator !== '==='
215
+ ) {
129
216
  return undefined;
130
217
  }
131
218
 
@@ -133,31 +220,39 @@ const getTypeofObjectIdentifierName = (
133
220
 
134
221
  const right = skipTypeWrappers(node.right);
135
222
 
223
+ const typeofSide = isLiteralOf(right, 'object')
224
+ ? left
225
+ : isLiteralOf(left, 'object')
226
+ ? right
227
+ : undefined;
228
+
136
229
  if (
137
- left.type !== AST_NODE_TYPES.UnaryExpression ||
138
- left.operator !== 'typeof' ||
139
- right.type !== AST_NODE_TYPES.Literal ||
140
- right.value !== 'object'
230
+ typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
231
+ typeofSide.operator !== 'typeof'
141
232
  ) {
142
233
  return undefined;
143
234
  }
144
235
 
145
- const argument = skipTypeWrappers(left.argument);
236
+ const argument = skipTypeWrappers(typeofSide.argument);
146
237
 
147
238
  return argument.type === AST_NODE_TYPES.Identifier
148
239
  ? argument.name
149
240
  : undefined;
150
241
  };
151
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
+ */
152
248
  const getNonNullCheckIdentifierName = (
153
249
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
154
250
  node: TSESTree.Node,
155
251
  ): string | undefined => {
156
- if (node.type !== AST_NODE_TYPES.BinaryExpression) {
157
- return undefined;
158
- }
159
-
160
- if (node.operator !== '!==') {
252
+ if (
253
+ node.type !== AST_NODE_TYPES.BinaryExpression ||
254
+ (node.operator !== '!==' && node.operator !== '!=')
255
+ ) {
161
256
  return undefined;
162
257
  }
163
258
 
@@ -165,9 +260,17 @@ const getNonNullCheckIdentifierName = (
165
260
 
166
261
  const right = skipTypeWrappers(node.right);
167
262
 
168
- return left.type !== AST_NODE_TYPES.Identifier ||
169
- right.type !== AST_NODE_TYPES.Literal ||
170
- right.value !== null
171
- ? undefined
172
- : 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;
173
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;
@@ -106,3 +106,110 @@ describe('prefer-is-non-null-object through type wrappers', () => {
106
106
  ],
107
107
  });
108
108
  });
109
+
110
+ describe('prefer-is-non-null-object in other forms', () => {
111
+ ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
112
+ valid: [
113
+ {
114
+ name: 'the two halves joined by `||`',
115
+ code: dedent`
116
+ const ok = typeof u === 'object' || u !== null;
117
+ `,
118
+ },
119
+ {
120
+ name: 'the two halves not next to each other in the chain',
121
+ code: dedent`
122
+ declare const a: boolean;
123
+ const ok = typeof u === 'object' && a && u !== null;
124
+ `,
125
+ },
126
+ {
127
+ name: 'an undefined check is not a null check',
128
+ code: dedent`
129
+ const ok = u !== undefined && typeof u === 'object';
130
+ `,
131
+ },
132
+ {
133
+ name: 'the halves split by parentheses',
134
+ code: dedent`
135
+ declare const b: boolean;
136
+ const ok = typeof u === 'object' && (u !== null && b);
137
+ `,
138
+ },
139
+ ],
140
+ invalid: [
141
+ {
142
+ name: 'the halves in reverse order',
143
+ code: dedent`
144
+ const ok = u !== null && typeof u === 'object';
145
+ `,
146
+ output: dedent`
147
+ import { isNonNullObject } from 'ts-std-forge';
148
+ const ok = isNonNullObject(u);
149
+ `,
150
+ errors: [{ messageId: 'useIsNonNullObject' }],
151
+ },
152
+ {
153
+ name: 'each comparison with its operands swapped',
154
+ code: dedent`
155
+ const ok = 'object' === typeof u && null !== u;
156
+ `,
157
+ output: dedent`
158
+ import { isNonNullObject } from 'ts-std-forge';
159
+ const ok = isNonNullObject(u);
160
+ `,
161
+ errors: [{ messageId: 'useIsNonNullObject' }],
162
+ },
163
+ {
164
+ name: '`!= null`, which typeof has already made the same check',
165
+ code: dedent`
166
+ const ok = typeof u === 'object' && u != null;
167
+ `,
168
+ output: dedent`
169
+ import { isNonNullObject } from 'ts-std-forge';
170
+ const ok = isNonNullObject(u);
171
+ `,
172
+ errors: [{ messageId: 'useIsNonNullObject' }],
173
+ },
174
+ {
175
+ name: 'the pair at the start, the end and the middle of a longer chain',
176
+ code: dedent`
177
+ declare const a: boolean;
178
+ declare const b: boolean;
179
+ const x = typeof u === 'object' && u !== null && 'k' in u;
180
+ const y = a && typeof u === 'object' && u !== null;
181
+ const z = a && u !== null && (typeof u === 'object') && b;
182
+ `,
183
+ output: dedent`
184
+ import { isNonNullObject } from 'ts-std-forge';
185
+ declare const a: boolean;
186
+ declare const b: boolean;
187
+ const x = isNonNullObject(u) && 'k' in u;
188
+ const y = a && isNonNullObject(u);
189
+ const z = a && isNonNullObject(u) && b;
190
+ `,
191
+ errors: [
192
+ { messageId: 'useIsNonNullObject' },
193
+ { messageId: 'useIsNonNullObject' },
194
+ { messageId: 'useIsNonNullObject' },
195
+ ],
196
+ },
197
+ {
198
+ name: 'two pairs in one chain',
199
+ code: dedent`
200
+ declare const v: unknown;
201
+ const ok = typeof u === 'object' && u !== null && v !== null && typeof v === 'object';
202
+ `,
203
+ output: dedent`
204
+ import { isNonNullObject } from 'ts-std-forge';
205
+ declare const v: unknown;
206
+ const ok = isNonNullObject(u) && isNonNullObject(v);
207
+ `,
208
+ errors: [
209
+ { messageId: 'useIsNonNullObject' },
210
+ { messageId: 'useIsNonNullObject' },
211
+ ],
212
+ },
213
+ ],
214
+ });
215
+ });
@@ -64,6 +64,24 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
64
64
  isAlreadyRecordType(checker, type),
65
65
  );
66
66
 
67
+ /**
68
+ * Whether the object may be an array at some layer of type wrappers.
69
+ * `isRecord` rejects an array, so `'0' in xs` would turn from `true` to
70
+ * `false`; a widening cast to a type that may be an array is the author
71
+ * saying it may be one. Without type information nothing is known, and
72
+ * the rule reports as it always has.
73
+ */
74
+ const mayBeArray = (
75
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
76
+ expression: TSESTree.Expression,
77
+ ): boolean =>
78
+ sourceCode.parserServices?.program != null &&
79
+ !typeHoldsAtEveryLayer(
80
+ sourceCode,
81
+ expression,
82
+ (type, checker) => !mayBeArrayType(checker, type),
83
+ );
84
+
67
85
  const mut_nodesToFix: {
68
86
  node: TSESTree.CallExpression | TSESTree.BinaryExpression;
69
87
  objExpression: TSESTree.Expression;
@@ -100,7 +118,8 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
100
118
  // Type guard: ensure arguments are Expressions (not SpreadElement)
101
119
  if (
102
120
  objArg.type === AST_NODE_TYPES.SpreadElement ||
103
- keyArg.type === AST_NODE_TYPES.SpreadElement
121
+ keyArg.type === AST_NODE_TYPES.SpreadElement ||
122
+ mayBeArray(objArg)
104
123
  ) {
105
124
  return;
106
125
  }
@@ -123,7 +142,10 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
123
142
  const objExpression = node.right;
124
143
 
125
144
  // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
126
- if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) {
145
+ if (
146
+ keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
147
+ mayBeArray(objExpression)
148
+ ) {
127
149
  return;
128
150
  }
129
151
 
@@ -137,25 +159,40 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
137
159
  'Program:exit': () => {
138
160
  const namedImports = getNamedImports(tsStdForgeImports);
139
161
 
140
- const rewrites = mut_nodesToFix.map((entry) => ({
141
- ...entry,
162
+ const rewrites = mut_nodesToFix.map((entry) => {
142
163
  // A record-typed object makes the `isRecord(...)` conjunct always
143
164
  // true, so it is left out of the replacement entirely.
144
- needsIsRecord: !isKnownRecord(entry.objExpression),
145
- }));
146
-
147
- const importsToAdd: readonly string[] = [
148
- ...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&
149
- !namedImports.includes('isRecord')
150
- ? ['isRecord']
151
- : []),
152
- ...(namedImports.includes('hasKey') ? [] : ['hasKey']),
153
- ];
154
-
155
- for (const [
156
- index,
157
- { node, objExpression, keyExpression, needsIsRecord },
158
- ] of rewrites.entries()) {
165
+ const needsIsRecord = !isKnownRecord(entry.objExpression);
166
+
167
+ return {
168
+ ...entry,
169
+ needsIsRecord,
170
+ // `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
171
+ fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
172
+ };
173
+ });
174
+
175
+ const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
176
+
177
+ const importsToAdd: readonly string[] = SafeArray.isEmpty(
178
+ fixableRewrites,
179
+ )
180
+ ? []
181
+ : [
182
+ ...(fixableRewrites.some(
183
+ ({ needsIsRecord }) => needsIsRecord,
184
+ ) && !namedImports.includes('isRecord')
185
+ ? ['isRecord']
186
+ : []),
187
+ ...(namedImports.includes('hasKey') ? [] : ['hasKey']),
188
+ ];
189
+
190
+ const importCarrier = fixableRewrites[0];
191
+
192
+ for (const rewrite of rewrites) {
193
+ const { node, objExpression, keyExpression, needsIsRecord } =
194
+ rewrite;
195
+
159
196
  const objText = getArgumentText(sourceCode, objExpression);
160
197
 
161
198
  const keyText = getArgumentText(sourceCode, keyExpression);
@@ -170,26 +207,32 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
170
207
  objName: objText,
171
208
  keyName: keyText,
172
209
  },
173
- fix: (fixer) => {
174
- const replacement = needsIsRecord
175
- ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
176
- : `hasKey(${objText}, ${keyText})`;
177
-
178
- // All import fixes have to live on the first report:
179
- // `insertTextBefore(program, …)` produces overlapping ranges
180
- // otherwise and only one of them would survive a pass.
181
- const importFixes =
182
- index === 0 && importsToAdd.length > 0
183
- ? buildNamedImportFix(
184
- fixer,
185
- program,
186
- TS_STD_FORGE_MODULE,
187
- importsToAdd,
188
- )
189
- : [];
190
-
191
- return [...importFixes, fixer.replaceText(node, replacement)];
192
- },
210
+ fix: !rewrite.fixable
211
+ ? undefined
212
+ : (fixer) => {
213
+ const replacement = needsIsRecord
214
+ ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
215
+ : `hasKey(${objText}, ${keyText})`;
216
+
217
+ // All import fixes have to live on the first fixable
218
+ // report: `insertTextBefore(program, …)` produces
219
+ // overlapping ranges otherwise and only one of them would
220
+ // survive a pass.
221
+ const importFixes =
222
+ rewrite === importCarrier && importsToAdd.length > 0
223
+ ? buildNamedImportFix(
224
+ fixer,
225
+ program,
226
+ TS_STD_FORGE_MODULE,
227
+ importsToAdd,
228
+ )
229
+ : [];
230
+
231
+ return [
232
+ ...importFixes,
233
+ fixer.replaceText(node, replacement),
234
+ ];
235
+ },
193
236
  });
194
237
  }
195
238
  },
@@ -198,6 +241,56 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
198
241
  defaultOptions: [],
199
242
  } as const;
200
243
 
244
+ /**
245
+ * Whether reading `node` again has no side effects: a variable, `this`, or a
246
+ * non-computed property chain on one, type wrappers aside.
247
+ */
248
+ const isSideEffectFree = (
249
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
250
+ wrapped: TSESTree.Node,
251
+ ): boolean => {
252
+ const node = skipTypeWrappers(wrapped);
253
+
254
+ return (
255
+ node.type === AST_NODE_TYPES.Identifier ||
256
+ node.type === AST_NODE_TYPES.ThisExpression ||
257
+ (node.type === AST_NODE_TYPES.MemberExpression &&
258
+ !node.computed &&
259
+ isSideEffectFree(node.object))
260
+ );
261
+ };
262
+
263
+ /**
264
+ * Whether a value of `type` may be an array or a tuple: some member of a union
265
+ * or an intersection is one, or the constraint of a deferred type is.
266
+ * `object` and `unknown` say nothing either way and are not counted.
267
+ */
268
+ const mayBeArrayType = (
269
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
270
+ checker: ts.TypeChecker,
271
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
272
+ type: ts.Type,
273
+ ): boolean => {
274
+ if (type.isUnionOrIntersection()) {
275
+ return type.types.some((member) => mayBeArrayType(checker, member));
276
+ }
277
+
278
+ if (
279
+ (type.flags &
280
+ (ts.TypeFlags.TypeParameter |
281
+ ts.TypeFlags.IndexedAccess |
282
+ ts.TypeFlags.Conditional |
283
+ ts.TypeFlags.Substitution)) !==
284
+ 0
285
+ ) {
286
+ const constraint = checker.getBaseConstraintOfType(type);
287
+
288
+ return constraint !== undefined && mayBeArrayType(checker, constraint);
289
+ }
290
+
291
+ return checker.isArrayType(type) || checker.isTupleType(type);
292
+ };
293
+
201
294
  /**
202
295
  * Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
203
296
  * constraint, so that guarding the call with `isRecord` adds nothing.
@@ -83,21 +83,6 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
83
83
  tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
84
84
  valid: [],
85
85
  invalid: [
86
- {
87
- // `isRecord` rejects an array, so dropping it on the cast's word
88
- // would turn `false` into `true`.
89
- name: 'keeps isRecord for an array cast to a record',
90
- code: dedent`
91
- declare const arr: string[];
92
- const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
93
- `,
94
- output: dedent`
95
- import { isRecord, hasKey } from 'ts-std-forge';
96
- declare const arr: string[];
97
- const ok = (isRecord(arr as unknown as Record<string, unknown>) && hasKey(arr as unknown as Record<string, unknown>, '0'));
98
- `,
99
- errors: [{ messageId: 'useIsRecordAndHasKey' }],
100
- },
101
86
  {
102
87
  name: 'keeps isRecord for an unknown value cast to a record in `in`',
103
88
  code: dedent`
@@ -138,19 +123,6 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
138
123
  `,
139
124
  errors: [{ messageId: 'useIsRecordAndHasKey' }],
140
125
  },
141
- {
142
- name: 'keeps isRecord for a record widened by `as` to one that may be an array',
143
- code: dedent`
144
- declare const obj: Readonly<Record<string, unknown>>;
145
- const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
146
- `,
147
- output: dedent`
148
- import { isRecord, hasKey } from 'ts-std-forge';
149
- declare const obj: Readonly<Record<string, unknown>>;
150
- const ok = (isRecord(obj as Readonly<Record<string, unknown>> | readonly string[]) && hasKey(obj as Readonly<Record<string, unknown>> | readonly string[], 'a'));
151
- `,
152
- errors: [{ messageId: 'useIsRecordAndHasKey' }],
153
- },
154
126
  {
155
127
  name: 'drops isRecord for a record that is a record at every layer',
156
128
  code: dedent`
@@ -186,34 +158,39 @@ describe('prefer-is-record-and-has-key with parenthesized operands', () => {
186
158
  valid: [],
187
159
  invalid: [
188
160
  {
189
- name: 'keeps the parentheses of a sequence object in `in`',
161
+ name: 'a sequence object in `in`, read twice by the fix, gets no fix',
190
162
  code: dedent`
191
163
  declare const a: number;
192
164
  declare const o: unknown;
193
165
  const ok = 'k' in (a, o);
194
166
  `,
195
- output: dedent`
196
- import { isRecord, hasKey } from 'ts-std-forge';
167
+ output: null,
168
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
169
+ },
170
+ {
171
+ name: 'a sequence object in Object.hasOwn, read twice by the fix, gets no fix',
172
+ code: dedent`
197
173
  declare const a: number;
198
174
  declare const o: unknown;
199
- const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
175
+ const ok = Object.hasOwn((a, o), 'k');
200
176
  `,
177
+ output: null,
201
178
  errors: [{ messageId: 'useIsRecordAndHasKey' }],
202
179
  },
203
180
  {
204
- name: 'keeps the parentheses of a sequence object in Object.hasOwn',
181
+ name: 'keeps the parentheses of a sequence object read once',
205
182
  code: dedent`
206
183
  declare const a: number;
207
- declare const o: unknown;
208
- const ok = Object.hasOwn((a, o), 'k');
184
+ declare const o: Readonly<Record<string, unknown>>;
185
+ const ok = 'k' in (a, o);
209
186
  `,
210
187
  output: dedent`
211
- import { isRecord, hasKey } from 'ts-std-forge';
188
+ import { hasKey } from 'ts-std-forge';
212
189
  declare const a: number;
213
- declare const o: unknown;
214
- const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
190
+ declare const o: Readonly<Record<string, unknown>>;
191
+ const ok = hasKey((a, o), 'k');
215
192
  `,
216
- errors: [{ messageId: 'useIsRecordAndHasKey' }],
193
+ errors: [{ messageId: 'useHasKey' }],
217
194
  },
218
195
  {
219
196
  name: 'keeps the parentheses of a sequence key',
@@ -233,3 +210,86 @@ describe('prefer-is-record-and-has-key with parenthesized operands', () => {
233
210
  ],
234
211
  });
235
212
  });
213
+
214
+ describe('prefer-is-record-and-has-key where the rewrite would change behavior', () => {
215
+ tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
216
+ valid: [
217
+ {
218
+ // `isRecord` rejects an array, so `'0' in xs` would turn false.
219
+ name: 'an array',
220
+ code: dedent`
221
+ declare const xs: readonly number[];
222
+ const a = '0' in xs;
223
+ const b = Object.hasOwn(xs, 'length');
224
+ `,
225
+ },
226
+ {
227
+ name: 'a union with an array',
228
+ code: dedent`
229
+ declare const v: Readonly<Record<string, unknown>> | readonly number[];
230
+ const ok = 'k' in v;
231
+ `,
232
+ },
233
+ {
234
+ name: 'an array cast to a record',
235
+ code: dedent`
236
+ declare const arr: readonly string[];
237
+ const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
238
+ `,
239
+ },
240
+ {
241
+ name: 'a record widened by `as` to one that may be an array: the author says it may be one',
242
+ code: dedent`
243
+ declare const obj: Readonly<Record<string, unknown>>;
244
+ const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
245
+ `,
246
+ },
247
+ ],
248
+ invalid: [
249
+ {
250
+ // `isRecord(…) && hasKey(…)` would evaluate the object twice.
251
+ name: 'an object with side effects is reported without a fix',
252
+ code: dedent`
253
+ declare const getObj: () => object;
254
+ declare const log: () => void;
255
+ declare const o: object;
256
+ const a = 'k' in getObj();
257
+ const b = Object.hasOwn(getObj(), 'k');
258
+ const c = 'k' in (log(), o);
259
+ `,
260
+ output: null,
261
+ errors: [
262
+ { messageId: 'useIsRecordAndHasKey' },
263
+ { messageId: 'useIsRecordAndHasKey' },
264
+ { messageId: 'useIsRecordAndHasKey' },
265
+ ],
266
+ },
267
+ {
268
+ name: 'an object with side effects is fixed when isRecord is dropped: it is read once',
269
+ code: dedent`
270
+ declare const getRec: () => Readonly<Record<string, unknown>>;
271
+ const ok = 'k' in getRec();
272
+ `,
273
+ output: dedent`
274
+ import { hasKey } from 'ts-std-forge';
275
+ declare const getRec: () => Readonly<Record<string, unknown>>;
276
+ const ok = hasKey(getRec(), 'k');
277
+ `,
278
+ errors: [{ messageId: 'useHasKey' }],
279
+ },
280
+ {
281
+ name: 'a reference read through type wrappers is still read without side effects',
282
+ code: dedent`
283
+ declare const o: { readonly p: object };
284
+ const ok = 'k' in (o.p satisfies object);
285
+ `,
286
+ output: dedent`
287
+ import { isRecord, hasKey } from 'ts-std-forge';
288
+ declare const o: { readonly p: object };
289
+ const ok = (isRecord(o.p satisfies object) && hasKey(o.p satisfies object, 'k'));
290
+ `,
291
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
292
+ },
293
+ ],
294
+ });
295
+ });