@blumintinc/eslint-plugin-blumint 1.20.170 → 1.20.171
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/enforce-assert-safe-object-key.js +78 -3
- package/lib/rules/enforce-memoize-async.js +12 -0
- package/lib/rules/enforce-memoize-getters.js +20 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +296 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +30 -2
- package/lib/rules/no-explicit-return-type.js +35 -0
- package/lib/rules/no-useless-usememo-primitives.js +204 -20
- package/lib/rules/prefer-type-over-interface.d.ts +10 -2
- package/lib/rules/prefer-type-over-interface.js +238 -13
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +6 -1
- package/lib/rules/prefer-use-deep-compare-memo.js +184 -7
- package/lib/rules/use-latest-callback.js +11 -2
- package/lib/utils/arrowAnnotationGap.js +227 -10
- package/lib/utils/importRemoval.d.ts +33 -4
- package/lib/utils/importRemoval.js +141 -9
- package/lib/utils/inlineExpressionParens.d.ts +30 -0
- package/lib/utils/inlineExpressionParens.js +465 -0
- package/lib/utils/replacementSegments.d.ts +5 -0
- package/lib/utils/replacementSegments.js +2 -1
- package/lib/utils/resourceHandleType.d.ts +21 -0
- package/lib/utils/resourceHandleType.js +217 -0
- package/package.json +1 -1
- package/release-manifest.json +86 -0
package/lib/index.js
CHANGED
|
@@ -640,6 +640,47 @@ function foldKeyDomains(domains) {
|
|
|
640
640
|
}
|
|
641
641
|
return values;
|
|
642
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* The span of the access a written key belongs to: `obj[key]` including the
|
|
645
|
+
* object and both brackets, `[key]` for a computed property (whose value is an
|
|
646
|
+
* expression of its own), and the whole comparison for `key in obj`. Null where
|
|
647
|
+
* the key sits in none of those, which leaves the wrap to span the key alone.
|
|
648
|
+
*
|
|
649
|
+
* This is the unit a fix that rewrites a key has to claim. The key's own range
|
|
650
|
+
* stops short of the `]` that closes the access, and a fixer that reformats the
|
|
651
|
+
* access spreads its edits across the whole of it — so a span ending between
|
|
652
|
+
* the key and its bracket splits that set in half. Claiming the access instead
|
|
653
|
+
* leaves a competing rewrite of it either wholly discarded (and re-made against
|
|
654
|
+
* the fixed text on a later pass) or wholly applied, both of which parse.
|
|
655
|
+
*/
|
|
656
|
+
function accessSpan(sourceCode, written) {
|
|
657
|
+
const { parent } = written;
|
|
658
|
+
if (!parent) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
if (parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
662
|
+
parent.computed &&
|
|
663
|
+
parent.property === written) {
|
|
664
|
+
return [parent.range[0], parent.range[1]];
|
|
665
|
+
}
|
|
666
|
+
if (parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
667
|
+
parent.computed &&
|
|
668
|
+
parent.key === written) {
|
|
669
|
+
// A computed property's range runs past its key to the end of its value,
|
|
670
|
+
// which the wrap has no business claiming; the bracket that closes the key
|
|
671
|
+
// is where this access ends.
|
|
672
|
+
const closing = sourceCode.getTokenAfter(written);
|
|
673
|
+
return closing?.value === ']'
|
|
674
|
+
? [parent.range[0], closing.range[1]]
|
|
675
|
+
: [written.range[0], written.range[1]];
|
|
676
|
+
}
|
|
677
|
+
if (parent.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
678
|
+
parent.operator === 'in' &&
|
|
679
|
+
parent.left === written) {
|
|
680
|
+
return [parent.range[0], parent.range[1]];
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
643
684
|
/**
|
|
644
685
|
* An enum is a compiler-checked finite set. Members with literal initializers
|
|
645
686
|
* enumerate their runtime key strings (which is what lets the forbidden-name
|
|
@@ -836,17 +877,51 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
836
877
|
const importStatement = `import { assertSafe } from '${computeImportSpecifier()}';\n`;
|
|
837
878
|
return (0, importInsertion_1.insertAtImportAnchor)(context.sourceCode, fixer, (0, importInsertion_1.importInsertionAnchor)(context.sourceCode), importStatement);
|
|
838
879
|
};
|
|
880
|
+
/**
|
|
881
|
+
* The wrap of the key, emitted over the whole access the key belongs to
|
|
882
|
+
* rather than over the key alone.
|
|
883
|
+
*
|
|
884
|
+
* ESLint merges the fixes of one report into a single edit spanning
|
|
885
|
+
* [first start, last end], splicing the original text back in between. The
|
|
886
|
+
* import anchor sits at the top of the file, so bundling it with the wrap
|
|
887
|
+
* turns a three-character edit into one that claims everything from the
|
|
888
|
+
* file's first statement to the end of the key — which is a point in the
|
|
889
|
+
* middle of the access, before the `]` that closes it. That span sorts
|
|
890
|
+
* ahead of every competing fix and wins the race against all of them, but
|
|
891
|
+
* only up to its end. A fixer whose edits are coherent only as a set (a
|
|
892
|
+
* formatter re-wrapping one access across several lines is the common
|
|
893
|
+
* case) then has the edits inside the span discarded and the edits past
|
|
894
|
+
* its end applied, and the two halves do not fit together: the emitted
|
|
895
|
+
* file does not parse.
|
|
896
|
+
*
|
|
897
|
+
* Ending the span where the access ends puts every edit of such a
|
|
898
|
+
* competing rewrite on one side of the boundary — either wholly inside the
|
|
899
|
+
* span (discarded whole, and re-made against the fixed text on the next
|
|
900
|
+
* pass) or wholly outside it. Both parse. The re-emitted head and tail are
|
|
901
|
+
* copied verbatim from the source, so the text this fix produces is
|
|
902
|
+
* character-for-character what replacing the key alone produced.
|
|
903
|
+
*/
|
|
904
|
+
const wrapKey = (fixer, node, argText) => {
|
|
905
|
+
const replacement = `assertSafe(${argText})`;
|
|
906
|
+
const span = accessSpan(context.sourceCode, node);
|
|
907
|
+
if (!span) {
|
|
908
|
+
return fixer.replaceText(node, replacement);
|
|
909
|
+
}
|
|
910
|
+
const [start, end] = span;
|
|
911
|
+
const { text } = context.sourceCode;
|
|
912
|
+
return fixer.replaceTextRange([start, end], `${text.slice(start, node.range[0])}${replacement}${text.slice(node.range[1], end)}`);
|
|
913
|
+
};
|
|
839
914
|
/**
|
|
840
915
|
* Helper function to create fixes for a node
|
|
841
916
|
*/
|
|
842
917
|
const createFixes = (fixer, node, argText) => {
|
|
843
918
|
const fixes = [];
|
|
844
|
-
|
|
919
|
+
const carriesImport = !importClaimed && !importsAssertSafe(context.sourceCode.ast);
|
|
920
|
+
if (carriesImport) {
|
|
845
921
|
fixes.push(addAssertSafeImport(fixer));
|
|
846
922
|
importClaimed = true;
|
|
847
923
|
}
|
|
848
|
-
|
|
849
|
-
fixes.push(fixer.replaceText(node, `assertSafe(${argText})`));
|
|
924
|
+
fixes.push(wrapKey(fixer, node, argText));
|
|
850
925
|
return fixes;
|
|
851
926
|
};
|
|
852
927
|
// The report is emitted even when suppressed: ESLint discards it, and
|
|
@@ -6,6 +6,7 @@ const createRule_1 = require("../utils/createRule");
|
|
|
6
6
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
7
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
8
8
|
const importInsertion_1 = require("../utils/importInsertion");
|
|
9
|
+
const resourceHandleType_1 = require("../utils/resourceHandleType");
|
|
9
10
|
const MEMOIZE_MODULE = '@blumintinc/typescript-memoize';
|
|
10
11
|
const ALLOWED_MEMOIZE_MODULES = new Set([MEMOIZE_MODULE, 'typescript-memoize']);
|
|
11
12
|
const MEMOIZE_NAME = 'Memoize';
|
|
@@ -641,6 +642,17 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
641
642
|
if (declaresVoidResult(node.value.returnType)) {
|
|
642
643
|
return;
|
|
643
644
|
}
|
|
645
|
+
// A method that hands back a resource handle — an object carrying the
|
|
646
|
+
// closure that releases what the call allocated — must run per caller.
|
|
647
|
+
// Cached, N concurrent callers share one lease and one release closure:
|
|
648
|
+
// the first `release()` frees it while the remaining N-1 keep running
|
|
649
|
+
// against budget nobody accounts for, and a caller's `finally` block
|
|
650
|
+
// disposes another caller's resource. The failure is silent and
|
|
651
|
+
// load-dependent, so the fixer would apply it unattended under `--fix`
|
|
652
|
+
// and a passing concurrency suite would keep passing.
|
|
653
|
+
if ((0, resourceHandleType_1.declaresResourceHandleResult)(node.value.returnType)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
644
656
|
// A transaction handle is valid only for the attempt that created it,
|
|
645
657
|
// so a result derived from one must not outlive that attempt. Caching
|
|
646
658
|
// the body of a `runTransaction` callback — or the method that owns the
|
|
@@ -7,6 +7,7 @@ const createRule_1 = require("../utils/createRule");
|
|
|
7
7
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
8
8
|
const disableDirectives_1 = require("../utils/disableDirectives");
|
|
9
9
|
const importInsertion_1 = require("../utils/importInsertion");
|
|
10
|
+
const resourceHandleType_1 = require("../utils/resourceHandleType");
|
|
10
11
|
const MEMOIZE_PREFERRED_MODULE = '@blumintinc/typescript-memoize';
|
|
11
12
|
const MEMOIZE_MODULES = new Set([
|
|
12
13
|
MEMOIZE_PREFERRED_MODULE,
|
|
@@ -436,6 +437,25 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
|
|
|
436
437
|
// declaration despite having no name.
|
|
437
438
|
if (classBody?.parent?.type === utils_1.AST_NODE_TYPES.ClassExpression)
|
|
438
439
|
return;
|
|
440
|
+
// A getter that hands back a resource handle — an object carrying the
|
|
441
|
+
// closure that releases what the read allocated — allocates that handle
|
|
442
|
+
// for the reader taking it. Memoized, every later read receives the
|
|
443
|
+
// FIRST reader's live lease and the release closure bound to it: the
|
|
444
|
+
// first `release()` frees it while the remaining readers keep running
|
|
445
|
+
// against budget nobody accounts for, and one reader's `finally` block
|
|
446
|
+
// disposes another reader's resource. The failure is silent and
|
|
447
|
+
// load-dependent, so the fixer would apply it unattended under `--fix`
|
|
448
|
+
// past a green concurrency suite.
|
|
449
|
+
//
|
|
450
|
+
// The predicate is SHARED with `enforce-memoize-async` and with
|
|
451
|
+
// `no-explicit-return-type`, which preserves exactly this annotation
|
|
452
|
+
// rather than stripping it as a restatement of the result. Split
|
|
453
|
+
// predicates would not merely drift, they would cancel: a shape this
|
|
454
|
+
// rule exempts and the reader strips loses the carve-out inside a
|
|
455
|
+
// single unattended `eslint --fix` run.
|
|
456
|
+
if ((0, resourceHandleType_1.declaresResourceHandleResult)(node.value.returnType)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
439
459
|
// A getter whose value is a fresh read of live external state is not a
|
|
440
460
|
// lazy factory: memoizing it pins the first observation forever, so the
|
|
441
461
|
// report is dropped along with its unattended `--fix` edit.
|
|
@@ -171,6 +171,275 @@ function getIndentBeforeNode(sourceCode, node) {
|
|
|
171
171
|
const match = lineText.match(/^[ \t]*/);
|
|
172
172
|
return match ? match[0] : '';
|
|
173
173
|
}
|
|
174
|
+
const EXHAUSTIVE_DEPS_DISABLE = '// eslint-disable-next-line react-hooks/exhaustive-deps';
|
|
175
|
+
/**
|
|
176
|
+
* One level of indentation, spelled because the fixer prints an argument list
|
|
177
|
+
* rather than nudging the existing one. Two spaces is prettier's `tabWidth`
|
|
178
|
+
* default and this repo's and its consumers' setting; a tab-indented region is
|
|
179
|
+
* declined below instead of being indented with a mixture of the two.
|
|
180
|
+
*/
|
|
181
|
+
const INDENT_STEP = ' ';
|
|
182
|
+
/**
|
|
183
|
+
* Wrappers that sit between a call and its statement without changing where
|
|
184
|
+
* prettier indents the call's arguments.
|
|
185
|
+
*/
|
|
186
|
+
const TRANSPARENT_PARENTS = new Set([
|
|
187
|
+
utils_1.AST_NODE_TYPES.AwaitExpression,
|
|
188
|
+
utils_1.AST_NODE_TYPES.ChainExpression,
|
|
189
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
190
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
191
|
+
]);
|
|
192
|
+
/**
|
|
193
|
+
* Where an expanded argument list is measured from, or null where that cannot
|
|
194
|
+
* be resolved.
|
|
195
|
+
*
|
|
196
|
+
* Giving an argument a leading own-line comment forces prettier to print one
|
|
197
|
+
* argument per line — the decision is the comment's, not the line width's —
|
|
198
|
+
* indented one step past the enclosing statement, with the closing paren back
|
|
199
|
+
* at the statement's indentation. That holds while the call is the whole of a
|
|
200
|
+
* statement. A call nested inside another expression sits in a group prettier
|
|
201
|
+
* may break as well, and its arguments then indent against that break rather
|
|
202
|
+
* than against the line the call starts on, so those positions are declined
|
|
203
|
+
* rather than guessed at.
|
|
204
|
+
*
|
|
205
|
+
* A concise arrow body is the one nested position with a settled answer, and it
|
|
206
|
+
* has to be handled rather than declined because it is the same call spelled
|
|
207
|
+
* another way: `() => useCallback(…)` and `() => { return useCallback(…); }`
|
|
208
|
+
* are one function, and a fixer that remedies only one of them reports a
|
|
209
|
+
* violation it will not fix. Measured at the repo's prettier settings, the
|
|
210
|
+
* break lands after the final `=>` with the call one step past the statement,
|
|
211
|
+
* whatever the length of the arrow chain ahead of it.
|
|
212
|
+
*/
|
|
213
|
+
function expandedArgumentAnchor(node) {
|
|
214
|
+
const skipTransparent = (from) => {
|
|
215
|
+
let current = from;
|
|
216
|
+
while (current.parent && TRANSPARENT_PARENTS.has(current.parent.type)) {
|
|
217
|
+
current = current.parent;
|
|
218
|
+
}
|
|
219
|
+
return current;
|
|
220
|
+
};
|
|
221
|
+
let current = skipTransparent(node);
|
|
222
|
+
let followsArrow = false;
|
|
223
|
+
while (current.parent?.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
224
|
+
current.parent.body === current) {
|
|
225
|
+
followsArrow = true;
|
|
226
|
+
current = skipTransparent(current.parent);
|
|
227
|
+
}
|
|
228
|
+
const parent = current.parent;
|
|
229
|
+
if (!parent) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (parent.type === utils_1.AST_NODE_TYPES.ExpressionStatement ||
|
|
233
|
+
parent.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
234
|
+
return { statement: parent, followsArrow };
|
|
235
|
+
}
|
|
236
|
+
// A second declarator is a break of its own, which moves the indentation the
|
|
237
|
+
// argument list is measured from off the declaration's line.
|
|
238
|
+
if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
239
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
240
|
+
parent.parent.declarations.length === 1) {
|
|
241
|
+
return { statement: parent.parent, followsArrow };
|
|
242
|
+
}
|
|
243
|
+
if (parent.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
244
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
|
245
|
+
return { statement: parent.parent, followsArrow };
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Source lines whose leading whitespace belongs to a string's value rather than
|
|
251
|
+
* to the file's indentation. Re-indenting one of them would change what the
|
|
252
|
+
* program prints, and prettier leaves them alone for the same reason.
|
|
253
|
+
*/
|
|
254
|
+
function stringContinuationLines(sourceCode, node) {
|
|
255
|
+
const lines = new Set();
|
|
256
|
+
for (const token of sourceCode.getTokens(node)) {
|
|
257
|
+
if (token.type !== utils_1.AST_TOKEN_TYPES.String &&
|
|
258
|
+
token.type !== utils_1.AST_TOKEN_TYPES.Template) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (let line = token.loc.start.line + 1; line <= token.loc.end.line; line += 1) {
|
|
262
|
+
lines.add(line);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return lines;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Moves a span of source text by `shift` columns, leaving its first line alone
|
|
269
|
+
* because that line's indentation is written by the caller.
|
|
270
|
+
*
|
|
271
|
+
* Returns null when a line carries a tab, since a shift expressed in spaces
|
|
272
|
+
* cannot preserve a tab-indented line's column.
|
|
273
|
+
*/
|
|
274
|
+
function shiftIndentation(text, shift, firstLine, protectedLines) {
|
|
275
|
+
if (shift === 0) {
|
|
276
|
+
return text;
|
|
277
|
+
}
|
|
278
|
+
const shifted = [];
|
|
279
|
+
const lines = text.split('\n');
|
|
280
|
+
for (const [index, line] of lines.entries()) {
|
|
281
|
+
// A blank line gets no indentation: padding one is exactly the trailing
|
|
282
|
+
// whitespace this fixer exists to stop emitting.
|
|
283
|
+
if (index === 0 || protectedLines.has(firstLine + index) || !line.trim()) {
|
|
284
|
+
shifted.push(line);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (line.startsWith('\t')) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
shifted.push(shift > 0
|
|
291
|
+
? `${' '.repeat(shift)}${line}`
|
|
292
|
+
: line.slice(Math.min(-shift, line.length - line.trimStart().length)));
|
|
293
|
+
}
|
|
294
|
+
return shifted.join('\n');
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Whether an argument that spans several lines keeps the layout prettier gave
|
|
298
|
+
* it once the expansion moves it to `argumentIndent`.
|
|
299
|
+
*
|
|
300
|
+
* Prettier breaks an argument for one of two reasons: the construct forces it,
|
|
301
|
+
* or it did not fit the room it had. Only the first survives the move, because
|
|
302
|
+
* the expansion hands every argument a different amount of room — an argument
|
|
303
|
+
* broken purely for width may fit on one line where it lands, and prettier would
|
|
304
|
+
* join it back up over text this fixer had copied verbatim.
|
|
305
|
+
*
|
|
306
|
+
* Two cases qualify. An argument already sitting alone at the target indent has
|
|
307
|
+
* lost no room at all. And a function with a non-empty block body is the shape
|
|
308
|
+
* prettier never prints on one line, whatever room it is given, provided
|
|
309
|
+
* everything ahead of the body already fits on one line.
|
|
310
|
+
*/
|
|
311
|
+
function keepsItsLayoutWhenMoved(sourceCode, argument, argumentIndent, currentIndent) {
|
|
312
|
+
if (argument.loc.start.line === argument.loc.end.line) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (currentIndent === argumentIndent &&
|
|
316
|
+
argument.loc.start.column === currentIndent.length) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
if (argument.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
320
|
+
argument.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
const { body } = argument;
|
|
324
|
+
return (body.type === utils_1.AST_NODE_TYPES.BlockStatement &&
|
|
325
|
+
body.loc.start.line === argument.loc.start.line &&
|
|
326
|
+
(body.body.length > 0 || sourceCode.getCommentsInside(body).length > 0));
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The text of `node` with the rewrites that fall inside it already applied, so
|
|
330
|
+
* that a caller re-emitting the surrounding span carries them along instead of
|
|
331
|
+
* overlapping them with a second edit.
|
|
332
|
+
*/
|
|
333
|
+
function applyRewrites(sourceCode, node, rewrites) {
|
|
334
|
+
const inside = rewrites
|
|
335
|
+
.filter(({ range }) => range[0] >= node.range[0] && range[1] <= node.range[1])
|
|
336
|
+
.sort((left, right) => left.range[0] - right.range[0]);
|
|
337
|
+
let cursor = node.range[0];
|
|
338
|
+
let text = '';
|
|
339
|
+
for (const rewrite of inside) {
|
|
340
|
+
text += sourceCode.text.slice(cursor, rewrite.range[0]) + rewrite.text;
|
|
341
|
+
cursor = rewrite.range[1];
|
|
342
|
+
}
|
|
343
|
+
return text + sourceCode.text.slice(cursor, node.range[1]);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Re-emits a call's argument list in the one-argument-per-line shape prettier
|
|
347
|
+
* prints once an argument carries an own-line comment, with `comment` placed
|
|
348
|
+
* above `commentTarget` and `rewrites` folded into the arguments they fall in.
|
|
349
|
+
*
|
|
350
|
+
* Inserting the comment in place instead leaves two marks of the pre-image on
|
|
351
|
+
* the file: the separator whitespace the inserted line break strands at the end
|
|
352
|
+
* of the preceding line, and the arguments' pre-expansion indentation. Owning
|
|
353
|
+
* the whole span between the parentheses settles both, at the cost of having to
|
|
354
|
+
* reproduce every argument — so a span this cannot reproduce faithfully returns
|
|
355
|
+
* null and the caller declines the fix outright.
|
|
356
|
+
*/
|
|
357
|
+
function expandArgumentList(sourceCode, node, commentTarget, comment, rewrites) {
|
|
358
|
+
const anchor = expandedArgumentAnchor(node);
|
|
359
|
+
if (!anchor) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const { statement, followsArrow } = anchor;
|
|
363
|
+
const lineStart = sourceCode.getIndexFromLoc({
|
|
364
|
+
line: statement.loc.start.line,
|
|
365
|
+
column: 0,
|
|
366
|
+
});
|
|
367
|
+
const statementIndent = sourceCode.text.slice(lineStart, statement.range[0]);
|
|
368
|
+
if (!/^ *$/.test(statementIndent)) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const edits = [];
|
|
372
|
+
let callIndent = statementIndent;
|
|
373
|
+
if (followsArrow) {
|
|
374
|
+
// The arrow chain ahead of the body stays on the statement's line, so the
|
|
375
|
+
// break the expansion forces is the one after the final `=>` — and the
|
|
376
|
+
// whitespace across that break is the fixer's to write, for the same reason
|
|
377
|
+
// the separator between two arguments is.
|
|
378
|
+
const arrowToken = sourceCode.getTokenBefore(node);
|
|
379
|
+
if (!arrowToken ||
|
|
380
|
+
arrowToken.value !== '=>' ||
|
|
381
|
+
arrowToken.loc.end.line !== statement.loc.start.line ||
|
|
382
|
+
!/^\s*$/.test(sourceCode.text.slice(arrowToken.range[1], node.range[0]))) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
callIndent = `${statementIndent}${INDENT_STEP}`;
|
|
386
|
+
edits.push({
|
|
387
|
+
range: [arrowToken.range[1], node.range[0]],
|
|
388
|
+
text: `\n${callIndent}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else if (statement.loc.start.line !== node.loc.start.line) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const openParen = sourceCode.getTokenAfter(node.typeParameters ?? node.callee, {
|
|
395
|
+
filter: utils_1.ASTUtils.isOpeningParenToken,
|
|
396
|
+
});
|
|
397
|
+
const closeParen = sourceCode.getLastToken(node);
|
|
398
|
+
if (!openParen || !closeParen || !utils_1.ASTUtils.isClosingParenToken(closeParen)) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
// A comment written between the arguments belongs to no argument, so the
|
|
402
|
+
// re-emitted list has nowhere to carry it and would delete it. Declining is
|
|
403
|
+
// the deliberate choice here: a formatting correction does not justify
|
|
404
|
+
// dropping a comment (#1877).
|
|
405
|
+
const strandsAComment = sourceCode
|
|
406
|
+
.getCommentsInside(node)
|
|
407
|
+
.some((existing) => existing.range[0] >= openParen.range[1] &&
|
|
408
|
+
existing.range[1] <= closeParen.range[0] &&
|
|
409
|
+
!node.arguments.some((argument) => existing.range[0] >= argument.range[0] &&
|
|
410
|
+
existing.range[1] <= argument.range[1]));
|
|
411
|
+
if (strandsAComment) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const argumentIndent = `${callIndent}${INDENT_STEP}`;
|
|
415
|
+
const protectedLines = stringContinuationLines(sourceCode, node);
|
|
416
|
+
const parts = [];
|
|
417
|
+
for (const argument of node.arguments) {
|
|
418
|
+
const base = getIndentBeforeNode(sourceCode, argument);
|
|
419
|
+
if (!/^ *$/.test(base)) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
if (!keepsItsLayoutWhenMoved(sourceCode, argument, argumentIndent, base)) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
// Wrapping an element in a call adds no line terminator, so each line of
|
|
426
|
+
// the rewritten text still stands for the source line at the same offset
|
|
427
|
+
// and the protected-line numbering survives the rewrite.
|
|
428
|
+
const shifted = shiftIndentation(applyRewrites(sourceCode, argument, rewrites), argumentIndent.length - base.length, argument.loc.start.line, protectedLines);
|
|
429
|
+
if (shifted === null) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
if (argument === commentTarget) {
|
|
433
|
+
parts.push(`${argumentIndent}${comment}\n`);
|
|
434
|
+
}
|
|
435
|
+
parts.push(`${argumentIndent}${shifted},\n`);
|
|
436
|
+
}
|
|
437
|
+
edits.push({
|
|
438
|
+
range: [openParen.range[1], closeParen.range[0]],
|
|
439
|
+
text: `\n${parts.join('')}${callIndent}`,
|
|
440
|
+
});
|
|
441
|
+
return edits;
|
|
442
|
+
}
|
|
174
443
|
function hasExhaustiveDepsDisable(sourceCode, callNode, depsNode) {
|
|
175
444
|
const [start, end] = [callNode.range[0], depsNode.range[1]];
|
|
176
445
|
const callStartLine = callNode.loc.start.line;
|
|
@@ -355,15 +624,40 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
355
624
|
!bindsHashImport(existingBinding, hashImport)) {
|
|
356
625
|
return null;
|
|
357
626
|
}
|
|
358
|
-
const fixes = [];
|
|
359
627
|
const seen = new Set();
|
|
628
|
+
const rewrites = [];
|
|
360
629
|
for (const { node: targetNode } of offendingElements) {
|
|
361
630
|
if (seen.has(targetNode.range[0]))
|
|
362
631
|
continue;
|
|
363
632
|
seen.add(targetNode.range[0]);
|
|
364
633
|
const original = sourceCode.getText(targetNode);
|
|
365
|
-
|
|
634
|
+
rewrites.push({
|
|
635
|
+
range: targetNode.range,
|
|
636
|
+
text: `${hashIdentifier}(${original})`,
|
|
637
|
+
});
|
|
366
638
|
}
|
|
639
|
+
// The disable comment and the `stableHash(...)` wraps are one edit
|
|
640
|
+
// whenever the comment lands: an own-line comment forces prettier
|
|
641
|
+
// to expand the argument list, and the expansion re-emits the very
|
|
642
|
+
// span the wraps live in, which ESLint rejects as two overlapping
|
|
643
|
+
// fixes. Where the comment is already there, the wraps stand alone
|
|
644
|
+
// and the call's layout is left as the author wrote it.
|
|
645
|
+
const needsDisable = !hasExhaustiveDepsDisable(sourceCode, node, depsArg);
|
|
646
|
+
const expansion = needsDisable
|
|
647
|
+
? expandArgumentList(sourceCode, node, depsArg, EXHAUSTIVE_DEPS_DISABLE, rewrites)
|
|
648
|
+
: null;
|
|
649
|
+
// A call whose argument list cannot be reproduced still needs the
|
|
650
|
+
// disable, since the wrapped dependency is what makes
|
|
651
|
+
// `react-hooks/exhaustive-deps` fire. Emitting the wraps without it
|
|
652
|
+
// would trade this rule's report for that one, so the whole fix is
|
|
653
|
+
// declined and the report stands for the author. The decision is
|
|
654
|
+
// taken before any fix is scheduled: `importPlanned` claims the
|
|
655
|
+
// file's import for this violation, and a later `return null` would
|
|
656
|
+
// strand the surviving violations with no import at all.
|
|
657
|
+
if (needsDisable && !expansion) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
const fixes = (expansion ?? rewrites).map(({ range, text }) => fixer.replaceTextRange(range, text));
|
|
367
661
|
if (!isStableHashImported(sourceCode, hashImport) &&
|
|
368
662
|
!importPlanned) {
|
|
369
663
|
const importText = `import { ${hashImport.importName} } from '${hashImport.source}';\n`;
|
|
@@ -387,17 +681,6 @@ exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
|
387
681
|
}
|
|
388
682
|
importPlanned = true;
|
|
389
683
|
}
|
|
390
|
-
if (!hasExhaustiveDepsDisable(sourceCode, node, depsArg)) {
|
|
391
|
-
const indent = getIndentBeforeNode(sourceCode, depsArg);
|
|
392
|
-
const tokenBefore = sourceCode.getTokenBefore(depsArg, {
|
|
393
|
-
includeComments: true,
|
|
394
|
-
});
|
|
395
|
-
const needsLeadingNewline = tokenBefore?.loc.end.line === depsArg.loc.start.line;
|
|
396
|
-
const commentText = needsLeadingNewline
|
|
397
|
-
? `\n${indent}// eslint-disable-next-line react-hooks/exhaustive-deps\n${indent}`
|
|
398
|
-
: `// eslint-disable-next-line react-hooks/exhaustive-deps\n${indent}`;
|
|
399
|
-
fixes.push(fixer.insertTextBefore(depsArg, commentText));
|
|
400
|
-
}
|
|
401
684
|
return fixes;
|
|
402
685
|
},
|
|
403
686
|
});
|
|
@@ -532,12 +532,40 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
532
532
|
* declares, so any other name would be written as an import that resolves
|
|
533
533
|
* nowhere.
|
|
534
534
|
*/
|
|
535
|
+
function importDeclarationsOf() {
|
|
536
|
+
return sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Where {@link planFastDeepEqualImport} lands its statement, told to the
|
|
540
|
+
* removal planner so the blank line under a deleted microdiff import is
|
|
541
|
+
* judged against a file that opens on the replacement rather than on the
|
|
542
|
+
* module body (issue #2078).
|
|
543
|
+
*
|
|
544
|
+
* The anchor positions are a superset of the one the fixer picks, since which
|
|
545
|
+
* declarations the plan deletes is not known until it exists. Naming a
|
|
546
|
+
* position the fix does not use only leaves a blank line standing, which is
|
|
547
|
+
* the layout the file already had.
|
|
548
|
+
*/
|
|
549
|
+
function fastDeepEqualImportSites() {
|
|
550
|
+
if (hasFastDeepEqualImport)
|
|
551
|
+
return [];
|
|
552
|
+
const importDeclarations = importDeclarationsOf();
|
|
553
|
+
const microdiffImports = importDeclarations.filter((declaration) => microdiffModules_1.MICRODIFF_MODULES.has(String(declaration.source.value)));
|
|
554
|
+
if (microdiffImports.length > 0) {
|
|
555
|
+
return [
|
|
556
|
+
...microdiffImports.map((declaration) => declaration.range[0]),
|
|
557
|
+
microdiffImports[0].range[1],
|
|
558
|
+
];
|
|
559
|
+
}
|
|
560
|
+
const last = importDeclarations[importDeclarations.length - 1];
|
|
561
|
+
return [last ? last.range[1] : 0];
|
|
562
|
+
}
|
|
535
563
|
function planFastDeepEqualImport(fixer, removalRanges) {
|
|
536
564
|
if (hasFastDeepEqualImport) {
|
|
537
565
|
return [];
|
|
538
566
|
}
|
|
539
567
|
const importStatement = (0, fastDeepEqualModules_1.fastDeepEqualImport)(fastDeepEqualImportName);
|
|
540
|
-
const importDeclarations =
|
|
568
|
+
const importDeclarations = importDeclarationsOf();
|
|
541
569
|
const removalStartOf = (declaration) => removalRanges.find((range) => range[0] <= declaration.range[0] &&
|
|
542
570
|
range[1] >= declaration.range[1])?.[0];
|
|
543
571
|
const microdiffImports = importDeclarations.filter((declaration) => microdiffModules_1.MICRODIFF_MODULES.has(String(declaration.source.value)));
|
|
@@ -741,7 +769,7 @@ exports.fastDeepEqualOverMicrodiff = (0, createRule_1.createRule)({
|
|
|
741
769
|
// its last use, and the pass that applies them all resolves every
|
|
742
770
|
// report — so this is the only moment the stranded import is visible.
|
|
743
771
|
const importRemoval = planned.length > 0
|
|
744
|
-
? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, planned.flatMap((entry) => entry.removed))
|
|
772
|
+
? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, planned.flatMap((entry) => entry.removed), { insertions: fastDeepEqualImportSites() })
|
|
745
773
|
: null;
|
|
746
774
|
const removalRanges = importRemoval ?? [];
|
|
747
775
|
// The whole batch ships as one fix, so no rewrite can land without the
|
|
@@ -8,6 +8,7 @@ const importRemoval_1 = require("../utils/importRemoval");
|
|
|
8
8
|
const typeDeclarationRemoval_1 = require("../utils/typeDeclarationRemoval");
|
|
9
9
|
const replacementSegments_1 = require("../utils/replacementSegments");
|
|
10
10
|
const lexicalScope_1 = require("../utils/lexicalScope");
|
|
11
|
+
const resourceHandleType_1 = require("../utils/resourceHandleType");
|
|
11
12
|
const arrowAnnotationGap_1 = require("../utils/arrowAnnotationGap");
|
|
12
13
|
const defaultOptions = {
|
|
13
14
|
allowRecursiveFunctions: true,
|
|
@@ -845,6 +846,36 @@ function declaresVoidResult(returnType) {
|
|
|
845
846
|
return (typeArguments?.length === 1 &&
|
|
846
847
|
typeArguments[0].type === utils_1.AST_NODE_TYPES.TSVoidKeyword);
|
|
847
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Returns true when the annotation declares that the function hands back a
|
|
851
|
+
* RESOURCE HANDLE: an object result carrying a function-valued member, which is
|
|
852
|
+
* the closure that releases whatever the call allocated.
|
|
853
|
+
*
|
|
854
|
+
* `declaresVoidResult` above answers the same objection for the opposite shape,
|
|
855
|
+
* and the reasoning transfers verbatim. This annotation is not a restatement of
|
|
856
|
+
* a result either — it is the evidence a sibling rule's correctness decision
|
|
857
|
+
* rests on. `enforce-memoize-async` reads exactly this shape and declines to
|
|
858
|
+
* demand `@Memoize()` on the method, because a memoized handle factory serves
|
|
859
|
+
* every caller after the first the FIRST caller's live lease and the release
|
|
860
|
+
* closure bound to it: N concurrent callers hold one lease between them while
|
|
861
|
+
* the pool accounts for N, and whichever caller finishes first disposes a
|
|
862
|
+
* resource the others still believe they own.
|
|
863
|
+
*
|
|
864
|
+
* Stripping the annotation therefore destroys information rather than removing
|
|
865
|
+
* redundancy, and it destroys it unattended: `eslint --fix` re-lints until the
|
|
866
|
+
* output settles, so the strip and the memoization it re-arms land in the same
|
|
867
|
+
* run. Nothing downstream catches what that re-arms, because the failure is
|
|
868
|
+
* silent and load-dependent — it needs concurrent callers to show itself, so a
|
|
869
|
+
* green suite stays green and the corruption surfaces under load.
|
|
870
|
+
*
|
|
871
|
+
* The predicate is SHARED with the owner (`../utils/resourceHandleType`) rather
|
|
872
|
+
* than restated here. A shape one rule exempts while the other strips is the
|
|
873
|
+
* whole defect; a shape one rule strips while the other exempts is the same
|
|
874
|
+
* defect pointing the other way, so the two cannot be allowed to drift apart.
|
|
875
|
+
*/
|
|
876
|
+
function isResourceHandleReturnType(returnType) {
|
|
877
|
+
return (0, resourceHandleType_1.declaresResourceHandleResult)(returnType);
|
|
878
|
+
}
|
|
848
879
|
// TypeScript's built-in decorator signatures. A factory annotated with one of
|
|
849
880
|
// these is the one shape where the annotation is WIDER than what inference
|
|
850
881
|
// produces rather than a restatement of it: `MethodDecorator` accepts three
|
|
@@ -1364,6 +1395,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1364
1395
|
if (isTypeGuardFunction(node) ||
|
|
1365
1396
|
isReadonlyWideningReturnType(returnType) ||
|
|
1366
1397
|
isAllowedVoidReturnType(returnType) ||
|
|
1398
|
+
isResourceHandleReturnType(returnType) ||
|
|
1367
1399
|
isDecoratorFactory(node, returnType) ||
|
|
1368
1400
|
isOverloadImplementation(node) ||
|
|
1369
1401
|
(mergedOptions.allowRecursiveFunctions &&
|
|
@@ -1383,6 +1415,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1383
1415
|
if (isTypeGuardFunction(node) ||
|
|
1384
1416
|
isReadonlyWideningReturnType(returnType) ||
|
|
1385
1417
|
isAllowedVoidReturnType(returnType) ||
|
|
1418
|
+
isResourceHandleReturnType(returnType) ||
|
|
1386
1419
|
isDecoratorFactory(node, returnType) ||
|
|
1387
1420
|
(mergedOptions.allowRecursiveFunctions &&
|
|
1388
1421
|
isRecursiveFunction(node)) ||
|
|
@@ -1398,6 +1431,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1398
1431
|
if (isTypeGuardFunction(node) ||
|
|
1399
1432
|
isReadonlyWideningReturnType(returnType) ||
|
|
1400
1433
|
isAllowedVoidReturnType(returnType) ||
|
|
1434
|
+
isResourceHandleReturnType(returnType) ||
|
|
1401
1435
|
isDecoratorFactory(node, returnType) ||
|
|
1402
1436
|
isReturnTypeRequiredByRecursion(node)) {
|
|
1403
1437
|
return;
|
|
@@ -1424,6 +1458,7 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
|
|
|
1424
1458
|
if (isTypeGuardFunction(node.value) ||
|
|
1425
1459
|
isReadonlyWideningReturnType(returnType) ||
|
|
1426
1460
|
isAllowedVoidReturnType(returnType) ||
|
|
1461
|
+
isResourceHandleReturnType(returnType) ||
|
|
1427
1462
|
isDecoratorFactory(node, returnType) ||
|
|
1428
1463
|
isOverloadImplementationMethod(node) ||
|
|
1429
1464
|
(mergedOptions.allowOverloadedFunctions &&
|