eslint-plugin-flawless 0.1.12 → 1.1.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 +18 -15
- package/dist/index.d.mts +74 -10
- package/dist/index.mjs +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/{plugin-DYVtpuio.mjs → plugin-B-YWsemf.mjs} +1304 -169
- package/package.json +2 -2
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { createSyncFn } from "synckit";
|
|
6
7
|
import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { findVariable, isArrowToken, isOpeningParenToken } from "@typescript-eslint/utils/ast-utils";
|
|
9
|
+
import ts9, { ScriptTarget, TypeFlags, isIdentifierPart, isIdentifierStart } from "typescript";
|
|
10
|
+
import { DefinitionType, ImplicitLibVariable, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
|
|
9
11
|
import assert from "node:assert";
|
|
12
|
+
import { getStaticJSONValue, parseForESLint } from "jsonc-eslint-parser";
|
|
10
13
|
import * as core from "@eslint-react/core";
|
|
11
|
-
import { findVariable } from "@typescript-eslint/utils/ast-utils";
|
|
12
|
-
import ts9 from "typescript";
|
|
13
14
|
import { getStaticTOMLValue } from "toml-eslint-parser";
|
|
14
15
|
//#region package.json
|
|
15
16
|
var name = "eslint-plugin-flawless";
|
|
16
|
-
var version = "
|
|
17
|
+
var version = "1.1.0";
|
|
17
18
|
var repository = {
|
|
18
19
|
"url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
|
|
19
20
|
"type": "git"
|
|
@@ -83,11 +84,11 @@ function createFlawlessRule({ createOnce, ...meta }) {
|
|
|
83
84
|
}
|
|
84
85
|
//#endregion
|
|
85
86
|
//#region src/rules/arrow-return-style/rule.ts
|
|
86
|
-
const RULE_NAME$
|
|
87
|
+
const RULE_NAME$15 = "arrow-return-style";
|
|
87
88
|
const IMPLICIT = "useImplicitReturn";
|
|
88
89
|
const EXPLICIT = "useExplicitReturn";
|
|
89
90
|
const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
|
|
90
|
-
const DEFAULTS = {
|
|
91
|
+
const DEFAULTS$1 = {
|
|
91
92
|
jsxAlwaysUseExplicitReturn: false,
|
|
92
93
|
maxLen: 80,
|
|
93
94
|
maxObjectProperties: 4,
|
|
@@ -231,9 +232,9 @@ function collectArrows(root) {
|
|
|
231
232
|
return arrows.sort((a, b) => a.range[0] - b.range[0]);
|
|
232
233
|
}
|
|
233
234
|
const arrowReturnStyle = createFlawlessRule({
|
|
234
|
-
name: RULE_NAME$
|
|
235
|
+
name: RULE_NAME$15,
|
|
235
236
|
createOnce(context) {
|
|
236
|
-
let config = DEFAULTS;
|
|
237
|
+
let config = DEFAULTS$1;
|
|
237
238
|
let sourceCode;
|
|
238
239
|
let lines;
|
|
239
240
|
let pendingConsults;
|
|
@@ -288,38 +289,50 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
288
289
|
};
|
|
289
290
|
}
|
|
290
291
|
/**
|
|
292
|
+
* Prepares the oxfmt question for `node`: how would the formatter render
|
|
293
|
+
* the enclosing statement once the candidate fix is applied? `collapse`
|
|
294
|
+
* supplies the implicit-direction candidate (the block body textually
|
|
295
|
+
* replaced by its collapsed expression); omitting it asks about the
|
|
296
|
+
* statement as written.
|
|
297
|
+
*
|
|
298
|
+
* @param node - The arrow function in question.
|
|
299
|
+
* @param collapse - The implicit-direction candidate, if any.
|
|
300
|
+
* @returns The consult, or `null` when the arrow cannot be located.
|
|
301
|
+
*/
|
|
302
|
+
function buildConsult(node, collapse) {
|
|
303
|
+
const statement = statementOf(node);
|
|
304
|
+
const arrowIndex = collectArrows(statement).findIndex((arrow) => arrow.range[0] === node.range[0]);
|
|
305
|
+
if (arrowIndex === -1) return null;
|
|
306
|
+
const offset = statement.range[0];
|
|
307
|
+
const source = sourceCode.getText(statement);
|
|
308
|
+
const snippet = collapse === void 0 ? source : source.slice(0, collapse.block.range[0] - offset) + collapse.replacement + source.slice(collapse.block.range[1] - offset);
|
|
309
|
+
const request = {
|
|
310
|
+
arrowIndex,
|
|
311
|
+
code: `${snippet}\n`,
|
|
312
|
+
printWidth: printWidth(),
|
|
313
|
+
tabWidth: config.tabWidth
|
|
314
|
+
};
|
|
315
|
+
return {
|
|
316
|
+
baseIndent: leadingWhitespace(lineOf(statement.loc.start.line)),
|
|
317
|
+
cacheKey: `${arrowIndex}${request.printWidth}${request.tabWidth}${snippet}`,
|
|
318
|
+
node,
|
|
319
|
+
request
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
291
323
|
* Resolves every deferred oxfmt consult with a single worker round-trip:
|
|
292
|
-
* would the formatter render each arrow (params through body) on
|
|
293
|
-
* line that fits `maxLen`?
|
|
294
|
-
*
|
|
295
|
-
* unavailable so that an unavailable formatter
|
|
296
|
-
* it would have to fight over.
|
|
324
|
+
* would the formatter render each candidate arrow (params through body) on
|
|
325
|
+
* a single line that fits `maxLen`? Explicit consults report when it
|
|
326
|
+
* cannot, implicit consults report when it can. Fails open (no report)
|
|
327
|
+
* when the worker or oxfmt is unavailable so that an unavailable formatter
|
|
328
|
+
* can never introduce reports it would have to fight over.
|
|
297
329
|
*/
|
|
298
330
|
function resolvePendingConsults() {
|
|
299
331
|
if (pendingConsults.length === 0) return;
|
|
300
|
-
const
|
|
332
|
+
const consults = pendingConsults;
|
|
301
333
|
pendingConsults = [];
|
|
302
334
|
const worker = getFormatSync();
|
|
303
335
|
if (worker === null) return;
|
|
304
|
-
const consults = nodes.flatMap((node) => {
|
|
305
|
-
const statement = statementOf(node);
|
|
306
|
-
const snippet = sourceCode.getText(statement);
|
|
307
|
-
const baseIndent = leadingWhitespace(lineOf(statement.loc.start.line));
|
|
308
|
-
const arrowIndex = collectArrows(statement).findIndex((arrow) => arrow.range[0] === node.range[0]);
|
|
309
|
-
if (arrowIndex === -1) return [];
|
|
310
|
-
const request = {
|
|
311
|
-
arrowIndex,
|
|
312
|
-
code: `${snippet}\n`,
|
|
313
|
-
printWidth: printWidth(),
|
|
314
|
-
tabWidth: config.tabWidth
|
|
315
|
-
};
|
|
316
|
-
return [{
|
|
317
|
-
baseIndent,
|
|
318
|
-
cacheKey: `${arrowIndex}${request.printWidth}${request.tabWidth}${snippet}`,
|
|
319
|
-
node,
|
|
320
|
-
request
|
|
321
|
-
}];
|
|
322
|
-
});
|
|
323
336
|
const misses = /* @__PURE__ */ new Map();
|
|
324
337
|
for (const consult of consults) if (formatCacheGet(consult.cacheKey) === void 0) misses.set(consult.cacheKey, consult.request);
|
|
325
338
|
if (misses.size > 0) {
|
|
@@ -338,7 +351,10 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
338
351
|
const response = formatCacheGet(consult.cacheKey);
|
|
339
352
|
if (response === void 0) continue;
|
|
340
353
|
if (response.lineText === null) continue;
|
|
341
|
-
|
|
354
|
+
const fits = response.singleLine && width(consult.baseIndent) + width(response.lineText) <= config.maxLen;
|
|
355
|
+
if (consult.kind === "implicit") {
|
|
356
|
+
if (fits) reportImplicit(consult.node, consult.block, consult.replacement);
|
|
357
|
+
} else if (!fits) reportExplicit(consult.node, EXPLICIT);
|
|
342
358
|
}
|
|
343
359
|
}
|
|
344
360
|
/**
|
|
@@ -365,6 +381,28 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
365
381
|
return arrowIndent.length > 0 ? " " : " ";
|
|
366
382
|
}
|
|
367
383
|
/**
|
|
384
|
+
* The `)` of a wrapped sole-argument call whose trailing comma the fix
|
|
385
|
+
* should absorb. `foo(() =>\n\tbody,\n)` only carries that comma because
|
|
386
|
+
* the argument was wrapped; once the block body hugs the call again the
|
|
387
|
+
* comma is debris (`},\n)` where the formatter writes `})`).
|
|
388
|
+
*
|
|
389
|
+
* @param node - The arrow function being converted.
|
|
390
|
+
* @param bodyEnd - The body's last token or node (including parens).
|
|
391
|
+
* @returns The closing paren to absorb up to, or `null`.
|
|
392
|
+
*/
|
|
393
|
+
function danglingCloser(node, bodyEnd) {
|
|
394
|
+
const { loc, parent } = node;
|
|
395
|
+
if (!(parent.type === AST_NODE_TYPES.CallExpression || parent.type === AST_NODE_TYPES.NewExpression) || parent.arguments.length !== 1 || parent.arguments[0] !== node) return null;
|
|
396
|
+
const opener = sourceCode.getTokenBefore(node);
|
|
397
|
+
if (opener?.value !== "(" || opener.loc.end.line !== loc.start.line) return null;
|
|
398
|
+
const comma = sourceCode.getTokenAfter(bodyEnd);
|
|
399
|
+
if (comma?.value !== ",") return null;
|
|
400
|
+
const closer = sourceCode.getTokenAfter(comma);
|
|
401
|
+
if (closer?.value !== ")" || closer.range[1] !== parent.range[1]) return null;
|
|
402
|
+
if (sourceCode.getCommentsBefore(closer).length > 0) return null;
|
|
403
|
+
return closer;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
368
406
|
* Reports and fixes an implicit arrow into an explicit block body.
|
|
369
407
|
*
|
|
370
408
|
* @param node - The arrow function to convert.
|
|
@@ -396,8 +434,9 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
396
434
|
const commentLines = comments.map((comment) => targetIndent + sourceCode.getText(comment));
|
|
397
435
|
const returnLine = `${targetIndent}return ${[firstLine, ...shifted].join("\n")};`;
|
|
398
436
|
const replacement = ` {\n${[...commentLines, returnLine].join("\n")}\n${arrowIndent}}`;
|
|
437
|
+
const end = danglingCloser(node, bodyEnd)?.range[0] ?? bodyEnd.range[1];
|
|
399
438
|
context.report({
|
|
400
|
-
fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1],
|
|
439
|
+
fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], end], replacement),
|
|
401
440
|
messageId,
|
|
402
441
|
node
|
|
403
442
|
});
|
|
@@ -432,7 +471,20 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
432
471
|
const prefix = lineOf(block.loc.start.line).slice(0, block.loc.start.column);
|
|
433
472
|
const suffix = lineOf(block.loc.end.line).slice(block.loc.end.column);
|
|
434
473
|
if (width(prefix + collapsed + suffix) > limit()) return;
|
|
435
|
-
|
|
474
|
+
if (statementOf(node).loc.end.line === block.loc.end.line || config.useOxfmt === false) {
|
|
475
|
+
reportImplicit(node, block, collapsed);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const consult = buildConsult(node, {
|
|
479
|
+
block,
|
|
480
|
+
replacement: collapsed
|
|
481
|
+
});
|
|
482
|
+
if (consult !== null) pendingConsults.push({
|
|
483
|
+
...consult,
|
|
484
|
+
block,
|
|
485
|
+
kind: "implicit",
|
|
486
|
+
replacement: collapsed
|
|
487
|
+
});
|
|
436
488
|
}
|
|
437
489
|
function checkExpressionBody(node) {
|
|
438
490
|
const body = node.body;
|
|
@@ -461,8 +513,15 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
461
513
|
return;
|
|
462
514
|
}
|
|
463
515
|
if (width(lineOf(bodyStart.loc.start.line)) > limit()) {
|
|
464
|
-
if (config.useOxfmt
|
|
465
|
-
|
|
516
|
+
if (config.useOxfmt === false) {
|
|
517
|
+
reportExplicit(node, EXPLICIT);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const consult = buildConsult(node);
|
|
521
|
+
if (consult !== null) pendingConsults.push({
|
|
522
|
+
...consult,
|
|
523
|
+
kind: "explicit"
|
|
524
|
+
});
|
|
466
525
|
return;
|
|
467
526
|
}
|
|
468
527
|
if (objectStyleWantsExplicit(body)) reportExplicit(node, COMPLEX_EXPLICIT);
|
|
@@ -474,7 +533,7 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
474
533
|
},
|
|
475
534
|
"before": function() {
|
|
476
535
|
config = {
|
|
477
|
-
...DEFAULTS,
|
|
536
|
+
...DEFAULTS$1,
|
|
478
537
|
...context.options[0]
|
|
479
538
|
};
|
|
480
539
|
({sourceCode} = context);
|
|
@@ -486,7 +545,7 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
486
545
|
}
|
|
487
546
|
};
|
|
488
547
|
},
|
|
489
|
-
defaultOptions: [DEFAULTS],
|
|
548
|
+
defaultOptions: [DEFAULTS$1],
|
|
490
549
|
meta: {
|
|
491
550
|
docs: {
|
|
492
551
|
description: "Enforce arrow function return style based on line length",
|
|
@@ -539,23 +598,23 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
539
598
|
});
|
|
540
599
|
//#endregion
|
|
541
600
|
//#region src/rules/jsx-shorthand-boolean/rule.ts
|
|
542
|
-
const RULE_NAME$
|
|
543
|
-
const MESSAGE_ID$
|
|
544
|
-
const messages$
|
|
545
|
-
function createOnce$
|
|
601
|
+
const RULE_NAME$14 = "jsx-shorthand-boolean";
|
|
602
|
+
const MESSAGE_ID$7 = "setAttributeValue";
|
|
603
|
+
const messages$14 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
|
|
604
|
+
function createOnce$9(context) {
|
|
546
605
|
return { JSXAttribute(node) {
|
|
547
606
|
if (node.value !== null) return;
|
|
548
607
|
context.report({
|
|
549
608
|
data: { name: context.sourceCode.getText(node.name) },
|
|
550
609
|
fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
|
|
551
|
-
messageId: MESSAGE_ID$
|
|
610
|
+
messageId: MESSAGE_ID$7,
|
|
552
611
|
node
|
|
553
612
|
});
|
|
554
613
|
} };
|
|
555
614
|
}
|
|
556
615
|
const jsxShorthandBoolean = createFlawlessRule({
|
|
557
|
-
name: RULE_NAME$
|
|
558
|
-
createOnce: createOnce$
|
|
616
|
+
name: RULE_NAME$14,
|
|
617
|
+
createOnce: createOnce$9,
|
|
559
618
|
defaultOptions: [],
|
|
560
619
|
meta: {
|
|
561
620
|
docs: {
|
|
@@ -565,23 +624,23 @@ const jsxShorthandBoolean = createFlawlessRule({
|
|
|
565
624
|
},
|
|
566
625
|
fixable: "code",
|
|
567
626
|
hasSuggestions: false,
|
|
568
|
-
messages: messages$
|
|
627
|
+
messages: messages$14,
|
|
569
628
|
schema: [],
|
|
570
629
|
type: "suggestion"
|
|
571
630
|
}
|
|
572
631
|
});
|
|
573
632
|
//#endregion
|
|
574
633
|
//#region src/rules/jsx-shorthand-fragment/rule.ts
|
|
575
|
-
const RULE_NAME$
|
|
634
|
+
const RULE_NAME$13 = "jsx-shorthand-fragment";
|
|
576
635
|
const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
577
636
|
const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
578
637
|
const DEFAULT_MODE = "syntax";
|
|
579
638
|
const DEFAULT_FRAGMENT_NAME = "Fragment";
|
|
580
|
-
const messages$
|
|
639
|
+
const messages$13 = {
|
|
581
640
|
[MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
|
|
582
641
|
[MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
|
|
583
642
|
};
|
|
584
|
-
const schema$
|
|
643
|
+
const schema$3 = [{
|
|
585
644
|
additionalProperties: false,
|
|
586
645
|
properties: {
|
|
587
646
|
fragmentName: {
|
|
@@ -613,7 +672,7 @@ function jsxNameToString(node) {
|
|
|
613
672
|
}
|
|
614
673
|
return null;
|
|
615
674
|
}
|
|
616
|
-
function createOnce$
|
|
675
|
+
function createOnce$8(context) {
|
|
617
676
|
let mode;
|
|
618
677
|
let fragmentName;
|
|
619
678
|
let namedFragments;
|
|
@@ -660,8 +719,8 @@ function createOnce$6(context) {
|
|
|
660
719
|
};
|
|
661
720
|
}
|
|
662
721
|
const jsxShorthandFragment = createFlawlessRule({
|
|
663
|
-
name: RULE_NAME$
|
|
664
|
-
createOnce: createOnce$
|
|
722
|
+
name: RULE_NAME$13,
|
|
723
|
+
createOnce: createOnce$8,
|
|
665
724
|
defaultOptions: [{
|
|
666
725
|
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
667
726
|
mode: DEFAULT_MODE
|
|
@@ -678,7 +737,328 @@ const jsxShorthandFragment = createFlawlessRule({
|
|
|
678
737
|
},
|
|
679
738
|
fixable: "code",
|
|
680
739
|
hasSuggestions: false,
|
|
681
|
-
messages: messages$
|
|
740
|
+
messages: messages$13,
|
|
741
|
+
schema: schema$3,
|
|
742
|
+
type: "suggestion"
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/rules/max-lines-per-function/core-ast-utils.ts
|
|
747
|
+
/**
|
|
748
|
+
* @file Helpers ported from ESLint core's `lib/rules/utils/ast-utils.js` and
|
|
749
|
+
* `lib/shared/string-utils.js`, so this rule's diagnostics read and point
|
|
750
|
+
* identically to core's `max-lines-per-function`. `@typescript-eslint/utils`
|
|
751
|
+
* re-exports `@eslint-community/eslint-utils` equivalents
|
|
752
|
+
* (`getFunctionNameWithKind`, `getFunctionHeadLocation`), but those diverge from
|
|
753
|
+
* core — they name variable-bound functions (`const f = () => {}` →
|
|
754
|
+
* `arrow function 'f'`) and bracket computed keys — so the port is what keeps
|
|
755
|
+
* parity with core, not availability. ESLint is MIT licensed — Copyright OpenJS
|
|
756
|
+
* Foundation and other contributors.
|
|
757
|
+
*/
|
|
758
|
+
/**
|
|
759
|
+
* Describes a function by name and kind, as core does, so diagnostics read the
|
|
760
|
+
* same. Examples: `function 'foo'`, `arrow function`, `constructor`,
|
|
761
|
+
* `static async generator method 'foo'`, `private method #foo`.
|
|
762
|
+
*
|
|
763
|
+
* Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted: this
|
|
764
|
+
* rule only visits the three function node types, so a signature node can never
|
|
765
|
+
* reach here.
|
|
766
|
+
*
|
|
767
|
+
* @param node - The function to describe.
|
|
768
|
+
* @returns The space-joined description.
|
|
769
|
+
*/
|
|
770
|
+
function getFunctionNameWithKind({ id, async, generator, parent, type }) {
|
|
771
|
+
const tokens = [];
|
|
772
|
+
if (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) {
|
|
773
|
+
if (parent.static) tokens.push("static");
|
|
774
|
+
if (!parent.computed && parent.key.type === AST_NODE_TYPES.PrivateIdentifier) tokens.push("private");
|
|
775
|
+
}
|
|
776
|
+
if (async) tokens.push("async");
|
|
777
|
+
if (generator) tokens.push("generator");
|
|
778
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition) {
|
|
779
|
+
if (parent.kind === "constructor") return "constructor";
|
|
780
|
+
if (parent.kind === "get") tokens.push("getter");
|
|
781
|
+
else if (parent.kind === "set") tokens.push("setter");
|
|
782
|
+
else tokens.push("method");
|
|
783
|
+
} else if (parent.type === AST_NODE_TYPES.PropertyDefinition) tokens.push("method");
|
|
784
|
+
else {
|
|
785
|
+
if (type === AST_NODE_TYPES.ArrowFunctionExpression) tokens.push("arrow");
|
|
786
|
+
tokens.push("function");
|
|
787
|
+
}
|
|
788
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) if (!parent.computed && parent.key.type === AST_NODE_TYPES.PrivateIdentifier) tokens.push(`#${parent.key.name}`);
|
|
789
|
+
else {
|
|
790
|
+
const name = getStaticPropertyName(parent);
|
|
791
|
+
if (name !== null) tokens.push(`'${name}'`);
|
|
792
|
+
else if (id !== null) tokens.push(`'${id.name}'`);
|
|
793
|
+
}
|
|
794
|
+
else if (id !== null) tokens.push(`'${id.name}'`);
|
|
795
|
+
return tokens.join(" ");
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Locates the head of a function — the part worth underlining in a diagnostic,
|
|
799
|
+
* rather than the function's whole multi-line body. For example the
|
|
800
|
+
* `function foo` of a declaration, or the `=>` of an arrow function.
|
|
801
|
+
*
|
|
802
|
+
* Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted for
|
|
803
|
+
* the same reason as in {@link getFunctionNameWithKind}: those nodes never
|
|
804
|
+
* reach here.
|
|
805
|
+
*
|
|
806
|
+
* @param node - The function to locate.
|
|
807
|
+
* @param sourceCode - The source code being linted.
|
|
808
|
+
* @returns The location of the function's head.
|
|
809
|
+
*/
|
|
810
|
+
function getFunctionHeadLoc(node, sourceCode) {
|
|
811
|
+
const { body, loc, parent, type } = node;
|
|
812
|
+
if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) return {
|
|
813
|
+
end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
|
|
814
|
+
start: parent.loc.start
|
|
815
|
+
};
|
|
816
|
+
if (type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
817
|
+
const arrowToken = sourceCode.getTokenBefore(body, isArrowToken);
|
|
818
|
+
if (arrowToken !== null) return {
|
|
819
|
+
end: arrowToken.loc.end,
|
|
820
|
+
start: arrowToken.loc.start
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
|
|
825
|
+
start: loc.start
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Upper-cases the first character of a string.
|
|
830
|
+
*
|
|
831
|
+
* @param value - The string to convert.
|
|
832
|
+
* @returns The converted string.
|
|
833
|
+
*/
|
|
834
|
+
function upperCaseFirst(value) {
|
|
835
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Reads the statically known string value of a property key.
|
|
839
|
+
*
|
|
840
|
+
* @param node - The key node.
|
|
841
|
+
* @returns The value as a string, or `null` when it is not statically known.
|
|
842
|
+
*/
|
|
843
|
+
function getStaticStringValue(node) {
|
|
844
|
+
if (node.type === AST_NODE_TYPES.Literal) return node.value === null ? node.raw : String(node.value);
|
|
845
|
+
if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0]?.value.cooked ?? null;
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Reads the property name of a member definition.
|
|
850
|
+
*
|
|
851
|
+
* @param node - The member whose key to read.
|
|
852
|
+
* @returns The property name, or `null` when the key is computed from a
|
|
853
|
+
* dynamic expression.
|
|
854
|
+
*/
|
|
855
|
+
function getStaticPropertyName(node) {
|
|
856
|
+
if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) return node.key.name;
|
|
857
|
+
return getStaticStringValue(node.key);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Finds the token opening a function's parameter list.
|
|
861
|
+
*
|
|
862
|
+
* @param node - The function whose parameters to locate.
|
|
863
|
+
* @param sourceCode - The source code being linted.
|
|
864
|
+
* @returns The opening paren, or — for an arrow function with a single
|
|
865
|
+
* parameter written without parentheses — that parameter's first token.
|
|
866
|
+
* `null` when neither can be found.
|
|
867
|
+
*/
|
|
868
|
+
function getOpeningParenOfParameters(node, sourceCode) {
|
|
869
|
+
const [firstParameter] = node.params;
|
|
870
|
+
if (node.type === AST_NODE_TYPES.ArrowFunctionExpression && node.params.length === 1 && firstParameter !== void 0) {
|
|
871
|
+
const argumentToken = sourceCode.getFirstToken(firstParameter);
|
|
872
|
+
if (argumentToken === null) return null;
|
|
873
|
+
const maybeParenToken = sourceCode.getTokenBefore(argumentToken);
|
|
874
|
+
return maybeParenToken !== null && isOpeningParenToken(maybeParenToken) ? maybeParenToken : argumentToken;
|
|
875
|
+
}
|
|
876
|
+
return node.id === null ? sourceCode.getFirstToken(node, isOpeningParenToken) : sourceCode.getTokenAfter(node.id, isOpeningParenToken);
|
|
877
|
+
}
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/rules/max-lines-per-function/rule.ts
|
|
880
|
+
const RULE_NAME$12 = "max-lines-per-function";
|
|
881
|
+
const MESSAGE_ID_EXCEED = "exceed";
|
|
882
|
+
/** A line that is empty or holds only whitespace. */
|
|
883
|
+
const BLANK_LINE = /^\s*$/u;
|
|
884
|
+
const DEFAULTS = {
|
|
885
|
+
countFrom: "body",
|
|
886
|
+
IIFEs: false,
|
|
887
|
+
max: 50,
|
|
888
|
+
skipBlankLines: false,
|
|
889
|
+
skipComments: false
|
|
890
|
+
};
|
|
891
|
+
const messages$12 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
|
|
892
|
+
const schema$2 = [{
|
|
893
|
+
additionalProperties: false,
|
|
894
|
+
properties: {
|
|
895
|
+
countFrom: {
|
|
896
|
+
description: "Where the counted range begins: \"body\" (default) excludes the signature, \"function\" counts the whole node like ESLint core.",
|
|
897
|
+
enum: ["body", "function"],
|
|
898
|
+
type: "string"
|
|
899
|
+
},
|
|
900
|
+
IIFEs: {
|
|
901
|
+
description: "Whether immediately-invoked function expressions are measured.",
|
|
902
|
+
type: "boolean"
|
|
903
|
+
},
|
|
904
|
+
max: {
|
|
905
|
+
description: "The maximum number of lines a function may span.",
|
|
906
|
+
minimum: 0,
|
|
907
|
+
type: "integer"
|
|
908
|
+
},
|
|
909
|
+
skipBlankLines: {
|
|
910
|
+
description: "Whether lines containing only whitespace are excluded from the count.",
|
|
911
|
+
type: "boolean"
|
|
912
|
+
},
|
|
913
|
+
skipComments: {
|
|
914
|
+
description: "Whether lines consisting solely of a comment are excluded from the count.",
|
|
915
|
+
type: "boolean"
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
type: "object"
|
|
919
|
+
}];
|
|
920
|
+
/**
|
|
921
|
+
* Indexes comments by every source line they occupy, so a line can be tested
|
|
922
|
+
* for comment-only status in constant time.
|
|
923
|
+
*
|
|
924
|
+
* @param comments - Every comment in the file.
|
|
925
|
+
* @returns A map from one-indexed line number to the comment on that line.
|
|
926
|
+
*/
|
|
927
|
+
function getCommentLineNumbers(comments) {
|
|
928
|
+
const map = /* @__PURE__ */ new Map();
|
|
929
|
+
for (const comment of comments) for (let { line } = comment.loc.start; line <= comment.loc.end.line; line += 1) map.set(line, comment);
|
|
930
|
+
return map;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Determines whether a comment occupies a whole line, leaving no code beside
|
|
934
|
+
* it.
|
|
935
|
+
*
|
|
936
|
+
* @param line - The source text of the line.
|
|
937
|
+
* @param lineNumber - The one-indexed number of that line.
|
|
938
|
+
* @param comment - The comment occupying part of the line.
|
|
939
|
+
* @returns `true` when nothing but the comment appears on the line.
|
|
940
|
+
*/
|
|
941
|
+
function isFullLineComment(line, lineNumber, comment) {
|
|
942
|
+
const { end, start } = comment.loc;
|
|
943
|
+
const isFirstTokenOnLine = start.line === lineNumber && line.slice(0, start.column).trim() === "";
|
|
944
|
+
const isLastTokenOnLine = end.line === lineNumber && line.slice(end.column).trim() === "";
|
|
945
|
+
return (start.line < lineNumber || isFirstTokenOnLine) && (end.line > lineNumber || isLastTokenOnLine);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Determines whether a function is the callee of its own call expression.
|
|
949
|
+
*
|
|
950
|
+
* @param node - The node to test.
|
|
951
|
+
* @returns `true` when the node is immediately invoked.
|
|
952
|
+
*/
|
|
953
|
+
function isIIFE(node) {
|
|
954
|
+
if (node.type !== AST_NODE_TYPES.ArrowFunctionExpression && node.type !== AST_NODE_TYPES.FunctionExpression) return false;
|
|
955
|
+
const { parent } = node;
|
|
956
|
+
return parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Determines whether a function is the value of a class method or of an object
|
|
960
|
+
* shorthand method or accessor. In that case the enclosing member — not the
|
|
961
|
+
* bare function expression — is the unit measured and reported.
|
|
962
|
+
*
|
|
963
|
+
* @param node - The function to test.
|
|
964
|
+
* @returns `true` when the function is embedded in a member definition.
|
|
965
|
+
*/
|
|
966
|
+
function isEmbedded(node) {
|
|
967
|
+
const { parent } = node;
|
|
968
|
+
if (parent.type === AST_NODE_TYPES.MethodDefinition) return parent.value === node;
|
|
969
|
+
if (parent.type === AST_NODE_TYPES.Property) return parent.value === node && (parent.method || parent.kind === "get" || parent.kind === "set");
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Selects the source range whose lines are counted for a function.
|
|
974
|
+
*
|
|
975
|
+
* Under `"body"` this is the function's body — the braces themselves for a
|
|
976
|
+
* block body, or the expression for a concise arrow body — so the signature is
|
|
977
|
+
* excluded. Under `"function"` it is the reported node, matching ESLint core.
|
|
978
|
+
*
|
|
979
|
+
* @param countFrom - Which range to measure.
|
|
980
|
+
* @param funcNode - The function being measured.
|
|
981
|
+
* @param reportNode - The node the diagnostic attaches to: the enclosing member
|
|
982
|
+
* for an embedded method, otherwise `funcNode` itself.
|
|
983
|
+
* @returns The location whose line span is counted.
|
|
984
|
+
*/
|
|
985
|
+
function getCountedLoc(countFrom, funcNode, reportNode) {
|
|
986
|
+
return countFrom === "function" ? reportNode.loc : funcNode.body.loc;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Builds the rule's per-file listener.
|
|
990
|
+
*
|
|
991
|
+
* @param context - The rule context.
|
|
992
|
+
* @returns The visitors measuring each function.
|
|
993
|
+
*/
|
|
994
|
+
function createOnce$7(context) {
|
|
995
|
+
let commentLineNumbers;
|
|
996
|
+
let config;
|
|
997
|
+
let lines;
|
|
998
|
+
let sourceCode;
|
|
999
|
+
/**
|
|
1000
|
+
* Counts a function's lines and reports it when it exceeds the maximum.
|
|
1001
|
+
*
|
|
1002
|
+
* @param funcNode - The function to measure.
|
|
1003
|
+
*/
|
|
1004
|
+
function processFunction(funcNode) {
|
|
1005
|
+
const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
|
|
1006
|
+
if (!config.IIFEs && isIIFE(node)) return;
|
|
1007
|
+
const { end, start } = getCountedLoc(config.countFrom, funcNode, node);
|
|
1008
|
+
let lineCount = 0;
|
|
1009
|
+
for (let index = start.line - 1; index < end.line; index += 1) {
|
|
1010
|
+
const line = lines[index] ?? "";
|
|
1011
|
+
const lineNumber = index + 1;
|
|
1012
|
+
if (config.skipComments) {
|
|
1013
|
+
const comment = commentLineNumbers.get(lineNumber);
|
|
1014
|
+
if (comment !== void 0 && isFullLineComment(line, lineNumber, comment)) continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (config.skipBlankLines && BLANK_LINE.test(line)) continue;
|
|
1017
|
+
lineCount += 1;
|
|
1018
|
+
}
|
|
1019
|
+
if (lineCount > config.max) context.report({
|
|
1020
|
+
data: {
|
|
1021
|
+
name: upperCaseFirst(getFunctionNameWithKind(funcNode)),
|
|
1022
|
+
lineCount,
|
|
1023
|
+
maxLines: config.max
|
|
1024
|
+
},
|
|
1025
|
+
loc: getFunctionHeadLoc(funcNode, sourceCode),
|
|
1026
|
+
messageId: MESSAGE_ID_EXCEED,
|
|
1027
|
+
node
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
ArrowFunctionExpression: processFunction,
|
|
1032
|
+
before() {
|
|
1033
|
+
const options = context.options[0];
|
|
1034
|
+
config = {
|
|
1035
|
+
countFrom: options?.countFrom ?? DEFAULTS.countFrom,
|
|
1036
|
+
IIFEs: options?.IIFEs ?? DEFAULTS.IIFEs,
|
|
1037
|
+
max: options?.max ?? DEFAULTS.max,
|
|
1038
|
+
skipBlankLines: options?.skipBlankLines ?? DEFAULTS.skipBlankLines,
|
|
1039
|
+
skipComments: options?.skipComments ?? DEFAULTS.skipComments
|
|
1040
|
+
};
|
|
1041
|
+
({sourceCode} = context);
|
|
1042
|
+
lines = [...sourceCode.lines];
|
|
1043
|
+
commentLineNumbers = config.skipComments ? getCommentLineNumbers(sourceCode.getAllComments()) : /* @__PURE__ */ new Map();
|
|
1044
|
+
},
|
|
1045
|
+
FunctionDeclaration: processFunction,
|
|
1046
|
+
FunctionExpression: processFunction
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const maxLinesPerFunction = createFlawlessRule({
|
|
1050
|
+
name: RULE_NAME$12,
|
|
1051
|
+
createOnce: createOnce$7,
|
|
1052
|
+
defaultOptions: [DEFAULTS],
|
|
1053
|
+
meta: {
|
|
1054
|
+
defaultOptions: [DEFAULTS],
|
|
1055
|
+
docs: {
|
|
1056
|
+
description: "Enforce a maximum number of lines of code in a function",
|
|
1057
|
+
recommended: false,
|
|
1058
|
+
requiresTypeChecking: false
|
|
1059
|
+
},
|
|
1060
|
+
hasSuggestions: false,
|
|
1061
|
+
messages: messages$12,
|
|
682
1062
|
schema: schema$2,
|
|
683
1063
|
type: "suggestion"
|
|
684
1064
|
}
|
|
@@ -1158,6 +1538,77 @@ function isUsedVariable(variable) {
|
|
|
1158
1538
|
});
|
|
1159
1539
|
}
|
|
1160
1540
|
//#endregion
|
|
1541
|
+
//#region src/rules/naming-convention/utils/contextual-type.ts
|
|
1542
|
+
/**
|
|
1543
|
+
* Determines if an object literal member's name is dictated by the contextual
|
|
1544
|
+
* type of the enclosing object literal (e.g. `{ ... } satisfies
|
|
1545
|
+
* Partial<Service>`). In that case the name is not the author's choice - it is
|
|
1546
|
+
* required by the declared type, which is itself validated at its declaration
|
|
1547
|
+
* site - so naming validation should be skipped.
|
|
1548
|
+
*
|
|
1549
|
+
* @param node - The non-computed object literal property or method node.
|
|
1550
|
+
* @param services - Parser services (may lack type information).
|
|
1551
|
+
* @param cache - Per-file cache of contextual types keyed by object literal.
|
|
1552
|
+
* @returns True if the member name is required by a contextual type.
|
|
1553
|
+
*/
|
|
1554
|
+
function isDictatedByContextualType(node, services, cache) {
|
|
1555
|
+
if (!services.program || node.parent.type !== AST_NODE_TYPES.ObjectExpression) return false;
|
|
1556
|
+
const checker = services.program.getTypeChecker();
|
|
1557
|
+
const tsObject = services.esTreeNodeToTSNodeMap.get(node.parent);
|
|
1558
|
+
const candidates = getCandidateTypes(node.parent, tsObject, checker, cache);
|
|
1559
|
+
if (candidates === null) return false;
|
|
1560
|
+
const name = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : String(node.key.value);
|
|
1561
|
+
return candidates.some((candidate) => {
|
|
1562
|
+
const property = checker.getPropertyOfType(candidate, name);
|
|
1563
|
+
return property !== void 0 && !isDeclaredWithinLiteral(property, tsObject);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Resolves the contextual type of an object literal into its candidate
|
|
1568
|
+
* constituent types, cached per object literal.
|
|
1569
|
+
*
|
|
1570
|
+
* Union arms are checked individually because `getPropertyOfType` on a union
|
|
1571
|
+
* only finds properties present in every arm, while a name dictated by a
|
|
1572
|
+
* single arm still isn't the author's choice.
|
|
1573
|
+
*
|
|
1574
|
+
* @param objectNode - The enclosing object literal (ESTree).
|
|
1575
|
+
* @param tsObject - The corresponding TypeScript node.
|
|
1576
|
+
* @param checker - The type checker.
|
|
1577
|
+
* @param cache - Per-file cache of contextual types keyed by object literal.
|
|
1578
|
+
* @returns The candidate types, or null if there is no contextual type.
|
|
1579
|
+
*/
|
|
1580
|
+
function getCandidateTypes(objectNode, tsObject, checker, cache) {
|
|
1581
|
+
const cached = cache.get(objectNode);
|
|
1582
|
+
if (cached !== void 0) return cached;
|
|
1583
|
+
const contextualType = checker.getContextualType(tsObject);
|
|
1584
|
+
let candidates = null;
|
|
1585
|
+
if (contextualType !== void 0) {
|
|
1586
|
+
const nonNullable = contextualType.getNonNullableType();
|
|
1587
|
+
candidates = (nonNullable.isUnion() ? nonNullable.types : [nonNullable]).map((arm) => checker.getApparentType(arm));
|
|
1588
|
+
}
|
|
1589
|
+
cache.set(objectNode, candidates);
|
|
1590
|
+
return candidates;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Guards against self-inference: in generic calls like `identity({ Name: 1 })`
|
|
1594
|
+
* the contextual type is inferred from the literal itself, so the property
|
|
1595
|
+
* symbol's declarations all live inside the literal - the name is still the
|
|
1596
|
+
* author's choice. Transient symbols without declarations (such as those from
|
|
1597
|
+
* mapped types like `Partial<T>`) originate from the contextual type and count
|
|
1598
|
+
* as dictated.
|
|
1599
|
+
*
|
|
1600
|
+
* @param symbol - The property symbol found on a candidate contextual type.
|
|
1601
|
+
* @param literal - The object literal being checked.
|
|
1602
|
+
* @returns True if every declaration of the symbol lies inside the literal.
|
|
1603
|
+
*/
|
|
1604
|
+
function isDeclaredWithinLiteral(symbol, literal) {
|
|
1605
|
+
const declarations = symbol.declarations ?? [];
|
|
1606
|
+
if (declarations.length === 0) return false;
|
|
1607
|
+
return declarations.every((declaration) => {
|
|
1608
|
+
return declaration.getSourceFile() === literal.getSourceFile() && declaration.pos >= literal.pos && declaration.end <= literal.end;
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1161
1612
|
//#region src/rules/naming-convention/utils/enums.ts
|
|
1162
1613
|
const Selector = {
|
|
1163
1614
|
variable: 1,
|
|
@@ -1226,6 +1677,8 @@ const TypeModifier = {
|
|
|
1226
1677
|
array: 2097152
|
|
1227
1678
|
};
|
|
1228
1679
|
const TypeModifierValueToKey = Object.fromEntries(Object.entries(TypeModifier).map(([key, value]) => [value, key]));
|
|
1680
|
+
const TYPE_REFERENCE_LOOSE_WEIGHT = 1 << 22;
|
|
1681
|
+
const TYPE_REFERENCE_STRICT_WEIGHT = 1 << 23;
|
|
1229
1682
|
const UnderscoreOption = {
|
|
1230
1683
|
forbid: 1,
|
|
1231
1684
|
allow: 2,
|
|
@@ -1506,31 +1959,130 @@ function createValidator(type, context, allConfigs) {
|
|
|
1506
1959
|
return false;
|
|
1507
1960
|
}
|
|
1508
1961
|
}
|
|
1509
|
-
const SelectorsAllowedToHaveTypes = Selector.variable | Selector.parameter | Selector.classProperty | Selector.objectLiteralProperty | Selector.typeProperty | Selector.parameterProperty | Selector.classicAccessor;
|
|
1962
|
+
const SelectorsAllowedToHaveTypes = Selector.variable | Selector.function | Selector.parameter | Selector.classProperty | Selector.objectLiteralProperty | Selector.typeProperty | Selector.parameterProperty | Selector.classicAccessor | Selector.classMethod | Selector.objectLiteralMethod | Selector.typeMethod;
|
|
1510
1963
|
function isAllTypesMatch(type, callback) {
|
|
1511
1964
|
if (type.isUnion()) return type.types.every((inner) => callback(inner));
|
|
1512
1965
|
return callback(type);
|
|
1513
1966
|
}
|
|
1967
|
+
function isAnyType(type) {
|
|
1968
|
+
return (type.flags & TypeFlags.Any) !== 0;
|
|
1969
|
+
}
|
|
1970
|
+
function symbolMatchesTypeReference(symbol, reference) {
|
|
1971
|
+
if (symbol === void 0 || reference.name === void 0 || symbol.name !== reference.name) return false;
|
|
1972
|
+
if (reference.from === void 0) return true;
|
|
1973
|
+
const { declarations } = symbol;
|
|
1974
|
+
if (!declarations || declarations.length === 0) return false;
|
|
1975
|
+
for (const declaration of declarations) {
|
|
1976
|
+
const { fileName } = declaration.getSourceFile();
|
|
1977
|
+
if (moduleSpecifierMatches(fileName, reference.from)) return true;
|
|
1978
|
+
}
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
function matchesNamedTypeReference(type, reference) {
|
|
1982
|
+
if (isAnyType(type)) return false;
|
|
1983
|
+
if (symbolMatchesTypeReference(type.aliasSymbol, reference)) return true;
|
|
1984
|
+
if (symbolMatchesTypeReference(type.symbol, reference)) return true;
|
|
1985
|
+
if (type.isIntersection() || type.isUnion()) {
|
|
1986
|
+
for (const inner of type.types) if (matchesNamedTypeReference(inner, reference)) return true;
|
|
1987
|
+
}
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
function matchesReturnTypeReference(type, reference) {
|
|
1991
|
+
if (isAnyType(type)) return false;
|
|
1992
|
+
for (const signature of type.getCallSignatures()) if (matchesTypeReference(signature.getReturnType(), reference)) return true;
|
|
1993
|
+
if (type.isIntersection() || type.isUnion()) {
|
|
1994
|
+
for (const inner of type.types) if (matchesReturnTypeReference(inner, reference)) return true;
|
|
1995
|
+
}
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
function matchesTypeReference(type, reference) {
|
|
1999
|
+
if (isAnyType(type)) return false;
|
|
2000
|
+
if (reference.name === void 0 && reference.returns === void 0) return false;
|
|
2001
|
+
if (reference.name !== void 0 && !matchesNamedTypeReference(type, reference)) return false;
|
|
2002
|
+
if (reference.returns !== void 0 && !matchesReturnTypeReference(type, reference.returns)) return false;
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
1514
2005
|
function isCorrectType(node, config, context, selector) {
|
|
1515
2006
|
if (config.types === void 0) return true;
|
|
1516
2007
|
if ((SelectorsAllowedToHaveTypes & selector) === 0) return true;
|
|
1517
2008
|
const services = getParserServices(context);
|
|
1518
2009
|
const checker = services.program.getTypeChecker();
|
|
1519
2010
|
const type = services.getTypeAtLocation(node).getNonNullableType();
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2011
|
+
const predicates = [];
|
|
2012
|
+
for (const allowedType of config.types) {
|
|
2013
|
+
if (typeof allowedType === "object") {
|
|
2014
|
+
predicates.push((inner) => matchesTypeReference(inner, allowedType));
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
switch (allowedType) {
|
|
2018
|
+
case TypeModifier.array:
|
|
2019
|
+
predicates.push((inner) => checker.isArrayType(inner) || checker.isTupleType(inner));
|
|
2020
|
+
break;
|
|
2021
|
+
case TypeModifier.boolean:
|
|
2022
|
+
case TypeModifier.number:
|
|
2023
|
+
case TypeModifier.string:
|
|
2024
|
+
predicates.push((inner) => {
|
|
2025
|
+
return checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(inner))) === TypeModifierValueToKey[allowedType];
|
|
2026
|
+
});
|
|
2027
|
+
break;
|
|
2028
|
+
case TypeModifier.function:
|
|
2029
|
+
predicates.push((inner) => inner.getCallSignatures().length > 0);
|
|
2030
|
+
break;
|
|
2031
|
+
}
|
|
1532
2032
|
}
|
|
1533
|
-
return
|
|
2033
|
+
return isAllTypesMatch(type, (inner) => predicates.some((predicate) => predicate(inner)));
|
|
2034
|
+
}
|
|
2035
|
+
const BACKSLASH_PATTERN = /\\/gu;
|
|
2036
|
+
const TYPESCRIPT_EXTENSION_PATTERN = /\.d\.ts$|\.tsx?$/u;
|
|
2037
|
+
const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\//u;
|
|
2038
|
+
const LEADING_DOT_OR_SLASH_PATTERN = /^(\.\/|\/)/u;
|
|
2039
|
+
/**
|
|
2040
|
+
* Path-form specifiers start with `.`, `/`, or a Windows drive letter (e.g.
|
|
2041
|
+
* `C:/`).
|
|
2042
|
+
*
|
|
2043
|
+
* @param specifier - The module specifier to classify.
|
|
2044
|
+
* @returns True if the specifier should be treated as a filesystem path rather
|
|
2045
|
+
* than a package name.
|
|
2046
|
+
*/
|
|
2047
|
+
function looksLikePath(specifier) {
|
|
2048
|
+
if (specifier.startsWith(".")) return true;
|
|
2049
|
+
if (specifier.startsWith("/")) return true;
|
|
2050
|
+
return WINDOWS_DRIVE_PATTERN.test(specifier);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Checks whether a declaration's source file matches a module specifier.
|
|
2054
|
+
*
|
|
2055
|
+
* Two specifier shapes are supported:
|
|
2056
|
+
*
|
|
2057
|
+
* 1. **Bare package specifier** (e.g. `"@rbxts/jecs"`, `"lodash"`) — matches
|
|
2058
|
+
* when the declaration's file path contains `/node_modules/<specifier>/` as
|
|
2059
|
+
* a substring. Handles flat and pnpm-style layouts (pnpm paths still contain
|
|
2060
|
+
* a final `/node_modules/<specifier>/` segment after the virtual store
|
|
2061
|
+
* directory). **Not** supported: Yarn Plug'n'Play (no `node_modules` on
|
|
2062
|
+
* disk), vendored packages outside `node_modules`, or types provided by
|
|
2063
|
+
* separate `@types/*` packages.
|
|
2064
|
+
* 2. **Path specifier** (starts with `.`, `/`, or a Windows drive letter) —
|
|
2065
|
+
* matches against the normalized declaration path with `.d.ts` / `.tsx?`
|
|
2066
|
+
* stripped. Windows absolute paths require exact equality. POSIX-style
|
|
2067
|
+
* absolute or relative paths are normalized to a bare tail and matched as a
|
|
2068
|
+
* suffix; this means `"./shared/network"` matches a declaration at
|
|
2069
|
+
* `<root>/shared/network.ts`.
|
|
2070
|
+
*
|
|
2071
|
+
* @param declarationFile - Path of the file declaring the matched symbol.
|
|
2072
|
+
* @param specifier - The `from` module specifier from the rule config.
|
|
2073
|
+
* @returns True if the declaration file matches the specifier.
|
|
2074
|
+
*/
|
|
2075
|
+
function moduleSpecifierMatches(declarationFile, specifier) {
|
|
2076
|
+
const normalizedFile = declarationFile.replace(BACKSLASH_PATTERN, "/");
|
|
2077
|
+
const normalizedSpecifier = specifier.replace(BACKSLASH_PATTERN, "/");
|
|
2078
|
+
if (looksLikePath(normalizedSpecifier)) {
|
|
2079
|
+
const stripped = normalizedFile.replace(TYPESCRIPT_EXTENSION_PATTERN, "");
|
|
2080
|
+
if (WINDOWS_DRIVE_PATTERN.test(normalizedSpecifier)) return stripped === normalizedSpecifier;
|
|
2081
|
+
const tail = normalizedSpecifier.replace(LEADING_DOT_OR_SLASH_PATTERN, "");
|
|
2082
|
+
if (stripped === tail) return true;
|
|
2083
|
+
return stripped.endsWith(`/${tail}`);
|
|
2084
|
+
}
|
|
2085
|
+
return normalizedFile.includes(`/node_modules/${normalizedSpecifier}/`);
|
|
1534
2086
|
}
|
|
1535
2087
|
//#endregion
|
|
1536
2088
|
//#region src/rules/naming-convention/utils/parse-options.ts
|
|
@@ -1538,10 +2090,22 @@ function parseOptions(context) {
|
|
|
1538
2090
|
const normalizedOptions = context.options.flatMap(normalizeOption);
|
|
1539
2091
|
return Object.fromEntries(Object.keys(Selector).map((key) => [key, createValidator(key, context, normalizedOptions)]));
|
|
1540
2092
|
}
|
|
2093
|
+
/**
|
|
2094
|
+
* A type-reference matcher counts as "strict" for sorting when any level of it
|
|
2095
|
+
* (including nested `returns` matchers) constrains the declaring module.
|
|
2096
|
+
*
|
|
2097
|
+
* @param reference - The matcher to inspect.
|
|
2098
|
+
* @returns True if the matcher carries a `from` constraint at any depth.
|
|
2099
|
+
*/
|
|
2100
|
+
function hasFromConstraint(reference) {
|
|
2101
|
+
if (reference.from !== void 0) return true;
|
|
2102
|
+
return reference.returns !== void 0 && hasFromConstraint(reference.returns);
|
|
2103
|
+
}
|
|
1541
2104
|
function normalizeOption(option) {
|
|
1542
2105
|
let weight = 0;
|
|
1543
2106
|
if (option.modifiers) for (const modifier of option.modifiers) weight |= Modifier[modifier];
|
|
1544
|
-
if (option.types) for (const type of option.types) weight |= TypeModifier[type];
|
|
2107
|
+
if (option.types) for (const type of option.types) if (typeof type === "string") weight |= TypeModifier[type];
|
|
2108
|
+
else weight |= hasFromConstraint(type) ? TYPE_REFERENCE_STRICT_WEIGHT : TYPE_REFERENCE_LOOSE_WEIGHT;
|
|
1545
2109
|
if (option.filter !== void 0) weight |= 1 << 30;
|
|
1546
2110
|
const normalizedOption = {
|
|
1547
2111
|
custom: option.custom ? {
|
|
@@ -1562,7 +2126,7 @@ function normalizeOption(option) {
|
|
|
1562
2126
|
prefix: option.prefix && option.prefix.length > 0 ? option.prefix : void 0,
|
|
1563
2127
|
suffix: option.suffix && option.suffix.length > 0 ? option.suffix : void 0,
|
|
1564
2128
|
trailingUnderscore: option.trailingUnderscore !== void 0 ? UnderscoreOption[option.trailingUnderscore] : void 0,
|
|
1565
|
-
types: option.types?.map((type) => TypeModifier[type]) ?? void 0
|
|
2129
|
+
types: option.types?.map((type) => typeof type === "string" ? TypeModifier[type] : type) ?? void 0
|
|
1566
2130
|
};
|
|
1567
2131
|
return (Array.isArray(option.selector) ? option.selector : [option.selector]).map((selector) => {
|
|
1568
2132
|
return {
|
|
@@ -1600,9 +2164,39 @@ const $DEFS = {
|
|
|
1600
2164
|
},
|
|
1601
2165
|
type: "array"
|
|
1602
2166
|
},
|
|
1603
|
-
|
|
2167
|
+
typeMatcher: { oneOf: [{
|
|
2168
|
+
description: "Built-in type modifier matching by widened TS type.",
|
|
1604
2169
|
enum: Object.keys(TypeModifier),
|
|
1605
2170
|
type: "string"
|
|
2171
|
+
}, { $ref: "#/$defs/typeReferenceMatcher" }] },
|
|
2172
|
+
typeReferenceMatcher: {
|
|
2173
|
+
additionalProperties: false,
|
|
2174
|
+
anyOf: [{
|
|
2175
|
+
required: ["name"],
|
|
2176
|
+
type: "object"
|
|
2177
|
+
}, {
|
|
2178
|
+
required: ["returns"],
|
|
2179
|
+
type: "object"
|
|
2180
|
+
}],
|
|
2181
|
+
dependencies: { from: ["name"] },
|
|
2182
|
+
description: "Type-reference matcher. Matches when the variable's type resolves to a symbol with the given `name`; if `from` is supplied, the symbol's declaration must also live in a file the specifier resolves to. `returns` instead (or additionally) matches callable types by the return type of their call signatures.",
|
|
2183
|
+
properties: {
|
|
2184
|
+
name: {
|
|
2185
|
+
description: "Symbol name to match (compared against the type's `aliasSymbol` then `symbol`).",
|
|
2186
|
+
minLength: 1,
|
|
2187
|
+
type: "string"
|
|
2188
|
+
},
|
|
2189
|
+
from: {
|
|
2190
|
+
description: "Module specifier the type must originate from. Bare package name (e.g. `@rbxts/jecs`) matches `/node_modules/<from>/` in the declaration path. Path-form (starts with `.`, `/`, or a Windows drive letter) matches the declaration path with extension stripped. Omit to match any source.",
|
|
2191
|
+
minLength: 1,
|
|
2192
|
+
type: "string"
|
|
2193
|
+
},
|
|
2194
|
+
returns: {
|
|
2195
|
+
$ref: "#/$defs/typeReferenceMatcher",
|
|
2196
|
+
description: "Nested matcher applied to call-signature return types. The type matches when at least one call signature's return type satisfies it. Lets anonymous function types (which have no symbol name) be matched, e.g. React components typed `(props: P) => ReactNode`."
|
|
2197
|
+
}
|
|
2198
|
+
},
|
|
2199
|
+
type: "object"
|
|
1606
2200
|
},
|
|
1607
2201
|
underscoreOptions: {
|
|
1608
2202
|
enum: Object.keys(UnderscoreOption),
|
|
@@ -1642,7 +2236,7 @@ function selectorSchema(selectorString, allowType, modifiers) {
|
|
|
1642
2236
|
};
|
|
1643
2237
|
if (allowType) selector["types"] = {
|
|
1644
2238
|
additionalItems: false,
|
|
1645
|
-
items: { $ref: "#/$defs/
|
|
2239
|
+
items: { $ref: "#/$defs/typeMatcher" },
|
|
1646
2240
|
type: "array"
|
|
1647
2241
|
};
|
|
1648
2242
|
return [{
|
|
@@ -1684,7 +2278,7 @@ function selectorsSchema() {
|
|
|
1684
2278
|
},
|
|
1685
2279
|
types: {
|
|
1686
2280
|
additionalItems: false,
|
|
1687
|
-
items: { $ref: "#/$defs/
|
|
2281
|
+
items: { $ref: "#/$defs/typeMatcher" },
|
|
1688
2282
|
type: "array"
|
|
1689
2283
|
}
|
|
1690
2284
|
},
|
|
@@ -1707,7 +2301,7 @@ const SCHEMA = {
|
|
|
1707
2301
|
"unused",
|
|
1708
2302
|
"async"
|
|
1709
2303
|
]),
|
|
1710
|
-
...selectorSchema("function",
|
|
2304
|
+
...selectorSchema("function", true, [
|
|
1711
2305
|
"exported",
|
|
1712
2306
|
"global",
|
|
1713
2307
|
"unused",
|
|
@@ -1767,7 +2361,7 @@ const SCHEMA = {
|
|
|
1767
2361
|
"override",
|
|
1768
2362
|
"async"
|
|
1769
2363
|
]),
|
|
1770
|
-
...selectorSchema("classMethod",
|
|
2364
|
+
...selectorSchema("classMethod", true, [
|
|
1771
2365
|
"abstract",
|
|
1772
2366
|
"private",
|
|
1773
2367
|
"#private",
|
|
@@ -1778,13 +2372,13 @@ const SCHEMA = {
|
|
|
1778
2372
|
"override",
|
|
1779
2373
|
"async"
|
|
1780
2374
|
]),
|
|
1781
|
-
...selectorSchema("objectLiteralMethod",
|
|
2375
|
+
...selectorSchema("objectLiteralMethod", true, [
|
|
1782
2376
|
"public",
|
|
1783
2377
|
"requiresQuotes",
|
|
1784
2378
|
"async"
|
|
1785
2379
|
]),
|
|
1786
|
-
...selectorSchema("typeMethod",
|
|
1787
|
-
...selectorSchema("method",
|
|
2380
|
+
...selectorSchema("typeMethod", true, ["public", "requiresQuotes"]),
|
|
2381
|
+
...selectorSchema("method", true, [
|
|
1788
2382
|
"abstract",
|
|
1789
2383
|
"private",
|
|
1790
2384
|
"#private",
|
|
@@ -1843,8 +2437,8 @@ const SCHEMA = {
|
|
|
1843
2437
|
};
|
|
1844
2438
|
//#endregion
|
|
1845
2439
|
//#region src/rules/naming-convention/rule.ts
|
|
1846
|
-
const RULE_NAME$
|
|
1847
|
-
const messages$
|
|
2440
|
+
const RULE_NAME$11 = "naming-convention";
|
|
2441
|
+
const messages$11 = {
|
|
1848
2442
|
doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
|
|
1849
2443
|
doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
|
|
1850
2444
|
missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
|
|
@@ -1874,25 +2468,16 @@ const camelCaseNamingConfig = [
|
|
|
1874
2468
|
selector: "typeLike"
|
|
1875
2469
|
}
|
|
1876
2470
|
];
|
|
1877
|
-
function create$
|
|
2471
|
+
function create$4(contextWithoutDefaults) {
|
|
1878
2472
|
const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
|
|
1879
2473
|
const validators = parseOptions(context);
|
|
1880
|
-
const
|
|
2474
|
+
const services = getParserServices(context, true);
|
|
2475
|
+
const compilerOptions = services.program?.getCompilerOptions() ?? {};
|
|
2476
|
+
const contextualTypeCache = /* @__PURE__ */ new WeakMap();
|
|
1881
2477
|
function handleMember(validator, { key }, modifiers) {
|
|
1882
|
-
if (requiresQuoting
|
|
2478
|
+
if (requiresQuoting(key, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
1883
2479
|
validator(key, modifiers);
|
|
1884
2480
|
}
|
|
1885
|
-
function getMemberModifiers(node) {
|
|
1886
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
1887
|
-
if ("key" in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) modifiers.add(Modifier["#private"]);
|
|
1888
|
-
else if (node.accessibility) modifiers.add(Modifier[node.accessibility]);
|
|
1889
|
-
else modifiers.add(Modifier.public);
|
|
1890
|
-
if (node.static) modifiers.add(Modifier.static);
|
|
1891
|
-
if ("readonly" in node && node.readonly) modifiers.add(Modifier.readonly);
|
|
1892
|
-
if ("override" in node && node.override) modifiers.add(Modifier.override);
|
|
1893
|
-
if (node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition || node.type === AST_NODE_TYPES.TSAbstractAccessorProperty) modifiers.add(Modifier.abstract);
|
|
1894
|
-
return modifiers;
|
|
1895
|
-
}
|
|
1896
2481
|
const { unusedVariables } = collectVariables(context);
|
|
1897
2482
|
function isUnused(name, initialScope) {
|
|
1898
2483
|
let variable = null;
|
|
@@ -1905,52 +2490,6 @@ function create$3(contextWithoutDefaults) {
|
|
|
1905
2490
|
if (!variable) return false;
|
|
1906
2491
|
return unusedVariables.has(variable);
|
|
1907
2492
|
}
|
|
1908
|
-
function isDestructured(id) {
|
|
1909
|
-
return id.parent.type === AST_NODE_TYPES.Property && id.parent.shorthand || id.parent.type === AST_NODE_TYPES.AssignmentPattern && id.parent.parent.type === AST_NODE_TYPES.Property && id.parent.parent.shorthand;
|
|
1910
|
-
}
|
|
1911
|
-
function isAsyncMemberOrProperty(propertyOrMemberNode) {
|
|
1912
|
-
return Boolean("value" in propertyOrMemberNode && propertyOrMemberNode.value && "async" in propertyOrMemberNode.value && propertyOrMemberNode.value.async);
|
|
1913
|
-
}
|
|
1914
|
-
function isAsyncVariableIdentifier(id) {
|
|
1915
|
-
return Boolean("async" in id.parent && id.parent.async || "init" in id.parent && id.parent.init && "async" in id.parent.init && id.parent.init.async);
|
|
1916
|
-
}
|
|
1917
|
-
/**
|
|
1918
|
-
* Determines if a VariableDeclarator represents an object-style enum declaration.
|
|
1919
|
-
*
|
|
1920
|
-
* Object-style enums are const assertions applied to object expressions, commonly
|
|
1921
|
-
* used as an alternative to TypeScript enums. Examples:
|
|
1922
|
-
* - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
|
|
1923
|
-
* - `const Status = <const>{ OK: 200, ERROR: 500 }`.
|
|
1924
|
-
*
|
|
1925
|
-
* @param node - The VariableDeclarator AST node to check.
|
|
1926
|
-
* @param parent - The parent VariableDeclaration node.
|
|
1927
|
-
* @returns True if this represents an object-style enum declaration.
|
|
1928
|
-
*/
|
|
1929
|
-
function isObjectStyleEnumDeclaration(node, parent) {
|
|
1930
|
-
if (parent.kind !== "const") return false;
|
|
1931
|
-
if (!node.init) return false;
|
|
1932
|
-
if (node.init.type === AST_NODE_TYPES.TSAsExpression && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
1933
|
-
if (node.init.type === AST_NODE_TYPES.TSTypeAssertion && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
1934
|
-
return false;
|
|
1935
|
-
}
|
|
1936
|
-
/**
|
|
1937
|
-
* Checks if a method name exists in any of the implemented interfaces.
|
|
1938
|
-
*
|
|
1939
|
-
* @param implementsList - List of implemented interfaces.
|
|
1940
|
-
* @param methodName - Name of the method to check.
|
|
1941
|
-
* @param checker - TypeScript type checker.
|
|
1942
|
-
* @param services - Parser services.
|
|
1943
|
-
* @returns True if the method name is found in any interface.
|
|
1944
|
-
*/
|
|
1945
|
-
function checkInterfacesForMethod(implementsList, methodName, checker, services) {
|
|
1946
|
-
for (const implementsClause of implementsList) {
|
|
1947
|
-
const interfaceType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(implementsClause));
|
|
1948
|
-
if (!interfaceType.getSymbol()) continue;
|
|
1949
|
-
const interfaceMembers = checker.getPropertiesOfType(interfaceType);
|
|
1950
|
-
for (const member of interfaceMembers) if (member.name === methodName) return true;
|
|
1951
|
-
}
|
|
1952
|
-
return false;
|
|
1953
|
-
}
|
|
1954
2493
|
/**
|
|
1955
2494
|
* Determines if a class method is implementing an interface method.
|
|
1956
2495
|
*
|
|
@@ -1958,7 +2497,6 @@ function create$3(contextWithoutDefaults) {
|
|
|
1958
2497
|
* @returns True if the method is implementing an interface method.
|
|
1959
2498
|
*/
|
|
1960
2499
|
function isImplementingInterfaceMethod(node) {
|
|
1961
|
-
const services = getParserServices(context, true);
|
|
1962
2500
|
if (!services.program) return false;
|
|
1963
2501
|
const checker = services.program.getTypeChecker();
|
|
1964
2502
|
let parentClass = null;
|
|
@@ -1986,6 +2524,7 @@ function create$3(contextWithoutDefaults) {
|
|
|
1986
2524
|
},
|
|
1987
2525
|
":not(ObjectPattern) > Property[computed = false][kind = \"init\"][value.type != \"ArrowFunctionExpression\"][value.type != \"FunctionExpression\"][value.type != \"TSEmptyBodyFunctionExpression\"]": {
|
|
1988
2526
|
handler: (node, validator) => {
|
|
2527
|
+
if (isDictatedByContextualType(node, services, contextualTypeCache)) return;
|
|
1989
2528
|
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
1990
2529
|
},
|
|
1991
2530
|
validator: validators.objectLiteralProperty
|
|
@@ -2022,6 +2561,7 @@ function create$3(contextWithoutDefaults) {
|
|
|
2022
2561
|
"Property[computed = false][kind = \"init\"][value.type = \"TSEmptyBodyFunctionExpression\"]"
|
|
2023
2562
|
].join(", ")]: {
|
|
2024
2563
|
handler: (node, validator) => {
|
|
2564
|
+
if (node.type === AST_NODE_TYPES.Property && isDictatedByContextualType(node, services, contextualTypeCache)) return;
|
|
2025
2565
|
const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
|
|
2026
2566
|
if (isAsyncMemberOrProperty(node)) modifiers.add(Modifier.async);
|
|
2027
2567
|
handleMember(validator, node, modifiers);
|
|
@@ -2113,7 +2653,7 @@ function create$3(contextWithoutDefaults) {
|
|
|
2113
2653
|
"TSEnumMember": {
|
|
2114
2654
|
handler: ({ id }, validator) => {
|
|
2115
2655
|
const modifiers = /* @__PURE__ */ new Set();
|
|
2116
|
-
if (requiresQuoting
|
|
2656
|
+
if (requiresQuoting(id, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
2117
2657
|
validator(id, modifiers);
|
|
2118
2658
|
},
|
|
2119
2659
|
validator: validators.enumMember
|
|
@@ -2192,8 +2732,8 @@ function create$3(contextWithoutDefaults) {
|
|
|
2192
2732
|
}));
|
|
2193
2733
|
}
|
|
2194
2734
|
const namingConvention = createEslintRule({
|
|
2195
|
-
name: RULE_NAME$
|
|
2196
|
-
create: create$
|
|
2735
|
+
name: RULE_NAME$11,
|
|
2736
|
+
create: create$4,
|
|
2197
2737
|
defaultOptions: camelCaseNamingConfig,
|
|
2198
2738
|
meta: {
|
|
2199
2739
|
docs: {
|
|
@@ -2203,18 +2743,75 @@ const namingConvention = createEslintRule({
|
|
|
2203
2743
|
},
|
|
2204
2744
|
fixable: void 0,
|
|
2205
2745
|
hasSuggestions: false,
|
|
2206
|
-
messages: messages$
|
|
2746
|
+
messages: messages$11,
|
|
2207
2747
|
schema: SCHEMA,
|
|
2208
2748
|
type: "suggestion"
|
|
2209
2749
|
}
|
|
2210
2750
|
});
|
|
2751
|
+
/**
|
|
2752
|
+
* Checks if a method name exists in any of the implemented interfaces.
|
|
2753
|
+
*
|
|
2754
|
+
* @param implementsList - List of implemented interfaces.
|
|
2755
|
+
* @param methodName - Name of the method to check.
|
|
2756
|
+
* @param checker - TypeScript type checker.
|
|
2757
|
+
* @param services - Parser services.
|
|
2758
|
+
* @returns True if the method name is found in any interface.
|
|
2759
|
+
*/
|
|
2760
|
+
function checkInterfacesForMethod(implementsList, methodName, checker, services) {
|
|
2761
|
+
for (const implementsClause of implementsList) {
|
|
2762
|
+
const interfaceType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(implementsClause));
|
|
2763
|
+
if (!interfaceType.getSymbol()) continue;
|
|
2764
|
+
const interfaceMembers = checker.getPropertiesOfType(interfaceType);
|
|
2765
|
+
for (const member of interfaceMembers) if (member.name === methodName) return true;
|
|
2766
|
+
}
|
|
2767
|
+
return false;
|
|
2768
|
+
}
|
|
2211
2769
|
function getIdentifiersFromPattern(pattern) {
|
|
2212
2770
|
const identifiers = [];
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2771
|
+
const stack = [pattern];
|
|
2772
|
+
for (const node of stack) switch (node.type) {
|
|
2773
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
2774
|
+
for (const element of node.elements) if (element !== null) stack.push(element);
|
|
2775
|
+
break;
|
|
2776
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
2777
|
+
stack.push(node.left);
|
|
2778
|
+
break;
|
|
2779
|
+
case AST_NODE_TYPES.Identifier:
|
|
2780
|
+
identifiers.push(node);
|
|
2781
|
+
break;
|
|
2782
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
2783
|
+
for (const property of node.properties) stack.push(property);
|
|
2784
|
+
break;
|
|
2785
|
+
case AST_NODE_TYPES.Property:
|
|
2786
|
+
stack.push(node.value);
|
|
2787
|
+
break;
|
|
2788
|
+
case AST_NODE_TYPES.RestElement:
|
|
2789
|
+
stack.push(node.argument);
|
|
2790
|
+
break;
|
|
2791
|
+
default: break;
|
|
2792
|
+
}
|
|
2216
2793
|
return identifiers;
|
|
2217
2794
|
}
|
|
2795
|
+
function getMemberModifiers(node) {
|
|
2796
|
+
const modifiers = /* @__PURE__ */ new Set();
|
|
2797
|
+
if ("key" in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) modifiers.add(Modifier["#private"]);
|
|
2798
|
+
else if (node.accessibility) modifiers.add(Modifier[node.accessibility]);
|
|
2799
|
+
else modifiers.add(Modifier.public);
|
|
2800
|
+
if (node.static) modifiers.add(Modifier.static);
|
|
2801
|
+
if ("readonly" in node && node.readonly) modifiers.add(Modifier.readonly);
|
|
2802
|
+
if ("override" in node && node.override) modifiers.add(Modifier.override);
|
|
2803
|
+
if (node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition || node.type === AST_NODE_TYPES.TSAbstractAccessorProperty) modifiers.add(Modifier.abstract);
|
|
2804
|
+
return modifiers;
|
|
2805
|
+
}
|
|
2806
|
+
function isAsyncMemberOrProperty(propertyOrMemberNode) {
|
|
2807
|
+
return Boolean("value" in propertyOrMemberNode && propertyOrMemberNode.value && "async" in propertyOrMemberNode.value && propertyOrMemberNode.value.async);
|
|
2808
|
+
}
|
|
2809
|
+
function isAsyncVariableIdentifier(id) {
|
|
2810
|
+
return Boolean("async" in id.parent && id.parent.async || "init" in id.parent && id.parent.init && "async" in id.parent.init && id.parent.init.async);
|
|
2811
|
+
}
|
|
2812
|
+
function isDestructured(id) {
|
|
2813
|
+
return id.parent.type === AST_NODE_TYPES.Property && id.parent.shorthand || id.parent.type === AST_NODE_TYPES.AssignmentPattern && id.parent.parent.type === AST_NODE_TYPES.Property && id.parent.parent.shorthand;
|
|
2814
|
+
}
|
|
2218
2815
|
function isExported(node, name, scope) {
|
|
2219
2816
|
if (node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) return true;
|
|
2220
2817
|
if (scope === null) return false;
|
|
@@ -2229,14 +2826,45 @@ function isGlobal(scope) {
|
|
|
2229
2826
|
if (scope === null) return false;
|
|
2230
2827
|
return scope.type === TSESLint.Scope.ScopeType.global || scope.type === TSESLint.Scope.ScopeType.module;
|
|
2231
2828
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2829
|
+
/**
|
|
2830
|
+
* Determines if a VariableDeclarator represents an object-style enum declaration.
|
|
2831
|
+
*
|
|
2832
|
+
* Object-style enums are const assertions applied to object expressions, commonly
|
|
2833
|
+
* used as an alternative to TypeScript enums. Examples:
|
|
2834
|
+
* - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
|
|
2835
|
+
* - `const Status = <const>{ OK: 200, ERROR: 500 }`.
|
|
2836
|
+
*
|
|
2837
|
+
* @param node - The VariableDeclarator AST node to check.
|
|
2838
|
+
* @param parent - The parent VariableDeclaration node.
|
|
2839
|
+
* @returns True if this represents an object-style enum declaration.
|
|
2840
|
+
*/
|
|
2841
|
+
function isObjectStyleEnumDeclaration(node, parent) {
|
|
2842
|
+
if (parent.kind !== "const") return false;
|
|
2843
|
+
if (!node.init) return false;
|
|
2844
|
+
if (node.init.type === AST_NODE_TYPES.TSAsExpression && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
2845
|
+
if (node.init.type === AST_NODE_TYPES.TSTypeAssertion && node.init.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.init.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && node.init.typeAnnotation.typeName.name === "const" && node.init.expression.type === AST_NODE_TYPES.ObjectExpression) return true;
|
|
2846
|
+
return false;
|
|
2847
|
+
}
|
|
2848
|
+
function isValidIdentifierText(name, languageVersion) {
|
|
2849
|
+
if (name.length === 0) return false;
|
|
2850
|
+
const firstCodePoint = name.codePointAt(0);
|
|
2851
|
+
if (firstCodePoint === void 0 || !isIdentifierStart(firstCodePoint, languageVersion)) return false;
|
|
2852
|
+
let index = firstCodePoint > 65535 ? 2 : 1;
|
|
2853
|
+
while (index < name.length) {
|
|
2854
|
+
const codePoint = name.codePointAt(index);
|
|
2855
|
+
if (codePoint === void 0 || !isIdentifierPart(codePoint, languageVersion)) return false;
|
|
2856
|
+
index += codePoint > 65535 ? 2 : 1;
|
|
2857
|
+
}
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2860
|
+
function requiresQuoting(node, target) {
|
|
2861
|
+
return !isValidIdentifierText(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target ?? ScriptTarget.Latest);
|
|
2234
2862
|
}
|
|
2235
2863
|
//#endregion
|
|
2236
2864
|
//#region src/rules/no-export-default-arrow/rule.ts
|
|
2237
|
-
const RULE_NAME$
|
|
2238
|
-
const MESSAGE_ID$
|
|
2239
|
-
const messages$
|
|
2865
|
+
const RULE_NAME$10 = "no-export-default-arrow";
|
|
2866
|
+
const MESSAGE_ID$6 = "disallowExportDefaultArrow";
|
|
2867
|
+
const messages$10 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
|
|
2240
2868
|
/**
|
|
2241
2869
|
* Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
|
|
2242
2870
|
* word separators (`use-mouse` -> `useMouse`).
|
|
@@ -2314,7 +2942,7 @@ function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCo
|
|
|
2314
2942
|
* @param context - The rule context.
|
|
2315
2943
|
* @returns The rule listener.
|
|
2316
2944
|
*/
|
|
2317
|
-
function createOnce$
|
|
2945
|
+
function createOnce$6(context) {
|
|
2318
2946
|
return { ArrowFunctionExpression(node) {
|
|
2319
2947
|
const { parent } = node;
|
|
2320
2948
|
if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
|
|
@@ -2325,14 +2953,14 @@ function createOnce$5(context) {
|
|
|
2325
2953
|
exportDeclaration: parent,
|
|
2326
2954
|
sourceCode: context.sourceCode
|
|
2327
2955
|
}),
|
|
2328
|
-
messageId: MESSAGE_ID$
|
|
2956
|
+
messageId: MESSAGE_ID$6,
|
|
2329
2957
|
node
|
|
2330
2958
|
});
|
|
2331
2959
|
} };
|
|
2332
2960
|
}
|
|
2333
2961
|
const noExportDefaultArrow = createFlawlessRule({
|
|
2334
|
-
name: RULE_NAME$
|
|
2335
|
-
createOnce: createOnce$
|
|
2962
|
+
name: RULE_NAME$10,
|
|
2963
|
+
createOnce: createOnce$6,
|
|
2336
2964
|
defaultOptions: [],
|
|
2337
2965
|
meta: {
|
|
2338
2966
|
docs: {
|
|
@@ -2342,7 +2970,391 @@ const noExportDefaultArrow = createFlawlessRule({
|
|
|
2342
2970
|
},
|
|
2343
2971
|
fixable: "code",
|
|
2344
2972
|
hasSuggestions: false,
|
|
2345
|
-
messages: messages$
|
|
2973
|
+
messages: messages$10,
|
|
2974
|
+
schema: [],
|
|
2975
|
+
type: "suggestion"
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
//#endregion
|
|
2979
|
+
//#region src/rules/no-redundant-tsconfig-options/resolve-extends.ts
|
|
2980
|
+
const TOP_LEVEL_KEYS = [
|
|
2981
|
+
"include",
|
|
2982
|
+
"exclude",
|
|
2983
|
+
"files"
|
|
2984
|
+
];
|
|
2985
|
+
function isRecord(value) {
|
|
2986
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Resolves one `extends` specifier to an absolute config path, or `undefined`
|
|
2990
|
+
* when it cannot be found. Path specifiers resolve against the extending file's
|
|
2991
|
+
* directory (appending `.json` or `/tsconfig.json` as TypeScript does); package
|
|
2992
|
+
* specifiers go through `require.resolve`, which honours the package `exports`
|
|
2993
|
+
* map (so `@scope/pkg/subpath` maps correctly).
|
|
2994
|
+
*
|
|
2995
|
+
* @param spec - The raw `extends` specifier.
|
|
2996
|
+
* @param fromFile - The absolute path of the config doing the extending.
|
|
2997
|
+
* @returns The resolved absolute path, or `undefined`.
|
|
2998
|
+
*/
|
|
2999
|
+
function resolveExtendsTarget(spec, fromFile) {
|
|
3000
|
+
if (isPathSpecifier(spec)) {
|
|
3001
|
+
const base = path.resolve(path.dirname(fromFile), spec);
|
|
3002
|
+
return (base.endsWith(".json") ? [base] : [`${base}.json`, path.join(base, "tsconfig.json")]).find((candidate) => fileExists(candidate));
|
|
3003
|
+
}
|
|
3004
|
+
const require = createRequire(fromFile);
|
|
3005
|
+
for (const candidate of [
|
|
3006
|
+
spec,
|
|
3007
|
+
`${spec}.json`,
|
|
3008
|
+
`${spec}/tsconfig.json`
|
|
3009
|
+
]) try {
|
|
3010
|
+
return require.resolve(candidate);
|
|
3011
|
+
} catch {}
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Builds the config a child inherits from its `extends` targets alone (the child
|
|
3015
|
+
* itself excluded). Later targets in an `extends` array override earlier ones,
|
|
3016
|
+
* matching TypeScript's precedence.
|
|
3017
|
+
*
|
|
3018
|
+
* @param childFile - The absolute path of the config being linted.
|
|
3019
|
+
* @param extendsField - The child's raw `extends` value (string or array).
|
|
3020
|
+
* @returns The inherited config, or `undefined` when nothing resolves.
|
|
3021
|
+
*/
|
|
3022
|
+
function buildInheritedConfig(childFile, extendsField) {
|
|
3023
|
+
const context = {
|
|
3024
|
+
cache: /* @__PURE__ */ new Map(),
|
|
3025
|
+
stack: /* @__PURE__ */ new Set([childFile])
|
|
3026
|
+
};
|
|
3027
|
+
const result = emptyConfig();
|
|
3028
|
+
let resolvedAny = false;
|
|
3029
|
+
for (const spec of normalizeExtends(extendsField)) {
|
|
3030
|
+
const target = resolveExtendsTarget(spec, childFile);
|
|
3031
|
+
if (target === void 0) continue;
|
|
3032
|
+
resolvedAny = true;
|
|
3033
|
+
mergeParent(result, flatten(target, context));
|
|
3034
|
+
}
|
|
3035
|
+
return resolvedAny ? result : void 0;
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* Whether an `extends` specifier is a filesystem path (relative or absolute)
|
|
3039
|
+
* rather than a package specifier. Matches TypeScript: a leading `.` or a rooted
|
|
3040
|
+
* path is a path; anything else resolves through node module resolution.
|
|
3041
|
+
*
|
|
3042
|
+
* @param spec - The raw `extends` specifier.
|
|
3043
|
+
* @returns True when the specifier should resolve against the filesystem.
|
|
3044
|
+
*/
|
|
3045
|
+
function isPathSpecifier(spec) {
|
|
3046
|
+
return spec.startsWith(".") || path.isAbsolute(spec);
|
|
3047
|
+
}
|
|
3048
|
+
function fileExists(candidate) {
|
|
3049
|
+
try {
|
|
3050
|
+
return statSync(candidate).isFile();
|
|
3051
|
+
} catch {
|
|
3052
|
+
return false;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
function normalizeExtends(value) {
|
|
3056
|
+
if (typeof value === "string") return [value];
|
|
3057
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string");
|
|
3058
|
+
return [];
|
|
3059
|
+
}
|
|
3060
|
+
function emptyConfig() {
|
|
3061
|
+
return {
|
|
3062
|
+
compilerOptions: /* @__PURE__ */ new Map(),
|
|
3063
|
+
topLevel: /* @__PURE__ */ new Map()
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function mergeInto(target, source) {
|
|
3067
|
+
for (const [key, entry] of source) target.set(key, entry);
|
|
3068
|
+
}
|
|
3069
|
+
function mergeParent(into, parent) {
|
|
3070
|
+
mergeInto(into.compilerOptions, parent.compilerOptions);
|
|
3071
|
+
mergeInto(into.topLevel, parent.topLevel);
|
|
3072
|
+
}
|
|
3073
|
+
function parseTsconfig(file) {
|
|
3074
|
+
try {
|
|
3075
|
+
const { ast } = parseForESLint(readFileSync(file, "utf8"), {});
|
|
3076
|
+
const statement = ast.body.at(0);
|
|
3077
|
+
if (statement?.expression.type !== "JSONObjectExpression") return;
|
|
3078
|
+
const value = getStaticJSONValue(statement.expression);
|
|
3079
|
+
return isRecord(value) ? value : void 0;
|
|
3080
|
+
} catch {
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Reads and parses a parent tsconfig from disk. Returns `undefined` for any
|
|
3086
|
+
* failure — missing file, unreadable, invalid JSONC, or a top level that is not
|
|
3087
|
+
* an object — so a broken ancestor never crashes the lint.
|
|
3088
|
+
*
|
|
3089
|
+
* @param file - The absolute path of the config to read.
|
|
3090
|
+
* @param cache - Per-resolution parse cache, keyed by absolute path.
|
|
3091
|
+
* @returns The parsed config subset, or `undefined`.
|
|
3092
|
+
*/
|
|
3093
|
+
function readTsconfig(file, cache) {
|
|
3094
|
+
if (cache.has(file)) return cache.get(file);
|
|
3095
|
+
const parsed = parseTsconfig(file);
|
|
3096
|
+
cache.set(file, parsed);
|
|
3097
|
+
return parsed;
|
|
3098
|
+
}
|
|
3099
|
+
function overlay(target, options, source) {
|
|
3100
|
+
if (!isRecord(options)) return;
|
|
3101
|
+
for (const [key, value] of Object.entries(options)) target.set(key, {
|
|
3102
|
+
source,
|
|
3103
|
+
value
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Flattens the effective config a file resolves to — its own extends chain,
|
|
3108
|
+
* overlaid by its own values (a file's own options win over what it extends).
|
|
3109
|
+
* The `visited` set guards against circular `extends`.
|
|
3110
|
+
*
|
|
3111
|
+
* @param file - The absolute path of the config to flatten.
|
|
3112
|
+
* @param context - The shared cycle stack and parse cache.
|
|
3113
|
+
* @returns The flattened config, with each entry's `source` naming its definer.
|
|
3114
|
+
*/
|
|
3115
|
+
function flatten(file, context) {
|
|
3116
|
+
const result = emptyConfig();
|
|
3117
|
+
if (context.stack.has(file)) return result;
|
|
3118
|
+
context.stack.add(file);
|
|
3119
|
+
try {
|
|
3120
|
+
const config = readTsconfig(file, context.cache);
|
|
3121
|
+
if (config === void 0) return result;
|
|
3122
|
+
for (const spec of normalizeExtends(config.extends)) {
|
|
3123
|
+
const target = resolveExtendsTarget(spec, file);
|
|
3124
|
+
if (target !== void 0) mergeParent(result, flatten(target, context));
|
|
3125
|
+
}
|
|
3126
|
+
overlay(result.compilerOptions, config.compilerOptions, file);
|
|
3127
|
+
for (const key of TOP_LEVEL_KEYS) if (config[key] !== void 0) result.topLevel.set(key, {
|
|
3128
|
+
source: file,
|
|
3129
|
+
value: config[key]
|
|
3130
|
+
});
|
|
3131
|
+
return result;
|
|
3132
|
+
} finally {
|
|
3133
|
+
context.stack.delete(file);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
//#endregion
|
|
3137
|
+
//#region src/rules/no-redundant-tsconfig-options/rule.ts
|
|
3138
|
+
const RULE_NAME$9 = "no-redundant-tsconfig-options";
|
|
3139
|
+
const MESSAGE_ID$5 = "redundant";
|
|
3140
|
+
/**
|
|
3141
|
+
* Compiler options TypeScript resolves case-insensitively; a child that re-sets
|
|
3142
|
+
* one differing only in case is still redundant.
|
|
3143
|
+
*/
|
|
3144
|
+
const ENUM_SCALAR_KEYS = /* @__PURE__ */ new Set([
|
|
3145
|
+
"jsx",
|
|
3146
|
+
"module",
|
|
3147
|
+
"moduleDetection",
|
|
3148
|
+
"moduleResolution",
|
|
3149
|
+
"newLine",
|
|
3150
|
+
"target"
|
|
3151
|
+
]);
|
|
3152
|
+
/**
|
|
3153
|
+
* Array-valued options TypeScript treats as unordered sets, so a reordered but
|
|
3154
|
+
* otherwise identical value is still redundant.
|
|
3155
|
+
*/
|
|
3156
|
+
const SET_KEYS = /* @__PURE__ */ new Set(["lib", "types"]);
|
|
3157
|
+
/**
|
|
3158
|
+
* `compilerOptions` keys whose values are resolved as paths relative to the
|
|
3159
|
+
* config that declares them. An identical value repeated in a child in a
|
|
3160
|
+
* different directory means a different path, so these are only redundant when
|
|
3161
|
+
* the value is location-independent (see {@link isLocationIndependent}).
|
|
3162
|
+
*/
|
|
3163
|
+
const PATH_KEYS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"baseUrl",
|
|
3165
|
+
"declarationDir",
|
|
3166
|
+
"outDir",
|
|
3167
|
+
"outFile",
|
|
3168
|
+
"paths",
|
|
3169
|
+
"rootDir",
|
|
3170
|
+
"rootDirs",
|
|
3171
|
+
"tsBuildInfoFile",
|
|
3172
|
+
"typeRoots"
|
|
3173
|
+
]);
|
|
3174
|
+
const messages$9 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
|
|
3175
|
+
/**
|
|
3176
|
+
* Structural equality between two static JSON values. Strings compare
|
|
3177
|
+
* case-insensitively when `caseInsensitive` is set (used for enum-valued
|
|
3178
|
+
* options and `lib` entries, which TypeScript itself folds).
|
|
3179
|
+
*
|
|
3180
|
+
* @param a - The child's value.
|
|
3181
|
+
* @param b - The inherited value.
|
|
3182
|
+
* @param caseInsensitive - Whether string scalars compare case-insensitively.
|
|
3183
|
+
* @returns Whether the two values are equal.
|
|
3184
|
+
*/
|
|
3185
|
+
function equalValues(a, b, caseInsensitive) {
|
|
3186
|
+
if (typeof a === "string" && typeof b === "string") return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
3187
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => equalValues(item, b[index], caseInsensitive));
|
|
3188
|
+
if (isRecord(a) && isRecord(b)) {
|
|
3189
|
+
const aKeys = Object.keys(a);
|
|
3190
|
+
const bKeys = Object.keys(b);
|
|
3191
|
+
return aKeys.length === bKeys.length && aKeys.every((key) => key in b && equalValues(a[key], b[key], caseInsensitive));
|
|
3192
|
+
}
|
|
3193
|
+
return a === b;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Set equality for two arrays: same length and every element of one has a
|
|
3197
|
+
* distinct equal counterpart in the other, ignoring order.
|
|
3198
|
+
*
|
|
3199
|
+
* @param a - The child's array.
|
|
3200
|
+
* @param b - The inherited array.
|
|
3201
|
+
* @param caseInsensitive - Whether string elements compare case-insensitively.
|
|
3202
|
+
* @returns Whether the arrays hold the same elements regardless of order.
|
|
3203
|
+
*/
|
|
3204
|
+
function equalUnordered(a, b, caseInsensitive) {
|
|
3205
|
+
if (a.length !== b.length) return false;
|
|
3206
|
+
const remaining = [...b];
|
|
3207
|
+
for (const item of a) {
|
|
3208
|
+
const index = remaining.findIndex((other) => equalValues(item, other, caseInsensitive));
|
|
3209
|
+
if (index === -1) return false;
|
|
3210
|
+
remaining.splice(index, 1);
|
|
3211
|
+
}
|
|
3212
|
+
return true;
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Whether a path-valued option is location-independent — every path string it
|
|
3216
|
+
* contains is either `${configDir}`-anchored (re-anchored to the extending
|
|
3217
|
+
* config, so an identical value resolves to the same files) or absolute. Only
|
|
3218
|
+
* then is repeating a path option genuinely redundant.
|
|
3219
|
+
*
|
|
3220
|
+
* @param value - The option value.
|
|
3221
|
+
* @returns Whether the value resolves the same regardless of config location.
|
|
3222
|
+
*/
|
|
3223
|
+
function isLocationIndependent(value) {
|
|
3224
|
+
if (typeof value === "string") return value.includes("${configDir}") || path.isAbsolute(value);
|
|
3225
|
+
if (Array.isArray(value)) return value.every((item) => isLocationIndependent(item));
|
|
3226
|
+
if (isRecord(value)) return Object.values(value).every((item) => isLocationIndependent(item));
|
|
3227
|
+
return true;
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Whether a child option re-setting `inherited` to `childValue` is redundant:
|
|
3231
|
+
* the values are equal and, for path-valued options, location-independent.
|
|
3232
|
+
*
|
|
3233
|
+
* @param key - The option name.
|
|
3234
|
+
* @param childValue - The value the child sets.
|
|
3235
|
+
* @param inherited - The inherited value.
|
|
3236
|
+
* @param isPathKey - Whether the key is path-valued.
|
|
3237
|
+
* @returns Whether the child option is redundant.
|
|
3238
|
+
*/
|
|
3239
|
+
function isRedundant(key, childValue, inherited, isPathKey) {
|
|
3240
|
+
const caseInsensitive = key === "lib" || ENUM_SCALAR_KEYS.has(key);
|
|
3241
|
+
if (!(SET_KEYS.has(key) && Array.isArray(childValue) && Array.isArray(inherited) ? equalUnordered(childValue, inherited, caseInsensitive) : equalValues(childValue, inherited, caseInsensitive))) return false;
|
|
3242
|
+
return !isPathKey || isLocationIndependent(childValue);
|
|
3243
|
+
}
|
|
3244
|
+
function keyName({ key }) {
|
|
3245
|
+
return key.type === "JSONIdentifier" ? key.name : String(getStaticJSONValue(key));
|
|
3246
|
+
}
|
|
3247
|
+
function findProperty(object, name) {
|
|
3248
|
+
return object.properties.find((property) => keyName(property) === name);
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Reads a node's static value, returning `undefined` when it cannot be evaluated
|
|
3252
|
+
* (such as an exotic non-JSON node), so an odd value never crashes the rule. The
|
|
3253
|
+
* result is boxed so a genuine `undefined` value stays distinguishable.
|
|
3254
|
+
*
|
|
3255
|
+
* @param node - The value node to evaluate.
|
|
3256
|
+
* @returns A box holding the value, or `undefined` on failure.
|
|
3257
|
+
*/
|
|
3258
|
+
function safeStaticValue(node) {
|
|
3259
|
+
try {
|
|
3260
|
+
return { value: getStaticJSONValue(node) };
|
|
3261
|
+
} catch {
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function create$3(context) {
|
|
3266
|
+
const { sourceCode } = context;
|
|
3267
|
+
if (sourceCode.parserServices.isJSON !== true) return {};
|
|
3268
|
+
const { filename } = context;
|
|
3269
|
+
if (!path.isAbsolute(filename)) return {};
|
|
3270
|
+
/**
|
|
3271
|
+
* Reports a redundant property, removing it (with its delimiter comma) unless
|
|
3272
|
+
* a comment is attached, in which case it reports without a fix to avoid
|
|
3273
|
+
* stranding the comment.
|
|
3274
|
+
*
|
|
3275
|
+
* @param property - The redundant property node.
|
|
3276
|
+
* @param name - The property's key name (already computed by the caller).
|
|
3277
|
+
* @param entry - The inherited value and the config that defined it.
|
|
3278
|
+
*/
|
|
3279
|
+
function report(property, name, entry) {
|
|
3280
|
+
const relative = path.relative(path.dirname(filename), entry.source) || entry.source;
|
|
3281
|
+
context.report({
|
|
3282
|
+
data: {
|
|
3283
|
+
option: name,
|
|
3284
|
+
source: relative.split(path.sep).join("/")
|
|
3285
|
+
},
|
|
3286
|
+
fix: buildFix(property),
|
|
3287
|
+
loc: property.key.loc,
|
|
3288
|
+
messageId: MESSAGE_ID$5
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
function buildFix(property) {
|
|
3292
|
+
const node = property;
|
|
3293
|
+
const before = sourceCode.getTokenBefore(node);
|
|
3294
|
+
const after = sourceCode.getTokenAfter(node);
|
|
3295
|
+
const leading = sourceCode.getCommentsBefore(node);
|
|
3296
|
+
const between = sourceCode.getCommentsAfter(node);
|
|
3297
|
+
const trailing = after?.value === "," ? sourceCode.getCommentsAfter(after).filter((comment) => comment.loc.start.line === after.loc.end.line) : [];
|
|
3298
|
+
if (leading.length > 0 || between.length > 0 || trailing.length > 0) return;
|
|
3299
|
+
let start;
|
|
3300
|
+
let end;
|
|
3301
|
+
if (after?.value === ",") {
|
|
3302
|
+
start = before?.range[1] ?? property.range[0];
|
|
3303
|
+
end = after.range[1];
|
|
3304
|
+
} else if (before?.value === ",") {
|
|
3305
|
+
start = before.range[0];
|
|
3306
|
+
end = property.range[1];
|
|
3307
|
+
} else {
|
|
3308
|
+
start = before?.range[1] ?? property.range[0];
|
|
3309
|
+
end = property.range[1];
|
|
3310
|
+
}
|
|
3311
|
+
return (fixer) => fixer.removeRange([start, end]);
|
|
3312
|
+
}
|
|
3313
|
+
function checkObject(object, lookup, isPathKey) {
|
|
3314
|
+
for (const property of object.properties) {
|
|
3315
|
+
const name = keyName(property);
|
|
3316
|
+
const entry = lookup(name);
|
|
3317
|
+
if (entry === void 0) continue;
|
|
3318
|
+
const childValue = safeStaticValue(property.value);
|
|
3319
|
+
if (childValue !== void 0 && isRedundant(name, childValue.value, entry.value, isPathKey(name))) report(property, name, entry);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return { Program() {
|
|
3323
|
+
const statement = sourceCode.ast.body.at(0);
|
|
3324
|
+
if (statement?.expression.type !== "JSONObjectExpression") return;
|
|
3325
|
+
const root = statement.expression;
|
|
3326
|
+
const extendsProperty = findProperty(root, "extends");
|
|
3327
|
+
if (extendsProperty === void 0) return;
|
|
3328
|
+
const extendsValue = safeStaticValue(extendsProperty.value);
|
|
3329
|
+
if (extendsValue === void 0) return;
|
|
3330
|
+
const inherited = buildInheritedConfig(filename, extendsValue.value);
|
|
3331
|
+
if (inherited === void 0) return;
|
|
3332
|
+
const compilerOptions = findProperty(root, "compilerOptions");
|
|
3333
|
+
if (compilerOptions?.value.type === "JSONObjectExpression") checkObject(compilerOptions.value, (name) => {
|
|
3334
|
+
return inherited.compilerOptions.get(name);
|
|
3335
|
+
}, (name) => {
|
|
3336
|
+
return PATH_KEYS.has(name);
|
|
3337
|
+
});
|
|
3338
|
+
checkObject(root, (name) => {
|
|
3339
|
+
return inherited.topLevel.get(name);
|
|
3340
|
+
}, () => {
|
|
3341
|
+
return true;
|
|
3342
|
+
});
|
|
3343
|
+
} };
|
|
3344
|
+
}
|
|
3345
|
+
const noRedundantTsconfigOptions = createEslintRule({
|
|
3346
|
+
name: RULE_NAME$9,
|
|
3347
|
+
create: create$3,
|
|
3348
|
+
defaultOptions: [],
|
|
3349
|
+
meta: {
|
|
3350
|
+
docs: {
|
|
3351
|
+
description: "Disallow tsconfig options that redundantly re-set a value already provided by an extended config",
|
|
3352
|
+
recommended: false,
|
|
3353
|
+
requiresTypeChecking: false
|
|
3354
|
+
},
|
|
3355
|
+
fixable: "code",
|
|
3356
|
+
hasSuggestions: false,
|
|
3357
|
+
messages: messages$9,
|
|
2346
3358
|
schema: [],
|
|
2347
3359
|
type: "suggestion"
|
|
2348
3360
|
}
|
|
@@ -2585,15 +3597,15 @@ function resolveFactory(sourceCode, node) {
|
|
|
2585
3597
|
}
|
|
2586
3598
|
//#endregion
|
|
2587
3599
|
//#region src/rules/no-unnecessary-use-callback/rule.ts
|
|
2588
|
-
const RULE_NAME$
|
|
3600
|
+
const RULE_NAME$8 = "no-unnecessary-use-callback";
|
|
2589
3601
|
const MESSAGE_ID_DEFAULT$1 = "default";
|
|
2590
3602
|
const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
|
|
2591
|
-
const messages$
|
|
3603
|
+
const messages$8 = {
|
|
2592
3604
|
[MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
|
|
2593
3605
|
[MESSAGE_ID_INSIDE_USE_EFFECT$1]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
2594
3606
|
};
|
|
2595
3607
|
const noUnnecessaryUseCallback = createFlawlessRule({
|
|
2596
|
-
name: RULE_NAME$
|
|
3608
|
+
name: RULE_NAME$8,
|
|
2597
3609
|
createOnce: createUnnecessaryHookRule({
|
|
2598
3610
|
hook: "useCallback",
|
|
2599
3611
|
messageIds: {
|
|
@@ -2609,22 +3621,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
|
|
|
2609
3621
|
requiresTypeChecking: false
|
|
2610
3622
|
},
|
|
2611
3623
|
hasSuggestions: false,
|
|
2612
|
-
messages: messages$
|
|
3624
|
+
messages: messages$8,
|
|
2613
3625
|
schema: [],
|
|
2614
3626
|
type: "suggestion"
|
|
2615
3627
|
}
|
|
2616
3628
|
});
|
|
2617
3629
|
//#endregion
|
|
2618
3630
|
//#region src/rules/no-unnecessary-use-memo/rule.ts
|
|
2619
|
-
const RULE_NAME$
|
|
3631
|
+
const RULE_NAME$7 = "no-unnecessary-use-memo";
|
|
2620
3632
|
const MESSAGE_ID_DEFAULT = "default";
|
|
2621
3633
|
const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
|
|
2622
|
-
const messages$
|
|
3634
|
+
const messages$7 = {
|
|
2623
3635
|
[MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
|
|
2624
3636
|
[MESSAGE_ID_INSIDE_USE_EFFECT]: "'{{name}}' is only used inside 1 useEffect, which may be unnecessary. You can move the computation into useEffect directly and merge the dependency arrays."
|
|
2625
3637
|
};
|
|
2626
3638
|
const noUnnecessaryUseMemo = createFlawlessRule({
|
|
2627
|
-
name: RULE_NAME$
|
|
3639
|
+
name: RULE_NAME$7,
|
|
2628
3640
|
createOnce: createUnnecessaryHookRule({
|
|
2629
3641
|
hook: "useMemo",
|
|
2630
3642
|
messageIds: {
|
|
@@ -2640,12 +3652,132 @@ const noUnnecessaryUseMemo = createFlawlessRule({
|
|
|
2640
3652
|
requiresTypeChecking: false
|
|
2641
3653
|
},
|
|
2642
3654
|
hasSuggestions: false,
|
|
2643
|
-
messages: messages$
|
|
3655
|
+
messages: messages$7,
|
|
2644
3656
|
schema: [],
|
|
2645
3657
|
type: "suggestion"
|
|
2646
3658
|
}
|
|
2647
3659
|
});
|
|
2648
3660
|
//#endregion
|
|
3661
|
+
//#region src/rules/padding-after-expect-assertions/rule.ts
|
|
3662
|
+
const RULE_NAME$6 = "padding-after-expect-assertions";
|
|
3663
|
+
const MESSAGE_ID$4 = "missingPadding";
|
|
3664
|
+
const messages$6 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
|
|
3665
|
+
/**
|
|
3666
|
+
* Matches a statement that declares the expected assertion count at the top of
|
|
3667
|
+
* a test: `expect.assertions(n)` or `expect.hasAssertions()`.
|
|
3668
|
+
*
|
|
3669
|
+
* Detection is purely syntactic (a non-computed `expect.assertions` /
|
|
3670
|
+
* `expect.hasAssertions` call) with no scope analysis, mirroring the deliberate
|
|
3671
|
+
* looseness of `@vitest/eslint-plugin`'s own padding rules. A shadowed local
|
|
3672
|
+
* `expect` is vanishingly rare, and the only consequence is a harmless blank
|
|
3673
|
+
* line.
|
|
3674
|
+
*
|
|
3675
|
+
* @param node - The expression statement to inspect.
|
|
3676
|
+
* @returns The matched member name (`expect.assertions` / `expect.hasAssertions`)
|
|
3677
|
+
* for the message, or `undefined` when the statement is not an assertion count.
|
|
3678
|
+
*/
|
|
3679
|
+
function getAssertionName({ expression }) {
|
|
3680
|
+
if (expression.type !== AST_NODE_TYPES.CallExpression) return;
|
|
3681
|
+
const { callee } = expression;
|
|
3682
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== AST_NODE_TYPES.Identifier || callee.object.name !== "expect" || callee.property.type !== AST_NODE_TYPES.Identifier) return;
|
|
3683
|
+
const { name } = callee.property;
|
|
3684
|
+
if (name !== "assertions" && name !== "hasAssertions") return;
|
|
3685
|
+
return `expect.${name}`;
|
|
3686
|
+
}
|
|
3687
|
+
/**
|
|
3688
|
+
* Returns the statement list that directly contains a node, so its following
|
|
3689
|
+
* sibling can be found. The assertion count opens an `it`/`test` callback body
|
|
3690
|
+
* (a `BlockStatement`) or, at worst, sits at the top level (`Program`).
|
|
3691
|
+
*
|
|
3692
|
+
* @param node - The node whose containing list is wanted.
|
|
3693
|
+
* @returns The sibling statements, or `undefined` when the parent holds no list.
|
|
3694
|
+
*/
|
|
3695
|
+
function getContainingList({ parent }) {
|
|
3696
|
+
if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) return parent.body;
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Locates the two tokens the padding is measured and inserted between: the last
|
|
3700
|
+
* token of the assertion statement (advanced past any trailing comment on the
|
|
3701
|
+
* same line) and the first token of the following statement (a leading comment
|
|
3702
|
+
* included). This mirrors ESLint core's `padding-line-between-statements`, so a
|
|
3703
|
+
* trailing `// note` does not defeat the rule and the fix lands in the right
|
|
3704
|
+
* place. Note that `yaml-block-key-blank-lines` deliberately takes the opposite
|
|
3705
|
+
* stance and bails on comments, since rewriting there would re-attach them.
|
|
3706
|
+
*
|
|
3707
|
+
* @param sourceCode - The source code, for token lookups.
|
|
3708
|
+
* @param node - The assertion statement.
|
|
3709
|
+
* @param nextNode - The statement that follows it.
|
|
3710
|
+
* @returns The anchor tokens, or `undefined` when either cannot be resolved.
|
|
3711
|
+
*/
|
|
3712
|
+
function findPaddingAnchor(sourceCode, node, nextNode) {
|
|
3713
|
+
const lastToken = sourceCode.getLastToken(node);
|
|
3714
|
+
if (lastToken === null) return;
|
|
3715
|
+
let previousToken = lastToken;
|
|
3716
|
+
const nextToken = sourceCode.getFirstTokenBetween(previousToken, nextNode, {
|
|
3717
|
+
filter(token) {
|
|
3718
|
+
if (ASTUtils.isTokenOnSameLine(previousToken, token)) {
|
|
3719
|
+
previousToken = token;
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
return true;
|
|
3723
|
+
},
|
|
3724
|
+
includeComments: true
|
|
3725
|
+
}) ?? sourceCode.getFirstToken(nextNode);
|
|
3726
|
+
if (nextToken === null) return;
|
|
3727
|
+
return {
|
|
3728
|
+
nextToken,
|
|
3729
|
+
previousToken
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* Requires a blank line after the assertion count that opens a test, keeping the
|
|
3734
|
+
* bookkeeping visually separate from the expectations that follow.
|
|
3735
|
+
*
|
|
3736
|
+
* @param context - The rule context.
|
|
3737
|
+
* @returns The rule listener.
|
|
3738
|
+
*/
|
|
3739
|
+
function createOnce$3(context) {
|
|
3740
|
+
return { ExpressionStatement(node) {
|
|
3741
|
+
const name = getAssertionName(node);
|
|
3742
|
+
if (name === void 0) return;
|
|
3743
|
+
const list = getContainingList(node);
|
|
3744
|
+
if (list === void 0) return;
|
|
3745
|
+
const nextNode = list[list.indexOf(node) + 1];
|
|
3746
|
+
if (nextNode === void 0) return;
|
|
3747
|
+
const { sourceCode } = context;
|
|
3748
|
+
const anchor = findPaddingAnchor(sourceCode, node, nextNode);
|
|
3749
|
+
if (anchor === void 0) return;
|
|
3750
|
+
const { nextToken, previousToken } = anchor;
|
|
3751
|
+
const gap = nextToken.loc.start.line - previousToken.loc.end.line;
|
|
3752
|
+
if (gap > 1) return;
|
|
3753
|
+
context.report({
|
|
3754
|
+
data: { name },
|
|
3755
|
+
fix(fixer) {
|
|
3756
|
+
return fixer.insertTextAfter(previousToken, gap === 0 ? "\n\n" : "\n");
|
|
3757
|
+
},
|
|
3758
|
+
loc: node.loc,
|
|
3759
|
+
messageId: MESSAGE_ID$4
|
|
3760
|
+
});
|
|
3761
|
+
} };
|
|
3762
|
+
}
|
|
3763
|
+
const paddingAfterExpectAssertions = createFlawlessRule({
|
|
3764
|
+
name: RULE_NAME$6,
|
|
3765
|
+
createOnce: createOnce$3,
|
|
3766
|
+
defaultOptions: [],
|
|
3767
|
+
meta: {
|
|
3768
|
+
docs: {
|
|
3769
|
+
description: "Enforce a blank line after `expect.assertions` and `expect.hasAssertions`",
|
|
3770
|
+
recommended: false,
|
|
3771
|
+
requiresTypeChecking: false
|
|
3772
|
+
},
|
|
3773
|
+
fixable: "whitespace",
|
|
3774
|
+
hasSuggestions: false,
|
|
3775
|
+
messages: messages$6,
|
|
3776
|
+
schema: [],
|
|
3777
|
+
type: "layout"
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
//#endregion
|
|
2649
3781
|
//#region src/rules/prefer-destructuring-assignment/rule.ts
|
|
2650
3782
|
const RULE_NAME$5 = "prefer-destructuring-assignment";
|
|
2651
3783
|
const MESSAGE_ID$3 = "default";
|
|
@@ -4209,10 +5341,13 @@ const plugin = {
|
|
|
4209
5341
|
"arrow-return-style": arrowReturnStyle,
|
|
4210
5342
|
"jsx-shorthand-boolean": jsxShorthandBoolean,
|
|
4211
5343
|
"jsx-shorthand-fragment": jsxShorthandFragment,
|
|
5344
|
+
"max-lines-per-function": maxLinesPerFunction,
|
|
4212
5345
|
"naming-convention": namingConvention,
|
|
4213
5346
|
"no-export-default-arrow": noExportDefaultArrow,
|
|
5347
|
+
"no-redundant-tsconfig-options": noRedundantTsconfigOptions,
|
|
4214
5348
|
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
|
|
4215
5349
|
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
|
|
5350
|
+
"padding-after-expect-assertions": paddingAfterExpectAssertions,
|
|
4216
5351
|
"prefer-destructuring-assignment": preferDestructuringAssignment,
|
|
4217
5352
|
"prefer-parameter-destructuring": preferParameterDestructuring,
|
|
4218
5353
|
"prefer-read-only-props": preferReadOnlyProps,
|