@blumintinc/eslint-plugin-blumint 1.20.134 → 1.20.136
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/lib/index.js +1 -1
- package/lib/rules/consistent-callback-naming.js +16 -0
- package/lib/rules/enforce-assert-safe-object-key.js +403 -9
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +40 -4
- package/lib/rules/enforce-id-capitalization.js +18 -0
- package/lib/rules/enforce-props-argument-name.js +28 -2
- package/lib/rules/enforce-props-naming-consistency.js +28 -2
- package/lib/rules/no-inline-component-prop.js +8 -5
- package/lib/rules/no-object-values-on-strings.js +23 -6
- package/lib/rules/no-passthrough-getters.js +10 -2
- package/lib/rules/no-redundant-annotation-assertion.js +611 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +46 -4
- package/lib/rules/no-useless-usememo-primitives.js +30 -9
- package/lib/rules/no-usememo-for-pass-by-value.js +185 -31
- package/lib/rules/parallelize-async-operations.js +10 -1
- package/lib/rules/prefer-map-over-conditional-dispatch.js +36 -8
- package/lib/utils/fixtureCorpus.d.ts +148 -8
- package/lib/utils/fixtureCorpus.js +207 -10
- package/lib/utils/harvestRuleTesterCases.d.ts +15 -0
- package/lib/utils/harvestRuleTesterCases.js +30 -3
- package/lib/utils/importRemoval.js +35 -6
- package/lib/utils/replacementSegments.d.ts +35 -0
- package/lib/utils/replacementSegments.js +55 -0
- package/package.json +1 -1
- package/release-manifest.json +143 -0
|
@@ -248,6 +248,27 @@ function declaredTypeNode(node) {
|
|
|
248
248
|
function checksExcessProperties(typeNode) {
|
|
249
249
|
return typeNode !== null && !UNCHECKED_ANNOTATION_TYPES.has(typeNode.type);
|
|
250
250
|
}
|
|
251
|
+
/** `as const` declares no members of its own, so it pins no name. */
|
|
252
|
+
function isConstAssertionType(typeNode) {
|
|
253
|
+
return (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
254
|
+
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
255
|
+
typeNode.typeName.name === 'const');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* An `as T` / `<T>` whose target type declares members the value must still
|
|
259
|
+
* have. Renaming one of them makes the assertion uncomparable (TS2352), so the
|
|
260
|
+
* name is dictated by `T` rather than chosen by the author — the same reasoning
|
|
261
|
+
* the annotation arms use, applied to the check an assertion actually performs.
|
|
262
|
+
*/
|
|
263
|
+
function isCheckedTypeAssertion(node) {
|
|
264
|
+
if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
265
|
+
node.type !== utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const { typeAnnotation } = node;
|
|
269
|
+
return (checksExcessProperties(typeAnnotation) &&
|
|
270
|
+
!isConstAssertionType(typeAnnotation));
|
|
271
|
+
}
|
|
251
272
|
function isFunctionNode(node) {
|
|
252
273
|
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
253
274
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
@@ -310,10 +331,18 @@ const EXPRESSION_ASSERTION_TYPES = new Set([
|
|
|
310
331
|
* members, and climbs THROUGH assertion wrappers, which change no runtime value
|
|
311
332
|
* and so cannot detach a literal from the declared type it is assigned to
|
|
312
333
|
* (#1597) — `enforce-object-literal-as-const` ships in the same recommended
|
|
313
|
-
* config and appends `as const` to exactly these literals by `--fix`.
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
334
|
+
* config and appends `as const` to exactly these literals by `--fix`.
|
|
335
|
+
*
|
|
336
|
+
* An `as T` is itself a signal when `T` declares members. It is true that an
|
|
337
|
+
* `as` clause does not reject EXCESS members the way an annotation does
|
|
338
|
+
* (`{ orderBy, extra } as Q` compiles; `const c: Q = { orderBy, extra }` is
|
|
339
|
+
* TS2322) — but the operation being gated is a RENAME, which removes a REQUIRED
|
|
340
|
+
* member, and that an assertion does reject: `{ order } as Q` is TS2352 when `Q`
|
|
341
|
+
* requires `orderBy`. Reasoning from excess properties alone made the rule
|
|
342
|
+
* demand a rename that does not compile (#1885). `as const` is the exception
|
|
343
|
+
* and stays transparent, declaring no members of its own.
|
|
344
|
+
*
|
|
345
|
+
* The walk stops at anything else.
|
|
317
346
|
*/
|
|
318
347
|
function hasConformanceSignal(node) {
|
|
319
348
|
let current = node;
|
|
@@ -330,6 +359,19 @@ function hasConformanceSignal(node) {
|
|
|
330
359
|
checksExcessProperties(parent.typeAnnotation)) {
|
|
331
360
|
return true;
|
|
332
361
|
}
|
|
362
|
+
// An `as T` DOES pin the member names it requires, even though it does not
|
|
363
|
+
// reject excess ones. The two are different checks: excess-property
|
|
364
|
+
// checking is what an annotation adds, but a RENAME removes a REQUIRED
|
|
365
|
+
// member, and that breaks comparability — `{ beta: 1 } as T` is TS2352 when
|
|
366
|
+
// `T` requires `alpha`. Reasoning from excess properties alone made the rule
|
|
367
|
+
// demand a rename that does not compile (#1885). `as const` stays
|
|
368
|
+
// transparent: it declares no members of its own, and
|
|
369
|
+
// `enforce-object-literal-as-const` appends one to exactly these literals.
|
|
370
|
+
if (isCheckedTypeAssertion(parent) &&
|
|
371
|
+
parent
|
|
372
|
+
.expression === current) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
333
375
|
if (EXPRESSION_ASSERTION_TYPES.has(parent.type)) {
|
|
334
376
|
current = parent;
|
|
335
377
|
continue;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.noUselessUsememoPrimitives = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const replacementSegments_1 = require("../utils/replacementSegments");
|
|
6
7
|
const tsTypeClassifier_1 = require("../utils/tsTypeClassifier");
|
|
7
8
|
const DEFAULT_OPTIONS = {
|
|
8
9
|
ignoreCallExpressions: true,
|
|
@@ -381,24 +382,44 @@ exports.noUselessUsememoPrimitives = (0, createRule_1.createRule)({
|
|
|
381
382
|
valueKind,
|
|
382
383
|
},
|
|
383
384
|
fix(fixer) {
|
|
385
|
+
const expressionText = sourceCode.getText(returnedExpression);
|
|
384
386
|
// Inlining replaces the entire useMemo(...) call with the returned
|
|
385
387
|
// expression's text, so any comment inside the call but outside
|
|
386
388
|
// that expression — an eslint-disable-next-line directive on the
|
|
387
389
|
// return statement among them — has no representation in the
|
|
388
|
-
// replacement
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// the
|
|
390
|
+
// replacement. Dropping one changes which rules report on the file
|
|
391
|
+
// (#1591), and declining the fix whenever one is present makes a
|
|
392
|
+
// comment decide whether the rule rewrites at all (#1877). Both are
|
|
393
|
+
// avoided by carrying every such comment into the replacement,
|
|
394
|
+
// where the directives among them keep the line relationship they
|
|
395
|
+
// were written with.
|
|
393
396
|
const strandedComments = sourceCode
|
|
394
397
|
.getCommentsInside(node)
|
|
395
398
|
.filter((comment) => comment.range[0] < returnedExpression.range[0] ||
|
|
396
399
|
comment.range[1] > returnedExpression.range[1]);
|
|
397
|
-
if (strandedComments.length
|
|
398
|
-
return
|
|
400
|
+
if (strandedComments.length === 0) {
|
|
401
|
+
return fixer.replaceText(node, `(${expressionText})`);
|
|
399
402
|
}
|
|
400
|
-
|
|
401
|
-
|
|
403
|
+
// A comment inside the call lies wholly on one side of the
|
|
404
|
+
// expression, since a comment is a token and cannot straddle a
|
|
405
|
+
// node; keeping each on its own side preserves what it annotates.
|
|
406
|
+
const toSegment = (comment) => ({
|
|
407
|
+
text: sourceCode.text.slice(comment.range[0], comment.range[1]),
|
|
408
|
+
breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
|
|
409
|
+
});
|
|
410
|
+
const isBefore = (comment) => comment.range[0] < returnedExpression.range[0];
|
|
411
|
+
const segments = [
|
|
412
|
+
...strandedComments.filter(isBefore).map(toSegment),
|
|
413
|
+
{ text: expressionText, breakAfter: false },
|
|
414
|
+
...strandedComments
|
|
415
|
+
.filter((comment) => !isBefore(comment))
|
|
416
|
+
.map(toSegment),
|
|
417
|
+
];
|
|
418
|
+
// The call can start mid-line, so the indentation of the line it
|
|
419
|
+
// opens on is the only anchor the carried comments have.
|
|
420
|
+
const startLine = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
421
|
+
const indent = /^[\t ]*/.exec(startLine)?.[0] ?? '';
|
|
422
|
+
return fixer.replaceText(node, (0, replacementSegments_1.joinSegments)(segments, indent));
|
|
402
423
|
},
|
|
403
424
|
});
|
|
404
425
|
},
|
|
@@ -7,6 +7,7 @@ exports.noUsememoForPassByValue = void 0;
|
|
|
7
7
|
const utils_1 = require("@typescript-eslint/utils");
|
|
8
8
|
const typescript_1 = __importDefault(require("typescript"));
|
|
9
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const replacementSegments_1 = require("../utils/replacementSegments");
|
|
10
11
|
const DEFAULT_EXPENSIVE_PATTERNS = [
|
|
11
12
|
'compute',
|
|
12
13
|
'calculate',
|
|
@@ -280,37 +281,109 @@ function removeCompleteImport(importDeclaration, sourceCode, fixer) {
|
|
|
280
281
|
}
|
|
281
282
|
return fixer.removeRange([start, removalEnd]);
|
|
282
283
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Removing the last binding removes the whole declaration, but a comment
|
|
286
|
+
* written inside it (between the braces, around the source) must not go with
|
|
287
|
+
* it: the declaration's span is re-emitted as just those comments, each kept in
|
|
288
|
+
* order, with a line break appended when the last one would otherwise swallow
|
|
289
|
+
* whatever follows the declaration on its line.
|
|
290
|
+
*/
|
|
291
|
+
function removeCompleteImportPreservingComments(importDeclaration, sourceCode, fixer) {
|
|
292
|
+
const comments = sourceCode.getCommentsInside(importDeclaration);
|
|
293
|
+
if (comments.length === 0) {
|
|
294
|
+
return removeCompleteImport(importDeclaration, sourceCode, fixer);
|
|
295
|
+
}
|
|
296
|
+
const text = sourceCode.getText();
|
|
297
|
+
const carried = comments
|
|
298
|
+
.map((comment) => text.slice(comment.range[0], comment.range[1]))
|
|
299
|
+
.join('\n');
|
|
300
|
+
const suffix = (0, replacementSegments_1.requiresLineBreakAfter)(comments[comments.length - 1])
|
|
301
|
+
? '\n'
|
|
302
|
+
: '';
|
|
303
|
+
return fixer.replaceText(importDeclaration, `${carried}${suffix}`);
|
|
304
|
+
}
|
|
305
|
+
function isCommaToken(token) {
|
|
306
|
+
return (token !== null &&
|
|
307
|
+
token.type === utils_1.AST_TOKEN_TYPES.Punctuator &&
|
|
308
|
+
token.value === ',');
|
|
302
309
|
}
|
|
303
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Deletes the `useMemo` specifier and one adjacent comma while other NAMED
|
|
312
|
+
* specifiers remain. Only the tokens being retired are edited — never the
|
|
313
|
+
* declaration re-emitted from its parts — so every comment elsewhere in the
|
|
314
|
+
* declaration survives verbatim. When nothing but whitespace separates the
|
|
315
|
+
* retired tokens from their neighbour the removal range swallows that
|
|
316
|
+
* whitespace too; when a comment sits in between, the tokens are removed
|
|
317
|
+
* individually around it.
|
|
318
|
+
*/
|
|
319
|
+
function removeSpecifierFixes(specifier, sourceCode, fixer) {
|
|
320
|
+
const commaAfter = sourceCode.getTokenAfter(specifier);
|
|
321
|
+
if (isCommaToken(commaAfter)) {
|
|
322
|
+
const nextToken = sourceCode.getTokenAfter(commaAfter);
|
|
323
|
+
if (nextToken && !sourceCode.commentsExistBetween(specifier, nextToken)) {
|
|
324
|
+
return [fixer.removeRange([specifier.range[0], nextToken.range[0]])];
|
|
325
|
+
}
|
|
326
|
+
return [fixer.remove(specifier), fixer.removeRange(commaAfter.range)];
|
|
327
|
+
}
|
|
328
|
+
const commaBefore = sourceCode.getTokenBefore(specifier);
|
|
329
|
+
if (isCommaToken(commaBefore)) {
|
|
330
|
+
if (!sourceCode.commentsExistBetween(commaBefore, specifier)) {
|
|
331
|
+
return [fixer.removeRange([commaBefore.range[0], specifier.range[1]])];
|
|
332
|
+
}
|
|
333
|
+
return [fixer.removeRange(commaBefore.range), fixer.remove(specifier)];
|
|
334
|
+
}
|
|
335
|
+
return [fixer.remove(specifier)];
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Deletes the whole `{ useMemo }` group — braces, specifier and the comma
|
|
339
|
+
* joining it to the default specifier — when `useMemo` is the only named
|
|
340
|
+
* binding but a default import keeps the declaration alive. As above, comments
|
|
341
|
+
* inside the group survive by shrinking the edit to the individual tokens.
|
|
342
|
+
*/
|
|
343
|
+
function removeNamedGroupFixes(specifier, sourceCode, fixer) {
|
|
344
|
+
const openBrace = sourceCode.getTokenBefore(specifier, {
|
|
345
|
+
filter: (token) => token.value === '{',
|
|
346
|
+
});
|
|
347
|
+
const closeBrace = sourceCode.getTokenAfter(specifier, {
|
|
348
|
+
filter: (token) => token.value === '}',
|
|
349
|
+
});
|
|
350
|
+
if (!openBrace || !closeBrace) {
|
|
351
|
+
return [fixer.remove(specifier)];
|
|
352
|
+
}
|
|
353
|
+
const commaBefore = sourceCode.getTokenBefore(openBrace);
|
|
354
|
+
const start = isCommaToken(commaBefore) ? commaBefore : openBrace;
|
|
355
|
+
if (!sourceCode.commentsExistBetween(start, closeBrace)) {
|
|
356
|
+
return [fixer.removeRange([start.range[0], closeBrace.range[1]])];
|
|
357
|
+
}
|
|
358
|
+
const fixes = [];
|
|
359
|
+
if (start !== openBrace) {
|
|
360
|
+
fixes.push(fixer.removeRange(start.range));
|
|
361
|
+
}
|
|
362
|
+
fixes.push(fixer.removeRange(openBrace.range), fixer.remove(specifier));
|
|
363
|
+
const trailingComma = sourceCode.getTokenAfter(specifier);
|
|
364
|
+
if (isCommaToken(trailingComma)) {
|
|
365
|
+
fixes.push(fixer.removeRange(trailingComma.range));
|
|
366
|
+
}
|
|
367
|
+
fixes.push(fixer.removeRange(closeBrace.range));
|
|
368
|
+
return fixes;
|
|
369
|
+
}
|
|
370
|
+
function getImportRemovalFixes(specifier, sourceCode, fixer) {
|
|
304
371
|
const importDeclaration = specifier.parent;
|
|
305
372
|
if (!importDeclaration ||
|
|
306
373
|
importDeclaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
307
|
-
return
|
|
374
|
+
return [];
|
|
308
375
|
}
|
|
309
376
|
const remainingSpecifiers = importDeclaration.specifiers.filter((candidate) => candidate !== specifier);
|
|
310
377
|
if (remainingSpecifiers.length === 0) {
|
|
311
|
-
return
|
|
378
|
+
return [
|
|
379
|
+
removeCompleteImportPreservingComments(importDeclaration, sourceCode, fixer),
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
const hasOtherNamedSpecifiers = remainingSpecifiers.some((candidate) => candidate.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
383
|
+
if (!hasOtherNamedSpecifiers) {
|
|
384
|
+
return removeNamedGroupFixes(specifier, sourceCode, fixer);
|
|
312
385
|
}
|
|
313
|
-
return
|
|
386
|
+
return removeSpecifierFixes(specifier, sourceCode, fixer);
|
|
314
387
|
}
|
|
315
388
|
function getReplacementText(callback, sourceCode) {
|
|
316
389
|
const returnedExpression = getReturnedExpression(callback);
|
|
@@ -323,6 +396,23 @@ function getReplacementText(callback, sourceCode) {
|
|
|
323
396
|
}
|
|
324
397
|
return sourceCode.getText(returnedExpression);
|
|
325
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Comments inside the `useMemo(...)` span but outside the returned expression.
|
|
401
|
+
*
|
|
402
|
+
* The fix replaces the whole call with the text of the returned expression, so
|
|
403
|
+
* the callback wrapper, the `return` keyword, the dependency array and every
|
|
404
|
+
* piece of trivia between them disappear. A comment sitting in any of those
|
|
405
|
+
* places has no surviving anchor in the replacement, so the fixer carries it
|
|
406
|
+
* into the replacement instead (#1877). A comment inside the returned
|
|
407
|
+
* expression needs no carrying: its text is copied verbatim with the
|
|
408
|
+
* expression.
|
|
409
|
+
*/
|
|
410
|
+
function strandedCommentsOf(node, returnedExpression, sourceCode) {
|
|
411
|
+
return sourceCode
|
|
412
|
+
.getCommentsInside(node)
|
|
413
|
+
.filter((comment) => comment.range[0] < returnedExpression.range[0] ||
|
|
414
|
+
comment.range[1] > returnedExpression.range[1]);
|
|
415
|
+
}
|
|
326
416
|
function isSafeAtomicExpression(expression) {
|
|
327
417
|
switch (expression.type) {
|
|
328
418
|
case utils_1.AST_NODE_TYPES.Identifier:
|
|
@@ -555,16 +645,80 @@ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
|
|
|
555
645
|
return null;
|
|
556
646
|
}
|
|
557
647
|
const needsParentheses = shouldParenthesizeReplacement(node, returnedExpression, sourceCode);
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
648
|
+
const strandedComments = strandedCommentsOf(node, returnedExpression, sourceCode);
|
|
649
|
+
const fixes = [];
|
|
650
|
+
if (strandedComments.length === 0) {
|
|
651
|
+
fixes.push(fixer.replaceText(node, needsParentheses ? `(${replacementText})` : replacementText));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Carrying must not change the non-comment token stream: whether the
|
|
655
|
+
// replacement is parenthesized is decided by the expression's context
|
|
656
|
+
// exactly as in the comment-free fix, never by the comments themselves.
|
|
657
|
+
// The call can start mid-line, so the indentation of the line it opens
|
|
658
|
+
// on is the only anchor the carried comments have.
|
|
659
|
+
const startLine = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
660
|
+
const indent = /^[\t ]*/.exec(startLine)?.[0] ?? '';
|
|
661
|
+
const text = sourceCode.getText();
|
|
662
|
+
const toSegment = (comment) => ({
|
|
663
|
+
text: text.slice(comment.range[0], comment.range[1]),
|
|
664
|
+
breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
|
|
665
|
+
});
|
|
666
|
+
// A stranded comment lies wholly on one side of the expression, since
|
|
667
|
+
// a comment is a token and cannot straddle a node; keeping each on its
|
|
668
|
+
// own side preserves what it annotates.
|
|
669
|
+
const isBefore = (comment) => comment.range[0] < returnedExpression.range[0];
|
|
670
|
+
const leadingComments = strandedComments.filter(isBefore);
|
|
671
|
+
const trailingComments = strandedComments.filter((comment) => !isBefore(comment));
|
|
672
|
+
if (needsParentheses) {
|
|
673
|
+
// Inside parentheses a newline can never trigger ASI, so every
|
|
674
|
+
// comment — line comments and -next-line directives included — can
|
|
675
|
+
// ride within the replacement on a line of its own.
|
|
676
|
+
const segments = [
|
|
677
|
+
...leadingComments.map(toSegment),
|
|
678
|
+
{ text: replacementText, breakAfter: false },
|
|
679
|
+
...trailingComments.map(toSegment),
|
|
680
|
+
];
|
|
681
|
+
fixes.push(fixer.replaceText(node, (0, replacementSegments_1.joinSegments)(segments, indent)));
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
// Without parentheses a line break between a restricted keyword
|
|
685
|
+
// (`return`, `throw`, `yield`) and the expression would change the
|
|
686
|
+
// program through ASI, so a leading comment that demands its own
|
|
687
|
+
// line is hoisted onto a full line of its own ABOVE the line the
|
|
688
|
+
// call starts on. That insertion can never split a token pair, and
|
|
689
|
+
// it lands a `-next-line` directive exactly one line above the
|
|
690
|
+
// statement that now hosts its subject. Everything else stays
|
|
691
|
+
// inline: a block comment beside the expression, and a trailing
|
|
692
|
+
// line-bound comment followed by a line break, which is safe after
|
|
693
|
+
// the expression has begun.
|
|
694
|
+
const hoistedComments = leadingComments.filter(replacementSegments_1.requiresLineBreakAfter);
|
|
695
|
+
if (hoistedComments.length > 0) {
|
|
696
|
+
const lineStartIndex = sourceCode.getIndexFromLoc({
|
|
697
|
+
line: node.loc.start.line,
|
|
698
|
+
column: 0,
|
|
699
|
+
});
|
|
700
|
+
const hoisted = hoistedComments
|
|
701
|
+
.map((comment) => `${indent}${text.slice(comment.range[0], comment.range[1])}\n`)
|
|
702
|
+
.join('');
|
|
703
|
+
fixes.push(fixer.insertTextBeforeRange([lineStartIndex, lineStartIndex], hoisted));
|
|
704
|
+
}
|
|
705
|
+
const segments = [
|
|
706
|
+
...leadingComments
|
|
707
|
+
.filter((comment) => !(0, replacementSegments_1.requiresLineBreakAfter)(comment))
|
|
708
|
+
.map(toSegment),
|
|
709
|
+
{ text: replacementText, breakAfter: false },
|
|
710
|
+
...trailingComments.map(toSegment),
|
|
711
|
+
];
|
|
712
|
+
const body = (0, replacementSegments_1.joinSegmentBody)(segments, indent);
|
|
713
|
+
const trailing = segments[segments.length - 1].breakAfter
|
|
714
|
+
? `\n${indent}`
|
|
715
|
+
: '';
|
|
716
|
+
fixes.push(fixer.replaceText(node, `${body}${trailing}`));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
561
719
|
const specifier = getRemovableImportSpecifier(node, imports, sourceCode);
|
|
562
|
-
const fixes = [fixer.replaceText(node, replacement)];
|
|
563
720
|
if (specifier) {
|
|
564
|
-
|
|
565
|
-
if (removal) {
|
|
566
|
-
fixes.push(removal);
|
|
567
|
-
}
|
|
721
|
+
fixes.push(...getImportRemovalFixes(specifier, sourceCode, fixer));
|
|
568
722
|
}
|
|
569
723
|
return fixes;
|
|
570
724
|
}
|
|
@@ -4,6 +4,7 @@ exports.parallelizeAsyncOperations = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
const compilePatternOption_1 = require("../utils/compilePatternOption");
|
|
7
8
|
// Anchored at the end of the path so multi-part suffixes such as
|
|
8
9
|
// `EventRegistry.integration.test.ts` are recognized while production modules
|
|
9
10
|
// that merely contain the word (`testHelpers.ts`, `latest.ts`, `contest/Thing.ts`)
|
|
@@ -91,7 +92,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
91
92
|
return {};
|
|
92
93
|
}
|
|
93
94
|
const sourceCode = context.sourceCode;
|
|
94
|
-
|
|
95
|
+
// Compiling with a bare `new RegExp` throws while the rule is being built,
|
|
96
|
+
// which aborts the whole lint run — every file, every other rule — with a
|
|
97
|
+
// message naming neither this option nor the offending pattern. The schema
|
|
98
|
+
// admits any string, so `'**'` reaches here from a valid config.
|
|
99
|
+
const declaredPatterns = options?.sideEffectPatterns ?? [];
|
|
100
|
+
const sideEffectMatchers = [
|
|
101
|
+
...(0, compilePatternOption_1.compilePatternOption)('parallelize-async-operations', 'sideEffectPatterns', declaredPatterns.filter((pattern) => typeof pattern === 'string'), 'i'),
|
|
102
|
+
...declaredPatterns.filter((pattern) => typeof pattern !== 'string'),
|
|
103
|
+
];
|
|
95
104
|
const reportedRanges = new Set();
|
|
96
105
|
/**
|
|
97
106
|
* Checks if a node is an await expression
|
|
@@ -377,6 +377,26 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
377
377
|
return 'ok';
|
|
378
378
|
}
|
|
379
379
|
// ---- AST helpers --------------------------------------------------------
|
|
380
|
+
/**
|
|
381
|
+
* The expression an optional chain wraps: ESTree hangs a `ChainExpression`
|
|
382
|
+
* above the OUTERMOST link of `a?.b`, so a shape test written against
|
|
383
|
+
* `MemberExpression` sees a different node for a spelling that asks the same
|
|
384
|
+
* question here. `?.` changes nothing this rule decides on shape — the
|
|
385
|
+
* generated lookup copies the discriminant's source text verbatim
|
|
386
|
+
* (`RESULT_BY_KIND[a?.b]`), so the optional link survives the fix intact, and
|
|
387
|
+
* whether the chain can produce `undefined` is answered by the discriminant's
|
|
388
|
+
* TYPE (`hasNullish`), never by its shape. Reading the raw node instead made
|
|
389
|
+
* the narrowing carve-out unreachable and turned a `?.` on a discriminated
|
|
390
|
+
* union into a false positive whose remedy does not compile (#1867).
|
|
391
|
+
*
|
|
392
|
+
* Only one unwrap is needed: inner links of the same chain are plain
|
|
393
|
+
* `MemberExpression`s carrying `optional`, not nested `ChainExpression`s.
|
|
394
|
+
*/
|
|
395
|
+
function unwrapChain(node) {
|
|
396
|
+
return node.type === utils_1.AST_NODE_TYPES.ChainExpression
|
|
397
|
+
? node.expression
|
|
398
|
+
: node;
|
|
399
|
+
}
|
|
380
400
|
/** Whether a discriminant expression is an identifier or a call-free,
|
|
381
401
|
* non-optional member chain (safe to collapse repeated evaluations). */
|
|
382
402
|
function isValidDiscriminant(node) {
|
|
@@ -576,14 +596,15 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
576
596
|
.toUpperCase();
|
|
577
597
|
}
|
|
578
598
|
function deriveLookupName(discriminant) {
|
|
599
|
+
const target = unwrapChain(discriminant);
|
|
579
600
|
let key = null;
|
|
580
|
-
if (
|
|
581
|
-
key =
|
|
601
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
602
|
+
key = target.name;
|
|
582
603
|
}
|
|
583
|
-
else if (
|
|
584
|
-
!
|
|
585
|
-
|
|
586
|
-
key =
|
|
604
|
+
else if (target.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
605
|
+
!target.computed &&
|
|
606
|
+
target.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
607
|
+
key = target.property.name;
|
|
587
608
|
}
|
|
588
609
|
if (!key) {
|
|
589
610
|
return null;
|
|
@@ -894,12 +915,19 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
894
915
|
* would lose (hoisting `this.obj.data` out of the switch drops the narrowing
|
|
895
916
|
* and the emitted code no longer typechecks), so a `this`-rooted chain is
|
|
896
917
|
* matched against `this` reads in the kept branches.
|
|
918
|
+
*
|
|
919
|
+
* `obj?.tag` narrows identically too — TypeScript discriminates a union
|
|
920
|
+
* through an optional chain — so the discriminant is unwrapped before its
|
|
921
|
+
* shape is read. A nullable discriminant is exactly where `?.` gets written,
|
|
922
|
+
* and reading the `ChainExpression` as "not a member access" skipped this
|
|
923
|
+
* carve-out entirely (#1867).
|
|
897
924
|
*/
|
|
898
925
|
function isNarrowingExempt(discriminant, keptValues) {
|
|
899
|
-
|
|
926
|
+
const chain = unwrapChain(discriminant);
|
|
927
|
+
if (chain.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
900
928
|
return false;
|
|
901
929
|
}
|
|
902
|
-
const root = chainRootName(
|
|
930
|
+
const root = chainRootName(chain);
|
|
903
931
|
if (!root) {
|
|
904
932
|
return false;
|
|
905
933
|
}
|