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

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 (44) hide show
  1. package/dist/rule-types.d.mts +2 -1
  2. package/dist/rule-types.d.mts.map +1 -1
  3. package/dist/rule-types.mjs.map +1 -1
  4. package/dist/rules/ast-utils.d.mts +8 -31
  5. package/dist/rules/ast-utils.d.mts.map +1 -1
  6. package/dist/rules/ast-utils.mjs +13 -75
  7. package/dist/rules/ast-utils.mjs.map +1 -1
  8. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  9. package/dist/rules/prefer-is-non-null-object.mjs +7 -5
  10. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  11. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  12. package/dist/rules/prefer-is-record-and-has-key.mjs +9 -13
  13. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  14. package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
  15. package/dist/rules/prefer-safe-array-is-array.mjs +8 -5
  16. package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
  17. package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
  18. package/dist/rules/prefer-safe-array-length-guard.mjs +11 -14
  19. package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
  20. package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
  21. package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -7
  22. package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
  23. package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
  24. package/dist/rules/prefer-safe-number-parse.mjs +9 -7
  25. package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
  26. package/package.json +5 -4
  27. package/src/rule-types.mts +2 -1
  28. package/src/rules/ast-utils.mts +20 -110
  29. package/src/rules/prefer-is-non-null-object.mts +13 -7
  30. package/src/rules/prefer-is-record-and-has-key.mts +28 -27
  31. package/src/rules/prefer-is-record-and-has-key.test.mts +30 -3
  32. package/src/rules/prefer-safe-array-is-array.mts +14 -10
  33. package/src/rules/prefer-safe-array-is-array.test.mts +14 -0
  34. package/src/rules/prefer-safe-array-length-guard.mts +25 -26
  35. package/src/rules/prefer-safe-array-length-guard.test.mts +19 -5
  36. package/src/rules/prefer-safe-number-parse-integer.mts +25 -19
  37. package/src/rules/prefer-safe-number-parse-integer.test.mts +19 -5
  38. package/src/rules/prefer-safe-number-parse.mts +25 -19
  39. package/src/rules/prefer-safe-number-parse.test.mts +19 -5
  40. package/dist/rules/import-utils.d.mts +0 -6
  41. package/dist/rules/import-utils.d.mts.map +0 -1
  42. package/dist/rules/import-utils.mjs +0 -28
  43. package/dist/rules/import-utils.mjs.map +0 -1
  44. package/src/rules/import-utils.mts +0 -58
@@ -1,7 +1,8 @@
1
1
  import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
+ import { buildNamedImportFix, getArgumentText, getImportDeclarations, getNamedImports, skipTypeWrappers, } from 'eslint-rule-forge';
2
3
  import * as ts from 'typescript';
3
- import { getArgumentText, getValueType, isIdentifierNamed, skipTypeWrappers, } from './ast-utils.mjs';
4
- import { buildImportFixes, getNamedImports, getTsStdForgeImport, } from './import-utils.mjs';
4
+ import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
5
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
5
6
 
6
7
 
7
8
 
@@ -42,7 +43,7 @@ export const preferSafeNumberParse = {
42
43
  create: (context) => {
43
44
  const sourceCode = context.sourceCode;
44
45
  const program = sourceCode.ast;
45
- const tsStdForgeImport = getTsStdForgeImport(program);
46
+ const tsStdForgeImports = getImportDeclarations(program, TS_STD_FORGE_MODULE);
46
47
  const mut_nodesToFix = [];
47
48
  return {
48
49
  CallExpression: (node) => {
@@ -74,14 +75,15 @@ export const preferSafeNumberParse = {
74
75
 
75
76
 
76
77
 
77
- const argType = getValueType(sourceCode, firstArg);
78
- if (argType === undefined || !isStringType(argType)) {
78
+
79
+
80
+ if (!typeHoldsAtEveryLayer(sourceCode, firstArg, isStringType)) {
79
81
  return;
80
82
  }
81
83
  mut_nodesToFix.push({ node, argExpression: firstArg });
82
84
  },
83
85
  'Program:exit': () => {
84
- const namedImports = getNamedImports(tsStdForgeImport);
86
+ const namedImports = getNamedImports(tsStdForgeImports);
85
87
  const missingImports = ['Result', 'SafeNumber'].filter((name) => !namedImports.includes(name));
86
88
  for (const [index, { node, argExpression },] of mut_nodesToFix.entries()) {
87
89
  const argText = getArgumentText(sourceCode, argExpression);
@@ -95,7 +97,7 @@ export const preferSafeNumberParse = {
95
97
  fix: (fixer) => {
96
98
  const core = `Result.unwrapOkOr(SafeNumber.parse(${argText}), Number.NaN)`;
97
99
  const importFixes = index === 0 && missingImports.length > 0
98
- ? buildImportFixes(fixer, program, missingImports)
100
+ ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, missingImports)
99
101
  : [];
100
102
  return [...importFixes, fixer.replaceText(node, core)];
101
103
  },
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.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.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer code toward ts-std-forge idioms.",
6
6
  "keywords": [
@@ -35,6 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@typescript-eslint/utils": "^8.71.1",
38
+ "eslint-rule-forge": "^0.1.0",
38
39
  "ts-std-forge": "0.5.1",
39
40
  "ts-type-forge": "~9.2.2"
40
41
  },
@@ -42,9 +43,9 @@
42
43
  "@typescript-eslint/parser": "8.71.1",
43
44
  "@typescript-eslint/rule-tester": "8.71.1",
44
45
  "dedent": "1.7.2",
45
- "eslint-config-typed": "5.17.3",
46
- "eslint-plugin-ts-fortress": "1.3.2",
47
- "eslint-plugin-ts-type-forge": "0.8.7",
46
+ "eslint-config-typed": "5.18.0",
47
+ "eslint-plugin-ts-fortress": "1.3.3",
48
+ "eslint-plugin-ts-type-forge": "0.8.8",
48
49
  "prettier": "3.9.9",
49
50
  "prettier-plugin-organize-imports": "4.3.0",
50
51
  "prettier-plugin-packagejson": "3.0.2",
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { Linter } from 'eslint';
7
7
  import { expectType } from 'ts-std-forge';
8
+ import type { ReadonlyRecord } from 'ts-type-forge';
8
9
  import type { tsStdForgeRules } from './rules/index.mjs';
9
10
 
10
11
  /**
@@ -106,7 +107,7 @@ export type EslintTsStdForgeRules = Readonly<{
106
107
  'ts-std-forge/prefer-safe-number-parse-integer': PreferSafeNumberParseInteger.RuleEntry;
107
108
  }>;
108
109
 
109
- export type EslintTsStdForgeRulesOption = Readonly<{}>;
110
+ export type EslintTsStdForgeRulesOption = ReadonlyRecord<never, never>;
110
111
 
111
112
  // If this assertion fails to type-check, this generated file has drifted from
112
113
  // the actual plugin rules — re-run `pnpm run gen:rule-types`.
@@ -1,33 +1,11 @@
1
1
  import {
2
2
  AST_NODE_TYPES,
3
- ASTUtils,
4
3
  type TSESLint,
5
4
  type TSESTree,
6
5
  } from '@typescript-eslint/utils';
6
+ import { skipTypeWrappers, typeWrapperLayers } from 'eslint-rule-forge';
7
7
  import type * as ts from 'typescript';
8
8
 
9
- /**
10
- * `node` with every `as`, `satisfies`, `!` and `<T>` around it taken off: the
11
- * expression whose value it is. Judge syntax and ask types on what this
12
- * returns, so that a wrapper neither hides a pattern (`xs.length > (0 as
13
- * const)`) nor vouches for a type (`Number(b as unknown as string)`, whose
14
- * value is still a boolean), and keep the original node for fix text. See
15
- * `docs/writing-lint-rules.md` at the repository root.
16
- *
17
- * A copy of the helper in `eslint-config-typed`, which this package, published
18
- * on its own, cannot import.
19
- */
20
- export const skipTypeWrappers = (
21
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
22
- node: TSESTree.Node,
23
- ): TSESTree.Node =>
24
- isTypeWrapper(node) ? skipTypeWrappers(node.expression) : node;
25
-
26
- export const isTypeWrapper = (
27
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
28
- node: TSESTree.Node,
29
- ): node is TypeWrapper => TYPE_WRAPPERS.has(node.type);
30
-
31
9
  /**
32
10
  * Whether `node`, read through its type wrappers, is the identifier `name`:
33
11
  * `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
@@ -44,104 +22,36 @@ export const isIdentifierNamed = (
44
22
  );
45
23
  };
46
24
 
47
- export type TypeWrapper =
48
- | TSESTree.TSAsExpression
49
- | TSESTree.TSNonNullExpression
50
- | TSESTree.TSSatisfiesExpression
51
- | TSESTree.TSTypeAssertion;
52
-
53
25
  /**
54
- * The type of the value `node` evaluates to, asked of the expression inside
55
- * its type wrappers rather than of the cast, or `undefined` without type
56
- * information. A `!` among the wrappers still takes `null` and `undefined`
57
- * out, since that much the source asserts about the value itself.
26
+ * Whether `holds` is true of the type of every layer of type wrappers around
27
+ * `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
28
+ * without type information. A widening cast (`s as string | number`) is the
29
+ * author saying the value may be more than it is, and is respected; a
30
+ * narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
31
+ * value's own type does not, and the fix, which keeps the wrapper, has to
32
+ * type-check at every layer too.
58
33
  */
59
- export const getValueType = (
34
+ export const typeHoldsAtEveryLayer = (
60
35
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
61
36
  sourceCode: TSESLint.SourceCode,
62
37
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
63
38
  node: TSESTree.Node,
64
- ): ts.Type | undefined => {
39
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
40
+ holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
41
+ ): boolean => {
65
42
  const services = sourceCode.parserServices;
66
43
 
67
44
  const checker = services?.program?.getTypeChecker();
68
45
 
69
- const tsNode = services?.esTreeNodeToTSNodeMap?.get(skipTypeWrappers(node));
70
-
71
- if (checker === undefined || tsNode === undefined) {
72
- return undefined;
46
+ if (checker === undefined) {
47
+ return false;
73
48
  }
74
49
 
75
- const type = checker.getTypeAtLocation(tsNode);
50
+ return typeWrapperLayers(node).every((layer) => {
51
+ const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
76
52
 
77
- return hasNonNullAssertion(node) ? checker.getNonNullableType(type) : type;
53
+ return (
54
+ tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
55
+ );
56
+ });
78
57
  };
79
-
80
- /**
81
- * The source text of `node` to put in a call's argument list. `getText`
82
- * leaves out the parentheses the source puts around a node, and a sequence
83
- * `(a, b)` without them would be read as two arguments.
84
- */
85
- export const getArgumentText = (
86
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
87
- sourceCode: TSESLint.SourceCode,
88
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
89
- node: TSESTree.Node,
90
- ): string => {
91
- const text = sourceCode.getText(node);
92
-
93
- return node.type === AST_NODE_TYPES.SequenceExpression ? `(${text})` : text;
94
- };
95
-
96
- /**
97
- * The range of `node` together with the parentheses the source puts around it,
98
- * which `node.range` leaves out: all of `((Array.isArray))` for the member
99
- * expression inside.
100
- */
101
- export const getRangeWithParens = (
102
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
103
- sourceCode: TSESLint.SourceCode,
104
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
105
- node: TSESTree.Node,
106
- ): TSESTree.Range => {
107
- const depth = parenthesesDepth(sourceCode, node);
108
-
109
- if (depth === 0) {
110
- return node.range;
111
- }
112
-
113
- const opening = sourceCode.getTokenBefore(node, { skip: depth - 1 });
114
-
115
- const closing = sourceCode.getTokenAfter(node, { skip: depth - 1 });
116
-
117
- return opening === null || closing === null
118
- ? node.range
119
- : [opening.range[0], closing.range[1]];
120
- };
121
-
122
- /** How many pairs of parentheses the source puts around `node`. */
123
- const parenthesesDepth = (
124
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
125
- sourceCode: TSESLint.SourceCode,
126
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
127
- node: TSESTree.Node,
128
- depth: number = 0,
129
- ): number =>
130
- ASTUtils.isParenthesized(depth + 1, node, sourceCode)
131
- ? parenthesesDepth(sourceCode, node, depth + 1)
132
- : depth;
133
-
134
- const hasNonNullAssertion = (
135
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
136
- node: TSESTree.Node,
137
- ): boolean =>
138
- isTypeWrapper(node) &&
139
- (node.type === AST_NODE_TYPES.TSNonNullExpression ||
140
- hasNonNullAssertion(node.expression));
141
-
142
- const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set([
143
- AST_NODE_TYPES.TSAsExpression,
144
- AST_NODE_TYPES.TSNonNullExpression,
145
- AST_NODE_TYPES.TSSatisfiesExpression,
146
- AST_NODE_TYPES.TSTypeAssertion,
147
- ]);
@@ -3,12 +3,13 @@ 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
- getTsStdForgeImport,
11
- } from './import-utils.mjs';
10
+ skipTypeWrappers,
11
+ } from 'eslint-rule-forge';
12
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
12
13
 
13
14
  type Options = readonly [];
14
15
 
@@ -34,7 +35,10 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
34
35
 
35
36
  const program = sourceCode.ast;
36
37
 
37
- const tsStdForgeImport = getTsStdForgeImport(program);
38
+ const tsStdForgeImports = getImportDeclarations(
39
+ program,
40
+ TS_STD_FORGE_MODULE,
41
+ );
38
42
 
39
43
  const mut_nodesToFix: {
40
44
  node: TSESTree.LogicalExpression;
@@ -53,7 +57,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
53
57
  },
54
58
  'Program:exit': () => {
55
59
  const hasIsNonNullObjectImport =
56
- getNamedImports(tsStdForgeImport).includes('isNonNullObject');
60
+ getNamedImports(tsStdForgeImports).includes('isNonNullObject');
57
61
 
58
62
  // The import fix lives on the first report only:
59
63
  // `insertTextBefore(program, …)` produces overlapping ranges
@@ -72,7 +76,9 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
72
76
 
73
77
  const importFixes =
74
78
  index === 0 && !hasIsNonNullObjectImport
75
- ? buildImportFixes(fixer, program, ['isNonNullObject'])
79
+ ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
80
+ 'isNonNullObject',
81
+ ])
76
82
  : [];
77
83
 
78
84
  return [...importFixes, fixer.replaceText(node, replacement)];
@@ -3,19 +3,17 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { SafeArray } from 'ts-std-forge';
7
- import * as ts from 'typescript';
8
6
  import {
7
+ buildNamedImportFix,
9
8
  getArgumentText,
10
- getValueType,
11
- isIdentifierNamed,
12
- skipTypeWrappers,
13
- } from './ast-utils.mjs';
14
- import {
15
- buildImportFixes,
9
+ getImportDeclarations,
16
10
  getNamedImports,
17
- getTsStdForgeImport,
18
- } from './import-utils.mjs';
11
+ skipTypeWrappers,
12
+ } from 'eslint-rule-forge';
13
+ import { SafeArray } from 'ts-std-forge';
14
+ import * as ts from 'typescript';
15
+ import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
16
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
19
17
 
20
18
  type Options = readonly [];
21
19
 
@@ -44,29 +42,27 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
44
42
 
45
43
  const program = sourceCode.ast;
46
44
 
47
- const tsStdForgeImport = getTsStdForgeImport(program);
48
-
49
- const checker = sourceCode.parserServices?.program?.getTypeChecker();
45
+ const tsStdForgeImports = getImportDeclarations(
46
+ program,
47
+ TS_STD_FORGE_MODULE,
48
+ );
50
49
 
51
50
  /**
52
51
  * Whether the `isRecord(...)` half of the rewrite would be dead code
53
52
  * because the object already satisfies `hasKey`'s
54
- * `R extends UnknownRecord` constraint. Without type information nothing
55
- * is known, so the guard is kept. The type is the value's, not a cast's:
56
- * an array cast to a record is still rejected by `isRecord`.
53
+ * `R extends UnknownRecord` constraint, at every layer of type
54
+ * wrappers: an array cast to a record is still rejected by `isRecord`,
55
+ * and a record widened to one that may be an array is the author saying
56
+ * it may need the guard. Without type information nothing is known, so
57
+ * the guard is kept.
57
58
  */
58
59
  const isKnownRecord = (
59
60
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
60
61
  expression: TSESTree.Expression,
61
- ): boolean => {
62
- if (checker === undefined) {
63
- return false;
64
- }
65
-
66
- const type = getValueType(sourceCode, expression);
67
-
68
- return type !== undefined && isAlreadyRecordType(checker, type);
69
- };
62
+ ): boolean =>
63
+ typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) =>
64
+ isAlreadyRecordType(checker, type),
65
+ );
70
66
 
71
67
  const mut_nodesToFix: {
72
68
  node: TSESTree.CallExpression | TSESTree.BinaryExpression;
@@ -139,7 +135,7 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
139
135
  },
140
136
 
141
137
  'Program:exit': () => {
142
- const namedImports = getNamedImports(tsStdForgeImport);
138
+ const namedImports = getNamedImports(tsStdForgeImports);
143
139
 
144
140
  const rewrites = mut_nodesToFix.map((entry) => ({
145
141
  ...entry,
@@ -184,7 +180,12 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
184
180
  // otherwise and only one of them would survive a pass.
185
181
  const importFixes =
186
182
  index === 0 && importsToAdd.length > 0
187
- ? buildImportFixes(fixer, program, importsToAdd)
183
+ ? buildNamedImportFix(
184
+ fixer,
185
+ program,
186
+ TS_STD_FORGE_MODULE,
187
+ importsToAdd,
188
+ )
188
189
  : [];
189
190
 
190
191
  return [...importFixes, fixer.replaceText(node, replacement)];
@@ -125,15 +125,42 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
125
125
  errors: [{ messageId: 'useHasKey' }],
126
126
  },
127
127
  {
128
- name: 'reads `!` on an optional record as the record it asserts',
128
+ // `Object.hasOwn(undefined, 'a')` throws; `isRecord` answers false.
129
+ name: 'keeps isRecord for `!` on an optional record, which leaves undefined in place',
129
130
  code: dedent`
130
131
  declare const obj: Readonly<Record<string, unknown>> | undefined;
131
132
  const ok = Object.hasOwn(obj!, 'a');
132
133
  `,
133
134
  output: dedent`
134
- import { hasKey } from 'ts-std-forge';
135
+ import { isRecord, hasKey } from 'ts-std-forge';
135
136
  declare const obj: Readonly<Record<string, unknown>> | undefined;
136
- const ok = hasKey(obj!, 'a');
137
+ const ok = (isRecord(obj!) && hasKey(obj!, 'a'));
138
+ `,
139
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
140
+ },
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
+ {
155
+ name: 'drops isRecord for a record that is a record at every layer',
156
+ code: dedent`
157
+ declare const obj: Readonly<Record<string, unknown>>;
158
+ const ok = Object.hasOwn(obj! as Readonly<Record<string, unknown>>, 'a');
159
+ `,
160
+ output: dedent`
161
+ import { hasKey } from 'ts-std-forge';
162
+ declare const obj: Readonly<Record<string, unknown>>;
163
+ const ok = hasKey(obj! as Readonly<Record<string, unknown>>, 'a');
137
164
  `,
138
165
  errors: [{ messageId: 'useHasKey' }],
139
166
  },
@@ -4,15 +4,14 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import {
7
+ buildNamedImportFix,
8
+ getImportDeclarations,
9
+ getNamedImports,
7
10
  getRangeWithParens,
8
- isIdentifierNamed,
9
11
  skipTypeWrappers,
10
- } from './ast-utils.mjs';
11
- import {
12
- buildImportFixes,
13
- getNamedImports,
14
- getTsStdForgeImport,
15
- } from './import-utils.mjs';
12
+ } from 'eslint-rule-forge';
13
+ import { isIdentifierNamed } from './ast-utils.mjs';
14
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
16
15
 
17
16
  type Options = readonly [];
18
17
 
@@ -39,7 +38,10 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
39
38
 
40
39
  const program = sourceCode.ast;
41
40
 
42
- const tsStdForgeImport = getTsStdForgeImport(program);
41
+ const tsStdForgeImports = getImportDeclarations(
42
+ program,
43
+ TS_STD_FORGE_MODULE,
44
+ );
43
45
 
44
46
  const mut_nodesToFix: TSESTree.CallExpression[] = [];
45
47
 
@@ -53,7 +55,7 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
53
55
  },
54
56
  'Program:exit': () => {
55
57
  const hasSafeArrayImport =
56
- getNamedImports(tsStdForgeImport).includes('SafeArray');
58
+ getNamedImports(tsStdForgeImports).includes('SafeArray');
57
59
 
58
60
  // The import fix lives on the first report only:
59
61
  // `insertTextBefore(program, …)` produces overlapping ranges
@@ -67,7 +69,9 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
67
69
  fix: (fixer) => {
68
70
  const importFixes =
69
71
  index === 0 && !hasSafeArrayImport
70
- ? buildImportFixes(fixer, program, ['SafeArray'])
72
+ ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
73
+ 'SafeArray',
74
+ ])
71
75
  : [];
72
76
 
73
77
  // Only the callee is replaced, type wrappers included, along
@@ -45,6 +45,20 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
45
45
  `,
46
46
  errors: [{ messageId: 'useSafeArrayIsArray' }],
47
47
  },
48
+ {
49
+ name: 'Sees SafeArray imported by a second ts-std-forge declaration',
50
+ code: dedent`
51
+ import type { Result } from 'ts-std-forge';
52
+ import { SafeArray } from 'ts-std-forge';
53
+ const result = Array.isArray(value);
54
+ `,
55
+ output: dedent`
56
+ import type { Result } from 'ts-std-forge';
57
+ import { SafeArray } from 'ts-std-forge';
58
+ const result = SafeArray.isArray(value);
59
+ `,
60
+ errors: [{ messageId: 'useSafeArrayIsArray' }],
61
+ },
48
62
  {
49
63
  name: 'Replace multiple Array.isArray calls, importing once',
50
64
  code: dedent`
@@ -3,17 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import type { ReadonlyRecord } from 'ts-type-forge';
7
6
  import {
7
+ buildNamedImportFix,
8
8
  getArgumentText,
9
- getValueType,
10
- skipTypeWrappers,
11
- } from './ast-utils.mjs';
12
- import {
13
- buildImportFixes,
9
+ getImportDeclarations,
14
10
  getNamedImports,
15
- getTsStdForgeImport,
16
- } from './import-utils.mjs';
11
+ skipTypeWrappers,
12
+ } from 'eslint-rule-forge';
13
+ import type { ReadonlyRecord } from 'ts-type-forge';
14
+ import { typeHoldsAtEveryLayer } from './ast-utils.mjs';
15
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
17
16
 
18
17
  type Options = readonly [];
19
18
 
@@ -82,31 +81,29 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
82
81
 
83
82
  const program = sourceCode.ast;
84
83
 
85
- const tsStdForgeImport = getTsStdForgeImport(program);
86
-
87
- const checker = sourceCode.parserServices?.program?.getTypeChecker();
84
+ const tsStdForgeImports = getImportDeclarations(
85
+ program,
86
+ TS_STD_FORGE_MODULE,
87
+ );
88
88
 
89
89
  /**
90
90
  * Whether `expression` is an array or tuple. Without type information the
91
91
  * answer is no: `.length` is also a string's, a function's and a
92
- * `TypedArray`'s, and none of those can be handed to the guard. The type is
93
- * the value's, not a cast's: a string cast to an array is still a string.
92
+ * `TypedArray`'s, and none of those can be handed to the guard. It has to
93
+ * be one at every layer of type wrappers: a string cast to an array is
94
+ * still a string, and the fix, which keeps the wrapper, hands the guard
95
+ * the type the outermost layer states.
94
96
  */
95
97
  const isArrayLike = (
96
98
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
97
99
  expression: TSESTree.Expression,
98
- ): boolean => {
99
- if (checker === undefined) {
100
- return false;
101
- }
102
-
103
- const type = getValueType(sourceCode, expression);
104
-
105
- return (
106
- type !== undefined &&
107
- (checker.isArrayType(type) || checker.isTupleType(type))
100
+ ): boolean =>
101
+ typeHoldsAtEveryLayer(
102
+ sourceCode,
103
+ expression,
104
+ (type, checker) =>
105
+ checker.isArrayType(type) || checker.isTupleType(type),
108
106
  );
109
- };
110
107
 
111
108
  const mut_nodesToFix: {
112
109
  node: TSESTree.BinaryExpression;
@@ -130,7 +127,7 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
130
127
  },
131
128
  'Program:exit': () => {
132
129
  const hasSafeArrayImport =
133
- getNamedImports(tsStdForgeImport).includes('SafeArray');
130
+ getNamedImports(tsStdForgeImports).includes('SafeArray');
134
131
 
135
132
  // The import fix lives on the first report only:
136
133
  // `insertTextBefore(program, …)` produces overlapping ranges
@@ -155,7 +152,9 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
155
152
 
156
153
  const importFixes =
157
154
  index === 0 && !hasSafeArrayImport
158
- ? buildImportFixes(fixer, program, ['SafeArray'])
155
+ ? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
156
+ 'SafeArray',
157
+ ])
159
158
  : [];
160
159
 
161
160
  return [...importFixes, fixer.replaceText(node, replacement)];