eslint-rule-forge 0.5.0 → 0.6.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.
@@ -3,8 +3,7 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { hasForeignBinding } from '../bindings.mjs';
7
- import { bindValueImports } from '../imports.mjs';
6
+ import { bindValueImportsAt } from '../imports.mjs';
8
7
  import { getRangeWithParens, parenthesesDepth } from '../parentheses.mjs';
9
8
  import { skipTypeWrappers } from '../type-wrappers.mjs';
10
9
 
@@ -28,7 +27,7 @@ export const createPreferIsNonNullObject = ({
28
27
  fixable: 'code',
29
28
  schema: [],
30
29
  messages: {
31
- useIsNonNullObject: `Replace the object/null check with \`isNonNullObject()\` from ${moduleName}.`,
30
+ useIsNonNullObject: `Replace the object/null check with \`{{isNonNullObject}}()\` from ${moduleName}.`,
32
31
  },
33
32
  defaultOptions: [],
34
33
  },
@@ -54,15 +53,21 @@ export const createPreferIsNonNullObject = ({
54
53
  );
55
54
  },
56
55
  'Program:exit': () => {
57
- const binding = bindValueImports(sourceCode, moduleName, [
56
+ if (mut_pairsToFix.length === 0) {
57
+ return;
58
+ }
59
+
60
+ const bindingAt = bindValueImportsAt(sourceCode, moduleName, [
58
61
  'isNonNullObject',
59
62
  ]);
60
63
 
61
- const localName = binding?.localNames[0];
62
-
63
64
  // Every fix brings `isNonNullObject` into scope itself, so that one
64
65
  // applied alone (an editor's quick fix) is complete.
65
66
  for (const { range, identifierName, chain } of mut_pairsToFix) {
67
+ const binding = bindingAt(chain);
68
+
69
+ const localName = binding?.localNames[0];
70
+
66
71
  const loc = {
67
72
  start: sourceCode.getLocFromIndex(range[0]),
68
73
  end: sourceCode.getLocFromIndex(range[1]),
@@ -71,10 +76,12 @@ export const createPreferIsNonNullObject = ({
71
76
  context.report({
72
77
  loc,
73
78
  messageId: 'useIsNonNullObject',
79
+ data: {
80
+ // The name the fix writes; with no fix, the export's own.
81
+ isNonNullObject: localName ?? 'isNonNullObject',
82
+ },
74
83
  fix:
75
- binding === undefined ||
76
- localName === undefined ||
77
- hasForeignBinding(sourceCode, chain, localName, moduleName)
84
+ binding === undefined || localName === undefined
78
85
  ? null
79
86
  : (fixer) => [
80
87
  ...binding.edits(fixer),
@@ -4,8 +4,7 @@ import {
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
6
  import * as ts from 'typescript';
7
- import { hasForeignBinding } from '../bindings.mjs';
8
- import { bindValueImports } from '../imports.mjs';
7
+ import { bindValueImportsAt } from '../imports.mjs';
9
8
  import { getArgumentText } from '../parentheses.mjs';
10
9
  import {
11
10
  isIdentifierNamed,
@@ -33,8 +32,8 @@ export const createPreferIsRecordAndHasKey = ({
33
32
  fixable: 'code',
34
33
  schema: [],
35
34
  messages: {
36
- useIsRecordAndHasKey: `Replace \`{{original}}\` with \`isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})\` from ${moduleName}.`,
37
- useHasKey: `Replace \`{{original}}\` with \`hasKey({{objName}}, {{keyName}})\` from ${moduleName}.`,
35
+ useIsRecordAndHasKey: `Replace \`{{original}}\` with \`{{isRecord}}({{objName}}) && {{hasKey}}({{objName}}, {{keyName}})\` from ${moduleName}.`,
36
+ useHasKey: `Replace \`{{original}}\` with \`{{hasKey}}({{objName}}, {{keyName}})\` from ${moduleName}.`,
38
37
  },
39
38
  defaultOptions: [],
40
39
  },
@@ -152,14 +151,18 @@ export const createPreferIsRecordAndHasKey = ({
152
151
  },
153
152
 
154
153
  'Program:exit': () => {
154
+ if (mut_nodesToFix.length === 0) {
155
+ return;
156
+ }
157
+
155
158
  // Bound once for each shape of rewrite: a file may import `hasKey`
156
159
  // but not `isRecord`, which only the rewrites that need it add.
157
- const withIsRecord = bindValueImports(sourceCode, moduleName, [
160
+ const withIsRecordAt = bindValueImportsAt(sourceCode, moduleName, [
158
161
  'isRecord',
159
162
  'hasKey',
160
163
  ]);
161
164
 
162
- const hasKeyOnly = bindValueImports(sourceCode, moduleName, [
165
+ const hasKeyOnlyAt = bindValueImportsAt(sourceCode, moduleName, [
163
166
  'hasKey',
164
167
  ]);
165
168
 
@@ -168,7 +171,9 @@ export const createPreferIsRecordAndHasKey = ({
168
171
  // true, so it is left out of the replacement entirely.
169
172
  const needsIsRecord = !isKnownRecord(objExpression);
170
173
 
171
- const binding = needsIsRecord ? withIsRecord : hasKeyOnly;
174
+ const binding = (needsIsRecord ? withIsRecordAt : hasKeyOnlyAt)(
175
+ node,
176
+ );
172
177
 
173
178
  const hasKeyName = binding?.localNames.at(-1);
174
179
 
@@ -185,11 +190,7 @@ export const createPreferIsRecordAndHasKey = ({
185
190
  hasKeyName !== undefined &&
186
191
  (!needsIsRecord || isRecordName !== undefined) &&
187
192
  // `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
188
- (!needsIsRecord || isSideEffectFree(objExpression)) &&
189
- binding.localNames.every(
190
- (name) =>
191
- !hasForeignBinding(sourceCode, node, name, moduleName),
192
- );
193
+ (!needsIsRecord || isSideEffectFree(objExpression));
193
194
 
194
195
  context.report({
195
196
  node,
@@ -198,6 +199,10 @@ export const createPreferIsRecordAndHasKey = ({
198
199
  original: sourceCode.getText(node),
199
200
  objName: objText,
200
201
  keyName: keyText,
202
+ // The names a fix here writes; where none can bind them, the
203
+ // exports' own.
204
+ isRecord: isRecordName ?? 'isRecord',
205
+ hasKey: hasKeyName ?? 'hasKey',
201
206
  },
202
207
  fix: fixable
203
208
  ? (fixer) => [
@@ -5,8 +5,7 @@ import {
5
5
  } from '@typescript-eslint/utils';
6
6
  import type { ReadonlyRecord } from 'ts-type-forge';
7
7
  import * as ts from 'typescript';
8
- import { hasForeignBinding } from '../bindings.mjs';
9
- import { bindValueImports } from '../imports.mjs';
8
+ import { bindValueImportsAt } from '../imports.mjs';
10
9
  import { getArgumentText } from '../parentheses.mjs';
11
10
  import {
12
11
  isIdentifierNamed,
@@ -142,7 +141,7 @@ const createSafeParseRule = <MessageId extends string>(
142
141
  schema: [],
143
142
  messages: {
144
143
  ...messages,
145
- replaceWithSafeParse: `Replace with \`Result.unwrapOkOr(${namespace}.${method}({{argName}}), Number.NaN)\`.`,
144
+ replaceWithSafeParse: `Replace with \`{{resultName}}.unwrapOkOr({{namespaceName}}.${method}({{argName}}), Number.NaN)\`.`,
146
145
  },
147
146
  defaultOptions: [],
148
147
  },
@@ -185,30 +184,39 @@ const createSafeParseRule = <MessageId extends string>(
185
184
  mut_reports.push({ node, argExpression: firstArg });
186
185
  },
187
186
  'Program:exit': () => {
188
- const binding = bindValueImports(
187
+ if (mut_reports.length === 0) {
188
+ return;
189
+ }
190
+
191
+ const bindingAt = bindValueImportsAt(
189
192
  sourceCode,
190
193
  moduleName,
191
194
  requiredImports,
192
195
  );
193
196
 
194
- const localNameOf = (name: string): string | undefined =>
195
- binding?.localNames[requiredImports.indexOf(name)];
196
-
197
- const namespaceName = localNameOf(namespace);
198
-
199
- const resultName = localNameOf('Result');
200
-
201
197
  // A suggestion, never an autofix: the replacement parses some
202
198
  // strings differently (`parseInt('1e3')` is 1, the rewrite 1000;
203
199
  // `parseFloat('12px')` is 12, the rewrite NaN; `Number('')` is 0, the
204
200
  // rewrite NaN), so applying it is a person's decision. Each one
205
201
  // brings its names into scope itself, so that it is complete alone.
206
202
  for (const { node, argExpression } of mut_reports) {
203
+ const binding = bindingAt(node);
204
+
205
+ const localNameOf = (name: string): string | undefined =>
206
+ binding?.localNames[requiredImports.indexOf(name)];
207
+
208
+ const namespaceName = localNameOf(namespace);
209
+
210
+ const resultName = localNameOf('Result');
211
+
207
212
  const argText = getArgumentText(sourceCode, argExpression);
208
213
 
209
214
  const data = {
210
215
  original: sourceCode.getText(node),
211
216
  argName: argText,
217
+ // The names the suggestion writes; with none, the exports' own.
218
+ namespaceName: namespaceName ?? namespace,
219
+ resultName: resultName ?? 'Result',
212
220
  } as const;
213
221
 
214
222
  context.report({
@@ -218,10 +226,7 @@ const createSafeParseRule = <MessageId extends string>(
218
226
  suggest:
219
227
  binding === undefined ||
220
228
  namespaceName === undefined ||
221
- resultName === undefined ||
222
- [namespaceName, resultName].some((name) =>
223
- hasForeignBinding(sourceCode, node, name, moduleName),
224
- )
229
+ resultName === undefined
225
230
  ? []
226
231
  : [
227
232
  {
@@ -244,12 +249,12 @@ const createSafeParseRule = <MessageId extends string>(
244
249
  } as const;
245
250
  };
246
251
 
247
- const messageText = ({
248
- moduleName,
249
- namespace,
250
- method,
251
- }: SafeParseRuleConfig): string =>
252
- `Replace \`{{original}}\` with \`Result.unwrapOkOr(${namespace}.${method}({{argName}}), Number.NaN)\` from ${moduleName}. It parses some strings differently from the call it replaces, so check the input before applying it.` as const;
252
+ /**
253
+ * The report's message. `{{namespaceName}}` and `{{resultName}}` are the names
254
+ * the suggestion writes.
255
+ */
256
+ const messageText = ({ moduleName, method }: SafeParseRuleConfig): string =>
257
+ `Replace \`{{original}}\` with \`{{resultName}}.unwrapOkOr({{namespaceName}}.${method}({{argName}}), Number.NaN)\` from ${moduleName}. It parses some strings differently from the call it replaces, so check the input before applying it.` as const;
253
258
 
254
259
  /** Whether `callee` is the global function `name`, as in `parseInt(...)`. */
255
260
  const isGlobalCall = (callee: TSESTree.Expression, name: string): boolean =>