eslint-rule-forge 0.3.1 → 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 +32 -33
- 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/negation.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.mjs +1 -1
- package/dist/rules/no-side-effect-import.mjs.map +1 -1
- package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-array-is-array.mjs +14 -20
- 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 +25 -26
- 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 +33 -45
- 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 +17 -16
- package/dist/rules/prefer-safe-parse.mjs.map +1 -1
- package/dist/type-wrappers.mjs.map +1 -1
- package/package.json +7 -7
- package/src/comments.mts +23 -0
- package/src/imports.mts +217 -17
- package/src/index.mts +1 -0
- package/src/negation.mts +26 -24
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/prefer-array-is-array.mts +24 -37
- package/src/rules/prefer-is-non-null-object.mts +38 -43
- package/src/rules/prefer-is-record-and-has-key.mts +43 -67
- package/src/rules/prefer-safe-parse.mts +32 -39
- package/src/type-wrappers.mts +1 -1
|
@@ -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,
|
|
@@ -39,15 +36,12 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
39
36
|
useIsRecordAndHasKey: `Replace \`{{original}}\` with \`isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})\` from ${moduleName}.`,
|
|
40
37
|
useHasKey: `Replace \`{{original}}\` with \`hasKey({{objName}}, {{keyName}})\` from ${moduleName}.`,
|
|
41
38
|
},
|
|
39
|
+
defaultOptions: [],
|
|
42
40
|
},
|
|
43
41
|
|
|
44
42
|
create: (context) => {
|
|
45
43
|
const sourceCode = context.sourceCode;
|
|
46
44
|
|
|
47
|
-
const program = sourceCode.ast;
|
|
48
|
-
|
|
49
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
50
|
-
|
|
51
45
|
/**
|
|
52
46
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
53
47
|
* because the object already satisfies `hasKey`'s
|
|
@@ -158,87 +152,69 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
158
152
|
},
|
|
159
153
|
|
|
160
154
|
'Program:exit': () => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
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) {
|
|
164
167
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
165
168
|
// true, so it is left out of the replacement entirely.
|
|
166
|
-
const needsIsRecord = !isKnownRecord(
|
|
169
|
+
const needsIsRecord = !isKnownRecord(objExpression);
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
...entry,
|
|
170
|
-
needsIsRecord,
|
|
171
|
-
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
172
|
-
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
173
|
-
};
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
171
|
+
const binding = needsIsRecord ? withIsRecord : hasKeyOnly;
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
fixableRewrites.length === 0
|
|
180
|
-
? []
|
|
181
|
-
: [
|
|
182
|
-
...(fixableRewrites.some(
|
|
183
|
-
({ needsIsRecord }) => needsIsRecord,
|
|
184
|
-
) && !namedImports.includes('isRecord')
|
|
185
|
-
? ['isRecord']
|
|
186
|
-
: []),
|
|
187
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
188
|
-
];
|
|
173
|
+
const hasKeyName = binding?.localNames.at(-1);
|
|
189
174
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
194
|
-
rewrite;
|
|
175
|
+
const isRecordName = needsIsRecord
|
|
176
|
+
? binding?.localNames[0]
|
|
177
|
+
: undefined;
|
|
195
178
|
|
|
196
179
|
const objText = getArgumentText(sourceCode, objExpression);
|
|
197
180
|
|
|
198
181
|
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
199
182
|
|
|
200
|
-
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
|
+
);
|
|
201
193
|
|
|
202
194
|
context.report({
|
|
203
195
|
node,
|
|
204
196
|
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
205
197
|
data: {
|
|
206
|
-
original:
|
|
198
|
+
original: sourceCode.getText(node),
|
|
207
199
|
objName: objText,
|
|
208
200
|
keyName: keyText,
|
|
209
201
|
},
|
|
210
|
-
fix:
|
|
211
|
-
?
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const importFixes =
|
|
222
|
-
rewrite === importCarrier && importsToAdd.length > 0
|
|
223
|
-
? buildNamedImportFix(
|
|
224
|
-
fixer,
|
|
225
|
-
program,
|
|
226
|
-
moduleName,
|
|
227
|
-
importsToAdd,
|
|
228
|
-
)
|
|
229
|
-
: [];
|
|
230
|
-
|
|
231
|
-
return [
|
|
232
|
-
...importFixes,
|
|
233
|
-
fixer.replaceText(node, replacement),
|
|
234
|
-
];
|
|
235
|
-
},
|
|
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,
|
|
236
213
|
});
|
|
237
214
|
}
|
|
238
215
|
},
|
|
239
216
|
};
|
|
240
217
|
},
|
|
241
|
-
defaultOptions: [],
|
|
242
218
|
}) as const;
|
|
243
219
|
|
|
244
220
|
/**
|
|
@@ -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
|
|
|
@@ -123,15 +118,12 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
123
118
|
fixable: 'code',
|
|
124
119
|
schema: [],
|
|
125
120
|
messages,
|
|
121
|
+
defaultOptions: [],
|
|
126
122
|
},
|
|
127
123
|
|
|
128
124
|
create: (context) => {
|
|
129
125
|
const sourceCode = context.sourceCode;
|
|
130
126
|
|
|
131
|
-
const program = sourceCode.ast;
|
|
132
|
-
|
|
133
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
134
|
-
|
|
135
127
|
const mut_nodesToFix: {
|
|
136
128
|
node: TSESTree.CallExpression;
|
|
137
129
|
argExpression: TSESTree.Expression;
|
|
@@ -168,19 +160,22 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
168
160
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
169
161
|
},
|
|
170
162
|
'Program:exit': () => {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
163
|
+
const binding = bindValueImports(
|
|
164
|
+
sourceCode,
|
|
165
|
+
moduleName,
|
|
166
|
+
requiredImports,
|
|
175
167
|
);
|
|
176
168
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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) {
|
|
184
179
|
const argText = getArgumentText(sourceCode, argExpression);
|
|
185
180
|
|
|
186
181
|
context.report({
|
|
@@ -190,28 +185,26 @@ const createSafeParseRule = <MessageId extends string>(
|
|
|
190
185
|
original: sourceCode.getText(node),
|
|
191
186
|
argName: argText,
|
|
192
187
|
},
|
|
193
|
-
fix:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
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
|
+
],
|
|
209
203
|
});
|
|
210
204
|
}
|
|
211
205
|
},
|
|
212
206
|
};
|
|
213
207
|
},
|
|
214
|
-
defaultOptions: [],
|
|
215
208
|
} as const;
|
|
216
209
|
};
|
|
217
210
|
|
package/src/type-wrappers.mts
CHANGED
|
@@ -119,7 +119,7 @@ export const typeHoldsAtEveryLayer = (
|
|
|
119
119
|
});
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set([
|
|
122
|
+
const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set<AST_NODE_TYPES>([
|
|
123
123
|
AST_NODE_TYPES.TSAsExpression,
|
|
124
124
|
AST_NODE_TYPES.TSNonNullExpression,
|
|
125
125
|
AST_NODE_TYPES.TSSatisfiesExpression,
|