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.
- package/README.md +15 -14
- package/dist/bindings.d.mts +8 -2
- package/dist/bindings.d.mts.map +1 -1
- package/dist/bindings.mjs +33 -2
- package/dist/bindings.mjs.map +1 -1
- package/dist/imports.d.mts +24 -2
- package/dist/imports.d.mts.map +1 -1
- package/dist/imports.mjs +83 -25
- package/dist/imports.mjs.map +1 -1
- package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-array-is-array.mjs +56 -49
- package/dist/rules/prefer-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +13 -8
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +14 -9
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-parse.mjs +19 -10
- package/dist/rules/prefer-safe-parse.mjs.map +1 -1
- package/package.json +6 -6
- package/src/bindings.mts +43 -3
- package/src/imports.mts +151 -54
- package/src/rules/prefer-array-is-array.mts +25 -18
- package/src/rules/prefer-is-non-null-object.mts +16 -9
- package/src/rules/prefer-is-record-and-has-key.mts +17 -12
- package/src/rules/prefer-safe-parse.mts +26 -21
|
@@ -3,8 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
160
|
+
const withIsRecordAt = bindValueImportsAt(sourceCode, moduleName, [
|
|
158
161
|
'isRecord',
|
|
159
162
|
'hasKey',
|
|
160
163
|
]);
|
|
161
164
|
|
|
162
|
-
const
|
|
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 ?
|
|
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 {
|
|
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 \`
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}: SafeParseRuleConfig): string =>
|
|
252
|
-
`Replace \`{{original}}\` with \`
|
|
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 =>
|