eslint-rule-forge 0.3.2 → 0.4.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 +30 -31
- package/dist/comments.d.mts +10 -0
- package/dist/comments.d.mts.map +1 -0
- package/dist/comments.mjs +11 -0
- package/dist/comments.mjs.map +1 -0
- package/dist/imports.d.mts +47 -6
- package/dist/imports.d.mts.map +1 -1
- package/dist/imports.mjs +123 -6
- package/dist/imports.mjs.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-array-is-array.mjs +13 -19
- 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 +24 -25
- 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 +32 -44
- 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 +16 -15
- package/dist/rules/prefer-safe-parse.mjs.map +1 -1
- package/package.json +4 -4
- package/src/comments.mts +23 -0
- package/src/imports.mts +217 -17
- package/src/index.mts +1 -0
- package/src/rules/prefer-array-is-array.mts +23 -36
- package/src/rules/prefer-is-non-null-object.mts +37 -42
- package/src/rules/prefer-is-record-and-has-key.mts +42 -66
- package/src/rules/prefer-safe-parse.mts +31 -38
|
@@ -3,11 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
getImportDeclarations,
|
|
9
|
-
getNamedImports,
|
|
10
|
-
} from '../imports.mjs';
|
|
6
|
+
import { hasForeignBinding } from '../bindings.mjs';
|
|
7
|
+
import { bindValueImports } from '../imports.mjs';
|
|
11
8
|
import { getRangeWithParens, parenthesesDepth } from '../parentheses.mjs';
|
|
12
9
|
import { skipTypeWrappers } from '../type-wrappers.mjs';
|
|
13
10
|
|
|
@@ -39,11 +36,8 @@ export const createPreferIsNonNullObject = ({
|
|
|
39
36
|
create: (context) => {
|
|
40
37
|
const sourceCode = context.sourceCode;
|
|
41
38
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
45
|
-
|
|
46
|
-
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
39
|
+
const mut_pairsToFix: (NonNullObjectPair &
|
|
40
|
+
Readonly<{ chain: TSESTree.LogicalExpression }>)[] = [];
|
|
47
41
|
|
|
48
42
|
return {
|
|
49
43
|
LogicalExpression: (node) => {
|
|
@@ -52,42 +46,43 @@ export const createPreferIsNonNullObject = ({
|
|
|
52
46
|
return;
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
mut_pairsToFix.push(
|
|
49
|
+
mut_pairsToFix.push(
|
|
50
|
+
...findNonNullObjectPairs(sourceCode, node).map((pair) => ({
|
|
51
|
+
...pair,
|
|
52
|
+
chain: node,
|
|
53
|
+
})),
|
|
54
|
+
);
|
|
56
55
|
},
|
|
57
56
|
'Program:exit': () => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
for (const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const binding = bindValueImports(sourceCode, moduleName, [
|
|
58
|
+
'isNonNullObject',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const localName = binding?.localNames[0];
|
|
62
|
+
|
|
63
|
+
// Every fix brings `isNonNullObject` into scope itself, so that one
|
|
64
|
+
// applied alone (an editor's quick fix) is complete.
|
|
65
|
+
for (const { range, identifierName, chain } of mut_pairsToFix) {
|
|
66
|
+
const loc = {
|
|
67
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
68
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
70
71
|
context.report({
|
|
71
|
-
loc
|
|
72
|
-
start: sourceCode.getLocFromIndex(range[0]),
|
|
73
|
-
end: sourceCode.getLocFromIndex(range[1]),
|
|
74
|
-
},
|
|
72
|
+
loc,
|
|
75
73
|
messageId: 'useIsNonNullObject',
|
|
76
|
-
fix:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fixer.replaceTextRange(range, replacement),
|
|
89
|
-
];
|
|
90
|
-
},
|
|
74
|
+
fix:
|
|
75
|
+
binding === undefined ||
|
|
76
|
+
localName === undefined ||
|
|
77
|
+
hasForeignBinding(sourceCode, chain, localName, moduleName)
|
|
78
|
+
? null
|
|
79
|
+
: (fixer) => [
|
|
80
|
+
...binding.edits(fixer),
|
|
81
|
+
fixer.replaceTextRange(
|
|
82
|
+
range,
|
|
83
|
+
`${localName}(${identifierName})`,
|
|
84
|
+
),
|
|
85
|
+
],
|
|
91
86
|
});
|
|
92
87
|
}
|
|
93
88
|
},
|
|
@@ -4,11 +4,8 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
getImportDeclarations,
|
|
10
|
-
getNamedImports,
|
|
11
|
-
} from '../imports.mjs';
|
|
7
|
+
import { hasForeignBinding } from '../bindings.mjs';
|
|
8
|
+
import { bindValueImports } from '../imports.mjs';
|
|
12
9
|
import { getArgumentText } from '../parentheses.mjs';
|
|
13
10
|
import {
|
|
14
11
|
isIdentifierNamed,
|
|
@@ -45,10 +42,6 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
45
42
|
create: (context) => {
|
|
46
43
|
const sourceCode = context.sourceCode;
|
|
47
44
|
|
|
48
|
-
const program = sourceCode.ast;
|
|
49
|
-
|
|
50
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
51
|
-
|
|
52
45
|
/**
|
|
53
46
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
54
47
|
* because the object already satisfies `hasKey`'s
|
|
@@ -159,81 +152,64 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
159
152
|
},
|
|
160
153
|
|
|
161
154
|
'Program:exit': () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
155
|
+
// Bound once for each shape of rewrite: a file may import `hasKey`
|
|
156
|
+
// but not `isRecord`, which only the rewrites that need it add.
|
|
157
|
+
const withIsRecord = bindValueImports(sourceCode, moduleName, [
|
|
158
|
+
'isRecord',
|
|
159
|
+
'hasKey',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const hasKeyOnly = bindValueImports(sourceCode, moduleName, [
|
|
163
|
+
'hasKey',
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
for (const { node, objExpression, keyExpression } of mut_nodesToFix) {
|
|
165
167
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
166
168
|
// true, so it is left out of the replacement entirely.
|
|
167
|
-
const needsIsRecord = !isKnownRecord(
|
|
169
|
+
const needsIsRecord = !isKnownRecord(objExpression);
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
...entry,
|
|
171
|
-
needsIsRecord,
|
|
172
|
-
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
173
|
-
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
171
|
+
const binding = needsIsRecord ? withIsRecord : hasKeyOnly;
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
fixableRewrites.length === 0
|
|
181
|
-
? []
|
|
182
|
-
: [
|
|
183
|
-
...(fixableRewrites.some(
|
|
184
|
-
({ needsIsRecord }) => needsIsRecord,
|
|
185
|
-
) && !namedImports.includes('isRecord')
|
|
186
|
-
? ['isRecord']
|
|
187
|
-
: []),
|
|
188
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
189
|
-
];
|
|
173
|
+
const hasKeyName = binding?.localNames.at(-1);
|
|
190
174
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
195
|
-
rewrite;
|
|
175
|
+
const isRecordName = needsIsRecord
|
|
176
|
+
? binding?.localNames[0]
|
|
177
|
+
: undefined;
|
|
196
178
|
|
|
197
179
|
const objText = getArgumentText(sourceCode, objExpression);
|
|
198
180
|
|
|
199
181
|
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
200
182
|
|
|
201
|
-
const
|
|
183
|
+
const fixable =
|
|
184
|
+
binding !== undefined &&
|
|
185
|
+
hasKeyName !== undefined &&
|
|
186
|
+
(!needsIsRecord || isRecordName !== undefined) &&
|
|
187
|
+
// `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
|
+
);
|
|
202
193
|
|
|
203
194
|
context.report({
|
|
204
195
|
node,
|
|
205
196
|
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
206
197
|
data: {
|
|
207
|
-
original:
|
|
198
|
+
original: sourceCode.getText(node),
|
|
208
199
|
objName: objText,
|
|
209
200
|
keyName: keyText,
|
|
210
201
|
},
|
|
211
|
-
fix:
|
|
212
|
-
?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const importFixes =
|
|
223
|
-
rewrite === importCarrier && importsToAdd.length > 0
|
|
224
|
-
? buildNamedImportFix(
|
|
225
|
-
fixer,
|
|
226
|
-
program,
|
|
227
|
-
moduleName,
|
|
228
|
-
importsToAdd,
|
|
229
|
-
)
|
|
230
|
-
: [];
|
|
231
|
-
|
|
232
|
-
return [
|
|
233
|
-
...importFixes,
|
|
234
|
-
fixer.replaceText(node, replacement),
|
|
235
|
-
];
|
|
236
|
-
},
|
|
202
|
+
fix: fixable
|
|
203
|
+
? (fixer) => [
|
|
204
|
+
...binding.edits(fixer),
|
|
205
|
+
fixer.replaceText(
|
|
206
|
+
node,
|
|
207
|
+
needsIsRecord
|
|
208
|
+
? `(${isRecordName}(${objText}) && ${hasKeyName}(${objText}, ${keyText}))`
|
|
209
|
+
: `${hasKeyName}(${objText}, ${keyText})`,
|
|
210
|
+
),
|
|
211
|
+
]
|
|
212
|
+
: null,
|
|
237
213
|
});
|
|
238
214
|
}
|
|
239
215
|
},
|
|
@@ -5,11 +5,8 @@ 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
|
-
|
|
10
|
-
getImportDeclarations,
|
|
11
|
-
getNamedImports,
|
|
12
|
-
} from '../imports.mjs';
|
|
8
|
+
import { hasForeignBinding } from '../bindings.mjs';
|
|
9
|
+
import { bindValueImports } from '../imports.mjs';
|
|
13
10
|
import { getArgumentText } from '../parentheses.mjs';
|
|
14
11
|
import {
|
|
15
12
|
isIdentifierNamed,
|
|
@@ -111,8 +108,6 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
111
108
|
) => boolean;
|
|
112
109
|
}>,
|
|
113
110
|
): TSESLint.RuleModule<MessageId, readonly []> => {
|
|
114
|
-
const parseFunction = `${namespace}.${method}` as const;
|
|
115
|
-
|
|
116
111
|
// Sorted, as an import sorter would write them.
|
|
117
112
|
const requiredImports = [namespace, 'Result'].toSorted();
|
|
118
113
|
|
|
@@ -129,10 +124,6 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
129
124
|
create: (context) => {
|
|
130
125
|
const sourceCode = context.sourceCode;
|
|
131
126
|
|
|
132
|
-
const program = sourceCode.ast;
|
|
133
|
-
|
|
134
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
135
|
-
|
|
136
127
|
const mut_nodesToFix: {
|
|
137
128
|
node: TSESTree.CallExpression;
|
|
138
129
|
argExpression: TSESTree.Expression;
|
|
@@ -169,19 +160,22 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
169
160
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
170
161
|
},
|
|
171
162
|
'Program:exit': () => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
const binding = bindValueImports(
|
|
164
|
+
sourceCode,
|
|
165
|
+
moduleName,
|
|
166
|
+
requiredImports,
|
|
176
167
|
);
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
169
|
+
const localNameOf = (name: string): string | undefined =>
|
|
170
|
+
binding?.localNames[requiredImports.indexOf(name)];
|
|
171
|
+
|
|
172
|
+
const namespaceName = localNameOf(namespace);
|
|
173
|
+
|
|
174
|
+
const resultName = localNameOf('Result');
|
|
175
|
+
|
|
176
|
+
// Every fix brings its names into scope itself, so that one applied
|
|
177
|
+
// alone (an editor's quick fix) is complete.
|
|
178
|
+
for (const { node, argExpression } of mut_nodesToFix) {
|
|
185
179
|
const argText = getArgumentText(sourceCode, argExpression);
|
|
186
180
|
|
|
187
181
|
context.report({
|
|
@@ -191,22 +185,21 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
191
185
|
original: sourceCode.getText(node),
|
|
192
186
|
argName: argText,
|
|
193
187
|
},
|
|
194
|
-
fix:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
},
|
|
188
|
+
fix:
|
|
189
|
+
binding === undefined ||
|
|
190
|
+
namespaceName === undefined ||
|
|
191
|
+
resultName === undefined ||
|
|
192
|
+
[namespaceName, resultName].some((name) =>
|
|
193
|
+
hasForeignBinding(sourceCode, node, name, moduleName),
|
|
194
|
+
)
|
|
195
|
+
? null
|
|
196
|
+
: (fixer) => [
|
|
197
|
+
...binding.edits(fixer),
|
|
198
|
+
fixer.replaceText(
|
|
199
|
+
node,
|
|
200
|
+
`${resultName}.unwrapOkOr(${namespaceName}.${method}(${argText}), Number.NaN)`,
|
|
201
|
+
),
|
|
202
|
+
],
|
|
210
203
|
});
|
|
211
204
|
}
|
|
212
205
|
},
|