eslint-plugin-flawless 0.1.12 → 1.0.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.
@@ -1,19 +1,21 @@
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";
8
+ import { findVariable, isArrowToken, isOpeningParenToken } from "@typescript-eslint/utils/ast-utils";
7
9
  import { DefinitionType, ImplicitLibVariable, PatternVisitor, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
8
10
  import { requiresQuoting } from "@typescript-eslint/type-utils";
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
14
  import ts9 from "typescript";
13
15
  import { getStaticTOMLValue } from "toml-eslint-parser";
14
16
  //#region package.json
15
17
  var name = "eslint-plugin-flawless";
16
- var version = "0.1.12";
18
+ var version = "1.0.0";
17
19
  var repository = {
18
20
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
19
21
  "type": "git"
@@ -83,11 +85,11 @@ function createFlawlessRule({ createOnce, ...meta }) {
83
85
  }
84
86
  //#endregion
85
87
  //#region src/rules/arrow-return-style/rule.ts
86
- const RULE_NAME$12 = "arrow-return-style";
88
+ const RULE_NAME$15 = "arrow-return-style";
87
89
  const IMPLICIT = "useImplicitReturn";
88
90
  const EXPLICIT = "useExplicitReturn";
89
91
  const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
90
- const DEFAULTS = {
92
+ const DEFAULTS$1 = {
91
93
  jsxAlwaysUseExplicitReturn: false,
92
94
  maxLen: 80,
93
95
  maxObjectProperties: 4,
@@ -231,9 +233,9 @@ function collectArrows(root) {
231
233
  return arrows.sort((a, b) => a.range[0] - b.range[0]);
232
234
  }
233
235
  const arrowReturnStyle = createFlawlessRule({
234
- name: RULE_NAME$12,
236
+ name: RULE_NAME$15,
235
237
  createOnce(context) {
236
- let config = DEFAULTS;
238
+ let config = DEFAULTS$1;
237
239
  let sourceCode;
238
240
  let lines;
239
241
  let pendingConsults;
@@ -288,38 +290,50 @@ const arrowReturnStyle = createFlawlessRule({
288
290
  };
289
291
  }
290
292
  /**
293
+ * Prepares the oxfmt question for `node`: how would the formatter render
294
+ * the enclosing statement once the candidate fix is applied? `collapse`
295
+ * supplies the implicit-direction candidate (the block body textually
296
+ * replaced by its collapsed expression); omitting it asks about the
297
+ * statement as written.
298
+ *
299
+ * @param node - The arrow function in question.
300
+ * @param collapse - The implicit-direction candidate, if any.
301
+ * @returns The consult, or `null` when the arrow cannot be located.
302
+ */
303
+ function buildConsult(node, collapse) {
304
+ const statement = statementOf(node);
305
+ const arrowIndex = collectArrows(statement).findIndex((arrow) => arrow.range[0] === node.range[0]);
306
+ if (arrowIndex === -1) return null;
307
+ const offset = statement.range[0];
308
+ const source = sourceCode.getText(statement);
309
+ const snippet = collapse === void 0 ? source : source.slice(0, collapse.block.range[0] - offset) + collapse.replacement + source.slice(collapse.block.range[1] - offset);
310
+ const request = {
311
+ arrowIndex,
312
+ code: `${snippet}\n`,
313
+ printWidth: printWidth(),
314
+ tabWidth: config.tabWidth
315
+ };
316
+ return {
317
+ baseIndent: leadingWhitespace(lineOf(statement.loc.start.line)),
318
+ cacheKey: `${arrowIndex}${request.printWidth}${request.tabWidth}${snippet}`,
319
+ node,
320
+ request
321
+ };
322
+ }
323
+ /**
291
324
  * Resolves every deferred oxfmt consult with a single worker round-trip:
292
- * would the formatter render each arrow (params through body) on a single
293
- * line that fits `maxLen`? Arrows the formatter cannot keep on one fitting
294
- * line are reported. Fails open (no report) when the worker or oxfmt is
295
- * unavailable so that an unavailable formatter can never introduce reports
296
- * it would have to fight over.
325
+ * would the formatter render each candidate arrow (params through body) on
326
+ * a single line that fits `maxLen`? Explicit consults report when it
327
+ * cannot, implicit consults report when it can. Fails open (no report)
328
+ * when the worker or oxfmt is unavailable so that an unavailable formatter
329
+ * can never introduce reports it would have to fight over.
297
330
  */
298
331
  function resolvePendingConsults() {
299
332
  if (pendingConsults.length === 0) return;
300
- const nodes = pendingConsults;
333
+ const consults = pendingConsults;
301
334
  pendingConsults = [];
302
335
  const worker = getFormatSync();
303
336
  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
337
  const misses = /* @__PURE__ */ new Map();
324
338
  for (const consult of consults) if (formatCacheGet(consult.cacheKey) === void 0) misses.set(consult.cacheKey, consult.request);
325
339
  if (misses.size > 0) {
@@ -338,7 +352,10 @@ const arrowReturnStyle = createFlawlessRule({
338
352
  const response = formatCacheGet(consult.cacheKey);
339
353
  if (response === void 0) continue;
340
354
  if (response.lineText === null) continue;
341
- if (!(response.singleLine && width(consult.baseIndent) + width(response.lineText) <= config.maxLen)) reportExplicit(consult.node, EXPLICIT);
355
+ const fits = response.singleLine && width(consult.baseIndent) + width(response.lineText) <= config.maxLen;
356
+ if (consult.kind === "implicit") {
357
+ if (fits) reportImplicit(consult.node, consult.block, consult.replacement);
358
+ } else if (!fits) reportExplicit(consult.node, EXPLICIT);
342
359
  }
343
360
  }
344
361
  /**
@@ -365,6 +382,28 @@ const arrowReturnStyle = createFlawlessRule({
365
382
  return arrowIndent.length > 0 ? " " : " ";
366
383
  }
367
384
  /**
385
+ * The `)` of a wrapped sole-argument call whose trailing comma the fix
386
+ * should absorb. `foo(() =>\n\tbody,\n)` only carries that comma because
387
+ * the argument was wrapped; once the block body hugs the call again the
388
+ * comma is debris (`},\n)` where the formatter writes `})`).
389
+ *
390
+ * @param node - The arrow function being converted.
391
+ * @param bodyEnd - The body's last token or node (including parens).
392
+ * @returns The closing paren to absorb up to, or `null`.
393
+ */
394
+ function danglingCloser(node, bodyEnd) {
395
+ const { loc, parent } = node;
396
+ if (!(parent.type === AST_NODE_TYPES.CallExpression || parent.type === AST_NODE_TYPES.NewExpression) || parent.arguments.length !== 1 || parent.arguments[0] !== node) return null;
397
+ const opener = sourceCode.getTokenBefore(node);
398
+ if (opener?.value !== "(" || opener.loc.end.line !== loc.start.line) return null;
399
+ const comma = sourceCode.getTokenAfter(bodyEnd);
400
+ if (comma?.value !== ",") return null;
401
+ const closer = sourceCode.getTokenAfter(comma);
402
+ if (closer?.value !== ")" || closer.range[1] !== parent.range[1]) return null;
403
+ if (sourceCode.getCommentsBefore(closer).length > 0) return null;
404
+ return closer;
405
+ }
406
+ /**
368
407
  * Reports and fixes an implicit arrow into an explicit block body.
369
408
  *
370
409
  * @param node - The arrow function to convert.
@@ -396,8 +435,9 @@ const arrowReturnStyle = createFlawlessRule({
396
435
  const commentLines = comments.map((comment) => targetIndent + sourceCode.getText(comment));
397
436
  const returnLine = `${targetIndent}return ${[firstLine, ...shifted].join("\n")};`;
398
437
  const replacement = ` {\n${[...commentLines, returnLine].join("\n")}\n${arrowIndent}}`;
438
+ const end = danglingCloser(node, bodyEnd)?.range[0] ?? bodyEnd.range[1];
399
439
  context.report({
400
- fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], bodyEnd.range[1]], replacement),
440
+ fix: (fixer) => fixer.replaceTextRange([arrowToken.range[1], end], replacement),
401
441
  messageId,
402
442
  node
403
443
  });
@@ -432,7 +472,20 @@ const arrowReturnStyle = createFlawlessRule({
432
472
  const prefix = lineOf(block.loc.start.line).slice(0, block.loc.start.column);
433
473
  const suffix = lineOf(block.loc.end.line).slice(block.loc.end.column);
434
474
  if (width(prefix + collapsed + suffix) > limit()) return;
435
- reportImplicit(node, block, collapsed);
475
+ if (statementOf(node).loc.end.line === block.loc.end.line || config.useOxfmt === false) {
476
+ reportImplicit(node, block, collapsed);
477
+ return;
478
+ }
479
+ const consult = buildConsult(node, {
480
+ block,
481
+ replacement: collapsed
482
+ });
483
+ if (consult !== null) pendingConsults.push({
484
+ ...consult,
485
+ block,
486
+ kind: "implicit",
487
+ replacement: collapsed
488
+ });
436
489
  }
437
490
  function checkExpressionBody(node) {
438
491
  const body = node.body;
@@ -461,8 +514,15 @@ const arrowReturnStyle = createFlawlessRule({
461
514
  return;
462
515
  }
463
516
  if (width(lineOf(bodyStart.loc.start.line)) > limit()) {
464
- if (config.useOxfmt !== false) pendingConsults.push(node);
465
- else reportExplicit(node, EXPLICIT);
517
+ if (config.useOxfmt === false) {
518
+ reportExplicit(node, EXPLICIT);
519
+ return;
520
+ }
521
+ const consult = buildConsult(node);
522
+ if (consult !== null) pendingConsults.push({
523
+ ...consult,
524
+ kind: "explicit"
525
+ });
466
526
  return;
467
527
  }
468
528
  if (objectStyleWantsExplicit(body)) reportExplicit(node, COMPLEX_EXPLICIT);
@@ -474,7 +534,7 @@ const arrowReturnStyle = createFlawlessRule({
474
534
  },
475
535
  "before": function() {
476
536
  config = {
477
- ...DEFAULTS,
537
+ ...DEFAULTS$1,
478
538
  ...context.options[0]
479
539
  };
480
540
  ({sourceCode} = context);
@@ -486,7 +546,7 @@ const arrowReturnStyle = createFlawlessRule({
486
546
  }
487
547
  };
488
548
  },
489
- defaultOptions: [DEFAULTS],
549
+ defaultOptions: [DEFAULTS$1],
490
550
  meta: {
491
551
  docs: {
492
552
  description: "Enforce arrow function return style based on line length",
@@ -539,23 +599,23 @@ const arrowReturnStyle = createFlawlessRule({
539
599
  });
540
600
  //#endregion
541
601
  //#region src/rules/jsx-shorthand-boolean/rule.ts
542
- const RULE_NAME$11 = "jsx-shorthand-boolean";
543
- const MESSAGE_ID$5 = "setAttributeValue";
544
- const messages$11 = { [MESSAGE_ID$5]: "Set an explicit value for boolean attribute '{{name}}'." };
545
- function createOnce$7(context) {
602
+ const RULE_NAME$14 = "jsx-shorthand-boolean";
603
+ const MESSAGE_ID$7 = "setAttributeValue";
604
+ const messages$14 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
605
+ function createOnce$9(context) {
546
606
  return { JSXAttribute(node) {
547
607
  if (node.value !== null) return;
548
608
  context.report({
549
609
  data: { name: context.sourceCode.getText(node.name) },
550
610
  fix: (fixer) => fixer.insertTextAfter(node.name, "={true}"),
551
- messageId: MESSAGE_ID$5,
611
+ messageId: MESSAGE_ID$7,
552
612
  node
553
613
  });
554
614
  } };
555
615
  }
556
616
  const jsxShorthandBoolean = createFlawlessRule({
557
- name: RULE_NAME$11,
558
- createOnce: createOnce$7,
617
+ name: RULE_NAME$14,
618
+ createOnce: createOnce$9,
559
619
  defaultOptions: [],
560
620
  meta: {
561
621
  docs: {
@@ -565,23 +625,23 @@ const jsxShorthandBoolean = createFlawlessRule({
565
625
  },
566
626
  fixable: "code",
567
627
  hasSuggestions: false,
568
- messages: messages$11,
628
+ messages: messages$14,
569
629
  schema: [],
570
630
  type: "suggestion"
571
631
  }
572
632
  });
573
633
  //#endregion
574
634
  //#region src/rules/jsx-shorthand-fragment/rule.ts
575
- const RULE_NAME$10 = "jsx-shorthand-fragment";
635
+ const RULE_NAME$13 = "jsx-shorthand-fragment";
576
636
  const MESSAGE_ID_NAMED = "useNamedFragment";
577
637
  const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
578
638
  const DEFAULT_MODE = "syntax";
579
639
  const DEFAULT_FRAGMENT_NAME = "Fragment";
580
- const messages$10 = {
640
+ const messages$13 = {
581
641
  [MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
582
642
  [MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
583
643
  };
584
- const schema$2 = [{
644
+ const schema$3 = [{
585
645
  additionalProperties: false,
586
646
  properties: {
587
647
  fragmentName: {
@@ -613,7 +673,7 @@ function jsxNameToString(node) {
613
673
  }
614
674
  return null;
615
675
  }
616
- function createOnce$6(context) {
676
+ function createOnce$8(context) {
617
677
  let mode;
618
678
  let fragmentName;
619
679
  let namedFragments;
@@ -660,8 +720,8 @@ function createOnce$6(context) {
660
720
  };
661
721
  }
662
722
  const jsxShorthandFragment = createFlawlessRule({
663
- name: RULE_NAME$10,
664
- createOnce: createOnce$6,
723
+ name: RULE_NAME$13,
724
+ createOnce: createOnce$8,
665
725
  defaultOptions: [{
666
726
  fragmentName: DEFAULT_FRAGMENT_NAME,
667
727
  mode: DEFAULT_MODE
@@ -678,7 +738,328 @@ const jsxShorthandFragment = createFlawlessRule({
678
738
  },
679
739
  fixable: "code",
680
740
  hasSuggestions: false,
681
- messages: messages$10,
741
+ messages: messages$13,
742
+ schema: schema$3,
743
+ type: "suggestion"
744
+ }
745
+ });
746
+ //#endregion
747
+ //#region src/rules/max-lines-per-function/core-ast-utils.ts
748
+ /**
749
+ * @file Helpers ported from ESLint core's `lib/rules/utils/ast-utils.js` and
750
+ * `lib/shared/string-utils.js`, so this rule's diagnostics read and point
751
+ * identically to core's `max-lines-per-function`. `@typescript-eslint/utils`
752
+ * re-exports `@eslint-community/eslint-utils` equivalents
753
+ * (`getFunctionNameWithKind`, `getFunctionHeadLocation`), but those diverge from
754
+ * core — they name variable-bound functions (`const f = () => {}` →
755
+ * `arrow function 'f'`) and bracket computed keys — so the port is what keeps
756
+ * parity with core, not availability. ESLint is MIT licensed — Copyright OpenJS
757
+ * Foundation and other contributors.
758
+ */
759
+ /**
760
+ * Describes a function by name and kind, as core does, so diagnostics read the
761
+ * same. Examples: `function 'foo'`, `arrow function`, `constructor`,
762
+ * `static async generator method 'foo'`, `private method #foo`.
763
+ *
764
+ * Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted: this
765
+ * rule only visits the three function node types, so a signature node can never
766
+ * reach here.
767
+ *
768
+ * @param node - The function to describe.
769
+ * @returns The space-joined description.
770
+ */
771
+ function getFunctionNameWithKind({ id, async, generator, parent, type }) {
772
+ const tokens = [];
773
+ if (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) {
774
+ if (parent.static) tokens.push("static");
775
+ if (!parent.computed && parent.key.type === AST_NODE_TYPES.PrivateIdentifier) tokens.push("private");
776
+ }
777
+ if (async) tokens.push("async");
778
+ if (generator) tokens.push("generator");
779
+ if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition) {
780
+ if (parent.kind === "constructor") return "constructor";
781
+ if (parent.kind === "get") tokens.push("getter");
782
+ else if (parent.kind === "set") tokens.push("setter");
783
+ else tokens.push("method");
784
+ } else if (parent.type === AST_NODE_TYPES.PropertyDefinition) tokens.push("method");
785
+ else {
786
+ if (type === AST_NODE_TYPES.ArrowFunctionExpression) tokens.push("arrow");
787
+ tokens.push("function");
788
+ }
789
+ 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}`);
790
+ else {
791
+ const name = getStaticPropertyName(parent);
792
+ if (name !== null) tokens.push(`'${name}'`);
793
+ else if (id !== null) tokens.push(`'${id.name}'`);
794
+ }
795
+ else if (id !== null) tokens.push(`'${id.name}'`);
796
+ return tokens.join(" ");
797
+ }
798
+ /**
799
+ * Locates the head of a function — the part worth underlining in a diagnostic,
800
+ * rather than the function's whole multi-line body. For example the
801
+ * `function foo` of a declaration, or the `=>` of an arrow function.
802
+ *
803
+ * Core's `TSPropertySignature` / `TSMethodSignature` branches are omitted for
804
+ * the same reason as in {@link getFunctionNameWithKind}: those nodes never
805
+ * reach here.
806
+ *
807
+ * @param node - The function to locate.
808
+ * @param sourceCode - The source code being linted.
809
+ * @returns The location of the function's head.
810
+ */
811
+ function getFunctionHeadLoc(node, sourceCode) {
812
+ const { body, loc, parent, type } = node;
813
+ if (parent.type === AST_NODE_TYPES.Property || parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.PropertyDefinition) return {
814
+ end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
815
+ start: parent.loc.start
816
+ };
817
+ if (type === AST_NODE_TYPES.ArrowFunctionExpression) {
818
+ const arrowToken = sourceCode.getTokenBefore(body, isArrowToken);
819
+ if (arrowToken !== null) return {
820
+ end: arrowToken.loc.end,
821
+ start: arrowToken.loc.start
822
+ };
823
+ }
824
+ return {
825
+ end: getOpeningParenOfParameters(node, sourceCode)?.loc.start ?? loc.end,
826
+ start: loc.start
827
+ };
828
+ }
829
+ /**
830
+ * Upper-cases the first character of a string.
831
+ *
832
+ * @param value - The string to convert.
833
+ * @returns The converted string.
834
+ */
835
+ function upperCaseFirst(value) {
836
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
837
+ }
838
+ /**
839
+ * Reads the statically known string value of a property key.
840
+ *
841
+ * @param node - The key node.
842
+ * @returns The value as a string, or `null` when it is not statically known.
843
+ */
844
+ function getStaticStringValue(node) {
845
+ if (node.type === AST_NODE_TYPES.Literal) return node.value === null ? node.raw : String(node.value);
846
+ if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0]?.value.cooked ?? null;
847
+ return null;
848
+ }
849
+ /**
850
+ * Reads the property name of a member definition.
851
+ *
852
+ * @param node - The member whose key to read.
853
+ * @returns The property name, or `null` when the key is computed from a
854
+ * dynamic expression.
855
+ */
856
+ function getStaticPropertyName(node) {
857
+ if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) return node.key.name;
858
+ return getStaticStringValue(node.key);
859
+ }
860
+ /**
861
+ * Finds the token opening a function's parameter list.
862
+ *
863
+ * @param node - The function whose parameters to locate.
864
+ * @param sourceCode - The source code being linted.
865
+ * @returns The opening paren, or — for an arrow function with a single
866
+ * parameter written without parentheses — that parameter's first token.
867
+ * `null` when neither can be found.
868
+ */
869
+ function getOpeningParenOfParameters(node, sourceCode) {
870
+ const [firstParameter] = node.params;
871
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression && node.params.length === 1 && firstParameter !== void 0) {
872
+ const argumentToken = sourceCode.getFirstToken(firstParameter);
873
+ if (argumentToken === null) return null;
874
+ const maybeParenToken = sourceCode.getTokenBefore(argumentToken);
875
+ return maybeParenToken !== null && isOpeningParenToken(maybeParenToken) ? maybeParenToken : argumentToken;
876
+ }
877
+ return node.id === null ? sourceCode.getFirstToken(node, isOpeningParenToken) : sourceCode.getTokenAfter(node.id, isOpeningParenToken);
878
+ }
879
+ //#endregion
880
+ //#region src/rules/max-lines-per-function/rule.ts
881
+ const RULE_NAME$12 = "max-lines-per-function";
882
+ const MESSAGE_ID_EXCEED = "exceed";
883
+ /** A line that is empty or holds only whitespace. */
884
+ const BLANK_LINE = /^\s*$/u;
885
+ const DEFAULTS = {
886
+ countFrom: "body",
887
+ IIFEs: false,
888
+ max: 50,
889
+ skipBlankLines: false,
890
+ skipComments: false
891
+ };
892
+ const messages$12 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
893
+ const schema$2 = [{
894
+ additionalProperties: false,
895
+ properties: {
896
+ countFrom: {
897
+ description: "Where the counted range begins: \"body\" (default) excludes the signature, \"function\" counts the whole node like ESLint core.",
898
+ enum: ["body", "function"],
899
+ type: "string"
900
+ },
901
+ IIFEs: {
902
+ description: "Whether immediately-invoked function expressions are measured.",
903
+ type: "boolean"
904
+ },
905
+ max: {
906
+ description: "The maximum number of lines a function may span.",
907
+ minimum: 0,
908
+ type: "integer"
909
+ },
910
+ skipBlankLines: {
911
+ description: "Whether lines containing only whitespace are excluded from the count.",
912
+ type: "boolean"
913
+ },
914
+ skipComments: {
915
+ description: "Whether lines consisting solely of a comment are excluded from the count.",
916
+ type: "boolean"
917
+ }
918
+ },
919
+ type: "object"
920
+ }];
921
+ /**
922
+ * Indexes comments by every source line they occupy, so a line can be tested
923
+ * for comment-only status in constant time.
924
+ *
925
+ * @param comments - Every comment in the file.
926
+ * @returns A map from one-indexed line number to the comment on that line.
927
+ */
928
+ function getCommentLineNumbers(comments) {
929
+ const map = /* @__PURE__ */ new Map();
930
+ for (const comment of comments) for (let { line } = comment.loc.start; line <= comment.loc.end.line; line += 1) map.set(line, comment);
931
+ return map;
932
+ }
933
+ /**
934
+ * Determines whether a comment occupies a whole line, leaving no code beside
935
+ * it.
936
+ *
937
+ * @param line - The source text of the line.
938
+ * @param lineNumber - The one-indexed number of that line.
939
+ * @param comment - The comment occupying part of the line.
940
+ * @returns `true` when nothing but the comment appears on the line.
941
+ */
942
+ function isFullLineComment(line, lineNumber, comment) {
943
+ const { end, start } = comment.loc;
944
+ const isFirstTokenOnLine = start.line === lineNumber && line.slice(0, start.column).trim() === "";
945
+ const isLastTokenOnLine = end.line === lineNumber && line.slice(end.column).trim() === "";
946
+ return (start.line < lineNumber || isFirstTokenOnLine) && (end.line > lineNumber || isLastTokenOnLine);
947
+ }
948
+ /**
949
+ * Determines whether a function is the callee of its own call expression.
950
+ *
951
+ * @param node - The node to test.
952
+ * @returns `true` when the node is immediately invoked.
953
+ */
954
+ function isIIFE(node) {
955
+ if (node.type !== AST_NODE_TYPES.ArrowFunctionExpression && node.type !== AST_NODE_TYPES.FunctionExpression) return false;
956
+ const { parent } = node;
957
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node;
958
+ }
959
+ /**
960
+ * Determines whether a function is the value of a class method or of an object
961
+ * shorthand method or accessor. In that case the enclosing member — not the
962
+ * bare function expression — is the unit measured and reported.
963
+ *
964
+ * @param node - The function to test.
965
+ * @returns `true` when the function is embedded in a member definition.
966
+ */
967
+ function isEmbedded(node) {
968
+ const { parent } = node;
969
+ if (parent.type === AST_NODE_TYPES.MethodDefinition) return parent.value === node;
970
+ if (parent.type === AST_NODE_TYPES.Property) return parent.value === node && (parent.method || parent.kind === "get" || parent.kind === "set");
971
+ return false;
972
+ }
973
+ /**
974
+ * Selects the source range whose lines are counted for a function.
975
+ *
976
+ * Under `"body"` this is the function's body — the braces themselves for a
977
+ * block body, or the expression for a concise arrow body — so the signature is
978
+ * excluded. Under `"function"` it is the reported node, matching ESLint core.
979
+ *
980
+ * @param countFrom - Which range to measure.
981
+ * @param funcNode - The function being measured.
982
+ * @param reportNode - The node the diagnostic attaches to: the enclosing member
983
+ * for an embedded method, otherwise `funcNode` itself.
984
+ * @returns The location whose line span is counted.
985
+ */
986
+ function getCountedLoc(countFrom, funcNode, reportNode) {
987
+ return countFrom === "function" ? reportNode.loc : funcNode.body.loc;
988
+ }
989
+ /**
990
+ * Builds the rule's per-file listener.
991
+ *
992
+ * @param context - The rule context.
993
+ * @returns The visitors measuring each function.
994
+ */
995
+ function createOnce$7(context) {
996
+ let commentLineNumbers;
997
+ let config;
998
+ let lines;
999
+ let sourceCode;
1000
+ /**
1001
+ * Counts a function's lines and reports it when it exceeds the maximum.
1002
+ *
1003
+ * @param funcNode - The function to measure.
1004
+ */
1005
+ function processFunction(funcNode) {
1006
+ const node = isEmbedded(funcNode) ? funcNode.parent : funcNode;
1007
+ if (!config.IIFEs && isIIFE(node)) return;
1008
+ const { end, start } = getCountedLoc(config.countFrom, funcNode, node);
1009
+ let lineCount = 0;
1010
+ for (let index = start.line - 1; index < end.line; index += 1) {
1011
+ const line = lines[index] ?? "";
1012
+ const lineNumber = index + 1;
1013
+ if (config.skipComments) {
1014
+ const comment = commentLineNumbers.get(lineNumber);
1015
+ if (comment !== void 0 && isFullLineComment(line, lineNumber, comment)) continue;
1016
+ }
1017
+ if (config.skipBlankLines && BLANK_LINE.test(line)) continue;
1018
+ lineCount += 1;
1019
+ }
1020
+ if (lineCount > config.max) context.report({
1021
+ data: {
1022
+ name: upperCaseFirst(getFunctionNameWithKind(funcNode)),
1023
+ lineCount,
1024
+ maxLines: config.max
1025
+ },
1026
+ loc: getFunctionHeadLoc(funcNode, sourceCode),
1027
+ messageId: MESSAGE_ID_EXCEED,
1028
+ node
1029
+ });
1030
+ }
1031
+ return {
1032
+ ArrowFunctionExpression: processFunction,
1033
+ before() {
1034
+ const options = context.options[0];
1035
+ config = {
1036
+ countFrom: options?.countFrom ?? DEFAULTS.countFrom,
1037
+ IIFEs: options?.IIFEs ?? DEFAULTS.IIFEs,
1038
+ max: options?.max ?? DEFAULTS.max,
1039
+ skipBlankLines: options?.skipBlankLines ?? DEFAULTS.skipBlankLines,
1040
+ skipComments: options?.skipComments ?? DEFAULTS.skipComments
1041
+ };
1042
+ ({sourceCode} = context);
1043
+ lines = [...sourceCode.lines];
1044
+ commentLineNumbers = config.skipComments ? getCommentLineNumbers(sourceCode.getAllComments()) : /* @__PURE__ */ new Map();
1045
+ },
1046
+ FunctionDeclaration: processFunction,
1047
+ FunctionExpression: processFunction
1048
+ };
1049
+ }
1050
+ const maxLinesPerFunction = createFlawlessRule({
1051
+ name: RULE_NAME$12,
1052
+ createOnce: createOnce$7,
1053
+ defaultOptions: [DEFAULTS],
1054
+ meta: {
1055
+ defaultOptions: [DEFAULTS],
1056
+ docs: {
1057
+ description: "Enforce a maximum number of lines of code in a function",
1058
+ recommended: false,
1059
+ requiresTypeChecking: false
1060
+ },
1061
+ hasSuggestions: false,
1062
+ messages: messages$12,
682
1063
  schema: schema$2,
683
1064
  type: "suggestion"
684
1065
  }
@@ -1843,8 +2224,8 @@ const SCHEMA = {
1843
2224
  };
1844
2225
  //#endregion
1845
2226
  //#region src/rules/naming-convention/rule.ts
1846
- const RULE_NAME$9 = "naming-convention";
1847
- const messages$9 = {
2227
+ const RULE_NAME$11 = "naming-convention";
2228
+ const messages$11 = {
1848
2229
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
1849
2230
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
1850
2231
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
@@ -1874,7 +2255,7 @@ const camelCaseNamingConfig = [
1874
2255
  selector: "typeLike"
1875
2256
  }
1876
2257
  ];
1877
- function create$3(contextWithoutDefaults) {
2258
+ function create$4(contextWithoutDefaults) {
1878
2259
  const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
1879
2260
  const validators = parseOptions(context);
1880
2261
  const compilerOptions = getParserServices(context, true).program?.getCompilerOptions() ?? {};
@@ -2192,8 +2573,8 @@ function create$3(contextWithoutDefaults) {
2192
2573
  }));
2193
2574
  }
2194
2575
  const namingConvention = createEslintRule({
2195
- name: RULE_NAME$9,
2196
- create: create$3,
2576
+ name: RULE_NAME$11,
2577
+ create: create$4,
2197
2578
  defaultOptions: camelCaseNamingConfig,
2198
2579
  meta: {
2199
2580
  docs: {
@@ -2203,7 +2584,7 @@ const namingConvention = createEslintRule({
2203
2584
  },
2204
2585
  fixable: void 0,
2205
2586
  hasSuggestions: false,
2206
- messages: messages$9,
2587
+ messages: messages$11,
2207
2588
  schema: SCHEMA,
2208
2589
  type: "suggestion"
2209
2590
  }
@@ -2234,9 +2615,9 @@ function requiresQuoting$1(node, target) {
2234
2615
  }
2235
2616
  //#endregion
2236
2617
  //#region src/rules/no-export-default-arrow/rule.ts
2237
- const RULE_NAME$8 = "no-export-default-arrow";
2238
- const MESSAGE_ID$4 = "disallowExportDefaultArrow";
2239
- const messages$8 = { [MESSAGE_ID$4]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
2618
+ const RULE_NAME$10 = "no-export-default-arrow";
2619
+ const MESSAGE_ID$6 = "disallowExportDefaultArrow";
2620
+ const messages$10 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
2240
2621
  /**
2241
2622
  * Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
2242
2623
  * word separators (`use-mouse` -> `useMouse`).
@@ -2314,7 +2695,7 @@ function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCo
2314
2695
  * @param context - The rule context.
2315
2696
  * @returns The rule listener.
2316
2697
  */
2317
- function createOnce$5(context) {
2698
+ function createOnce$6(context) {
2318
2699
  return { ArrowFunctionExpression(node) {
2319
2700
  const { parent } = node;
2320
2701
  if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
@@ -2325,14 +2706,14 @@ function createOnce$5(context) {
2325
2706
  exportDeclaration: parent,
2326
2707
  sourceCode: context.sourceCode
2327
2708
  }),
2328
- messageId: MESSAGE_ID$4,
2709
+ messageId: MESSAGE_ID$6,
2329
2710
  node
2330
2711
  });
2331
2712
  } };
2332
2713
  }
2333
2714
  const noExportDefaultArrow = createFlawlessRule({
2334
- name: RULE_NAME$8,
2335
- createOnce: createOnce$5,
2715
+ name: RULE_NAME$10,
2716
+ createOnce: createOnce$6,
2336
2717
  defaultOptions: [],
2337
2718
  meta: {
2338
2719
  docs: {
@@ -2342,7 +2723,391 @@ const noExportDefaultArrow = createFlawlessRule({
2342
2723
  },
2343
2724
  fixable: "code",
2344
2725
  hasSuggestions: false,
2345
- messages: messages$8,
2726
+ messages: messages$10,
2727
+ schema: [],
2728
+ type: "suggestion"
2729
+ }
2730
+ });
2731
+ //#endregion
2732
+ //#region src/rules/no-redundant-tsconfig-options/resolve-extends.ts
2733
+ const TOP_LEVEL_KEYS = [
2734
+ "include",
2735
+ "exclude",
2736
+ "files"
2737
+ ];
2738
+ function isRecord(value) {
2739
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2740
+ }
2741
+ /**
2742
+ * Resolves one `extends` specifier to an absolute config path, or `undefined`
2743
+ * when it cannot be found. Path specifiers resolve against the extending file's
2744
+ * directory (appending `.json` or `/tsconfig.json` as TypeScript does); package
2745
+ * specifiers go through `require.resolve`, which honours the package `exports`
2746
+ * map (so `@scope/pkg/subpath` maps correctly).
2747
+ *
2748
+ * @param spec - The raw `extends` specifier.
2749
+ * @param fromFile - The absolute path of the config doing the extending.
2750
+ * @returns The resolved absolute path, or `undefined`.
2751
+ */
2752
+ function resolveExtendsTarget(spec, fromFile) {
2753
+ if (isPathSpecifier(spec)) {
2754
+ const base = path.resolve(path.dirname(fromFile), spec);
2755
+ return (base.endsWith(".json") ? [base] : [`${base}.json`, path.join(base, "tsconfig.json")]).find((candidate) => fileExists(candidate));
2756
+ }
2757
+ const require = createRequire(fromFile);
2758
+ for (const candidate of [
2759
+ spec,
2760
+ `${spec}.json`,
2761
+ `${spec}/tsconfig.json`
2762
+ ]) try {
2763
+ return require.resolve(candidate);
2764
+ } catch {}
2765
+ }
2766
+ /**
2767
+ * Builds the config a child inherits from its `extends` targets alone (the child
2768
+ * itself excluded). Later targets in an `extends` array override earlier ones,
2769
+ * matching TypeScript's precedence.
2770
+ *
2771
+ * @param childFile - The absolute path of the config being linted.
2772
+ * @param extendsField - The child's raw `extends` value (string or array).
2773
+ * @returns The inherited config, or `undefined` when nothing resolves.
2774
+ */
2775
+ function buildInheritedConfig(childFile, extendsField) {
2776
+ const context = {
2777
+ cache: /* @__PURE__ */ new Map(),
2778
+ stack: /* @__PURE__ */ new Set([childFile])
2779
+ };
2780
+ const result = emptyConfig();
2781
+ let resolvedAny = false;
2782
+ for (const spec of normalizeExtends(extendsField)) {
2783
+ const target = resolveExtendsTarget(spec, childFile);
2784
+ if (target === void 0) continue;
2785
+ resolvedAny = true;
2786
+ mergeParent(result, flatten(target, context));
2787
+ }
2788
+ return resolvedAny ? result : void 0;
2789
+ }
2790
+ /**
2791
+ * Whether an `extends` specifier is a filesystem path (relative or absolute)
2792
+ * rather than a package specifier. Matches TypeScript: a leading `.` or a rooted
2793
+ * path is a path; anything else resolves through node module resolution.
2794
+ *
2795
+ * @param spec - The raw `extends` specifier.
2796
+ * @returns True when the specifier should resolve against the filesystem.
2797
+ */
2798
+ function isPathSpecifier(spec) {
2799
+ return spec.startsWith(".") || path.isAbsolute(spec);
2800
+ }
2801
+ function fileExists(candidate) {
2802
+ try {
2803
+ return statSync(candidate).isFile();
2804
+ } catch {
2805
+ return false;
2806
+ }
2807
+ }
2808
+ function normalizeExtends(value) {
2809
+ if (typeof value === "string") return [value];
2810
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === "string");
2811
+ return [];
2812
+ }
2813
+ function emptyConfig() {
2814
+ return {
2815
+ compilerOptions: /* @__PURE__ */ new Map(),
2816
+ topLevel: /* @__PURE__ */ new Map()
2817
+ };
2818
+ }
2819
+ function mergeInto(target, source) {
2820
+ for (const [key, entry] of source) target.set(key, entry);
2821
+ }
2822
+ function mergeParent(into, parent) {
2823
+ mergeInto(into.compilerOptions, parent.compilerOptions);
2824
+ mergeInto(into.topLevel, parent.topLevel);
2825
+ }
2826
+ function parseTsconfig(file) {
2827
+ try {
2828
+ const { ast } = parseForESLint(readFileSync(file, "utf8"), {});
2829
+ const statement = ast.body.at(0);
2830
+ if (statement?.expression.type !== "JSONObjectExpression") return;
2831
+ const value = getStaticJSONValue(statement.expression);
2832
+ return isRecord(value) ? value : void 0;
2833
+ } catch {
2834
+ return;
2835
+ }
2836
+ }
2837
+ /**
2838
+ * Reads and parses a parent tsconfig from disk. Returns `undefined` for any
2839
+ * failure — missing file, unreadable, invalid JSONC, or a top level that is not
2840
+ * an object — so a broken ancestor never crashes the lint.
2841
+ *
2842
+ * @param file - The absolute path of the config to read.
2843
+ * @param cache - Per-resolution parse cache, keyed by absolute path.
2844
+ * @returns The parsed config subset, or `undefined`.
2845
+ */
2846
+ function readTsconfig(file, cache) {
2847
+ if (cache.has(file)) return cache.get(file);
2848
+ const parsed = parseTsconfig(file);
2849
+ cache.set(file, parsed);
2850
+ return parsed;
2851
+ }
2852
+ function overlay(target, options, source) {
2853
+ if (!isRecord(options)) return;
2854
+ for (const [key, value] of Object.entries(options)) target.set(key, {
2855
+ source,
2856
+ value
2857
+ });
2858
+ }
2859
+ /**
2860
+ * Flattens the effective config a file resolves to — its own extends chain,
2861
+ * overlaid by its own values (a file's own options win over what it extends).
2862
+ * The `visited` set guards against circular `extends`.
2863
+ *
2864
+ * @param file - The absolute path of the config to flatten.
2865
+ * @param context - The shared cycle stack and parse cache.
2866
+ * @returns The flattened config, with each entry's `source` naming its definer.
2867
+ */
2868
+ function flatten(file, context) {
2869
+ const result = emptyConfig();
2870
+ if (context.stack.has(file)) return result;
2871
+ context.stack.add(file);
2872
+ try {
2873
+ const config = readTsconfig(file, context.cache);
2874
+ if (config === void 0) return result;
2875
+ for (const spec of normalizeExtends(config.extends)) {
2876
+ const target = resolveExtendsTarget(spec, file);
2877
+ if (target !== void 0) mergeParent(result, flatten(target, context));
2878
+ }
2879
+ overlay(result.compilerOptions, config.compilerOptions, file);
2880
+ for (const key of TOP_LEVEL_KEYS) if (config[key] !== void 0) result.topLevel.set(key, {
2881
+ source: file,
2882
+ value: config[key]
2883
+ });
2884
+ return result;
2885
+ } finally {
2886
+ context.stack.delete(file);
2887
+ }
2888
+ }
2889
+ //#endregion
2890
+ //#region src/rules/no-redundant-tsconfig-options/rule.ts
2891
+ const RULE_NAME$9 = "no-redundant-tsconfig-options";
2892
+ const MESSAGE_ID$5 = "redundant";
2893
+ /**
2894
+ * Compiler options TypeScript resolves case-insensitively; a child that re-sets
2895
+ * one differing only in case is still redundant.
2896
+ */
2897
+ const ENUM_SCALAR_KEYS = /* @__PURE__ */ new Set([
2898
+ "jsx",
2899
+ "module",
2900
+ "moduleDetection",
2901
+ "moduleResolution",
2902
+ "newLine",
2903
+ "target"
2904
+ ]);
2905
+ /**
2906
+ * Array-valued options TypeScript treats as unordered sets, so a reordered but
2907
+ * otherwise identical value is still redundant.
2908
+ */
2909
+ const SET_KEYS = /* @__PURE__ */ new Set(["lib", "types"]);
2910
+ /**
2911
+ * `compilerOptions` keys whose values are resolved as paths relative to the
2912
+ * config that declares them. An identical value repeated in a child in a
2913
+ * different directory means a different path, so these are only redundant when
2914
+ * the value is location-independent (see {@link isLocationIndependent}).
2915
+ */
2916
+ const PATH_KEYS = /* @__PURE__ */ new Set([
2917
+ "baseUrl",
2918
+ "declarationDir",
2919
+ "outDir",
2920
+ "outFile",
2921
+ "paths",
2922
+ "rootDir",
2923
+ "rootDirs",
2924
+ "tsBuildInfoFile",
2925
+ "typeRoots"
2926
+ ]);
2927
+ const messages$9 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
2928
+ /**
2929
+ * Structural equality between two static JSON values. Strings compare
2930
+ * case-insensitively when `caseInsensitive` is set (used for enum-valued
2931
+ * options and `lib` entries, which TypeScript itself folds).
2932
+ *
2933
+ * @param a - The child's value.
2934
+ * @param b - The inherited value.
2935
+ * @param caseInsensitive - Whether string scalars compare case-insensitively.
2936
+ * @returns Whether the two values are equal.
2937
+ */
2938
+ function equalValues(a, b, caseInsensitive) {
2939
+ if (typeof a === "string" && typeof b === "string") return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
2940
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => equalValues(item, b[index], caseInsensitive));
2941
+ if (isRecord(a) && isRecord(b)) {
2942
+ const aKeys = Object.keys(a);
2943
+ const bKeys = Object.keys(b);
2944
+ return aKeys.length === bKeys.length && aKeys.every((key) => key in b && equalValues(a[key], b[key], caseInsensitive));
2945
+ }
2946
+ return a === b;
2947
+ }
2948
+ /**
2949
+ * Set equality for two arrays: same length and every element of one has a
2950
+ * distinct equal counterpart in the other, ignoring order.
2951
+ *
2952
+ * @param a - The child's array.
2953
+ * @param b - The inherited array.
2954
+ * @param caseInsensitive - Whether string elements compare case-insensitively.
2955
+ * @returns Whether the arrays hold the same elements regardless of order.
2956
+ */
2957
+ function equalUnordered(a, b, caseInsensitive) {
2958
+ if (a.length !== b.length) return false;
2959
+ const remaining = [...b];
2960
+ for (const item of a) {
2961
+ const index = remaining.findIndex((other) => equalValues(item, other, caseInsensitive));
2962
+ if (index === -1) return false;
2963
+ remaining.splice(index, 1);
2964
+ }
2965
+ return true;
2966
+ }
2967
+ /**
2968
+ * Whether a path-valued option is location-independent — every path string it
2969
+ * contains is either `${configDir}`-anchored (re-anchored to the extending
2970
+ * config, so an identical value resolves to the same files) or absolute. Only
2971
+ * then is repeating a path option genuinely redundant.
2972
+ *
2973
+ * @param value - The option value.
2974
+ * @returns Whether the value resolves the same regardless of config location.
2975
+ */
2976
+ function isLocationIndependent(value) {
2977
+ if (typeof value === "string") return value.includes("${configDir}") || path.isAbsolute(value);
2978
+ if (Array.isArray(value)) return value.every((item) => isLocationIndependent(item));
2979
+ if (isRecord(value)) return Object.values(value).every((item) => isLocationIndependent(item));
2980
+ return true;
2981
+ }
2982
+ /**
2983
+ * Whether a child option re-setting `inherited` to `childValue` is redundant:
2984
+ * the values are equal and, for path-valued options, location-independent.
2985
+ *
2986
+ * @param key - The option name.
2987
+ * @param childValue - The value the child sets.
2988
+ * @param inherited - The inherited value.
2989
+ * @param isPathKey - Whether the key is path-valued.
2990
+ * @returns Whether the child option is redundant.
2991
+ */
2992
+ function isRedundant(key, childValue, inherited, isPathKey) {
2993
+ const caseInsensitive = key === "lib" || ENUM_SCALAR_KEYS.has(key);
2994
+ if (!(SET_KEYS.has(key) && Array.isArray(childValue) && Array.isArray(inherited) ? equalUnordered(childValue, inherited, caseInsensitive) : equalValues(childValue, inherited, caseInsensitive))) return false;
2995
+ return !isPathKey || isLocationIndependent(childValue);
2996
+ }
2997
+ function keyName({ key }) {
2998
+ return key.type === "JSONIdentifier" ? key.name : String(getStaticJSONValue(key));
2999
+ }
3000
+ function findProperty(object, name) {
3001
+ return object.properties.find((property) => keyName(property) === name);
3002
+ }
3003
+ /**
3004
+ * Reads a node's static value, returning `undefined` when it cannot be evaluated
3005
+ * (such as an exotic non-JSON node), so an odd value never crashes the rule. The
3006
+ * result is boxed so a genuine `undefined` value stays distinguishable.
3007
+ *
3008
+ * @param node - The value node to evaluate.
3009
+ * @returns A box holding the value, or `undefined` on failure.
3010
+ */
3011
+ function safeStaticValue(node) {
3012
+ try {
3013
+ return { value: getStaticJSONValue(node) };
3014
+ } catch {
3015
+ return;
3016
+ }
3017
+ }
3018
+ function create$3(context) {
3019
+ const { sourceCode } = context;
3020
+ if (sourceCode.parserServices.isJSON !== true) return {};
3021
+ const { filename } = context;
3022
+ if (!path.isAbsolute(filename)) return {};
3023
+ /**
3024
+ * Reports a redundant property, removing it (with its delimiter comma) unless
3025
+ * a comment is attached, in which case it reports without a fix to avoid
3026
+ * stranding the comment.
3027
+ *
3028
+ * @param property - The redundant property node.
3029
+ * @param name - The property's key name (already computed by the caller).
3030
+ * @param entry - The inherited value and the config that defined it.
3031
+ */
3032
+ function report(property, name, entry) {
3033
+ const relative = path.relative(path.dirname(filename), entry.source) || entry.source;
3034
+ context.report({
3035
+ data: {
3036
+ option: name,
3037
+ source: relative.split(path.sep).join("/")
3038
+ },
3039
+ fix: buildFix(property),
3040
+ loc: property.key.loc,
3041
+ messageId: MESSAGE_ID$5
3042
+ });
3043
+ }
3044
+ function buildFix(property) {
3045
+ const node = property;
3046
+ const before = sourceCode.getTokenBefore(node);
3047
+ const after = sourceCode.getTokenAfter(node);
3048
+ const leading = sourceCode.getCommentsBefore(node);
3049
+ const between = sourceCode.getCommentsAfter(node);
3050
+ const trailing = after?.value === "," ? sourceCode.getCommentsAfter(after).filter((comment) => comment.loc.start.line === after.loc.end.line) : [];
3051
+ if (leading.length > 0 || between.length > 0 || trailing.length > 0) return;
3052
+ let start;
3053
+ let end;
3054
+ if (after?.value === ",") {
3055
+ start = before?.range[1] ?? property.range[0];
3056
+ end = after.range[1];
3057
+ } else if (before?.value === ",") {
3058
+ start = before.range[0];
3059
+ end = property.range[1];
3060
+ } else {
3061
+ start = before?.range[1] ?? property.range[0];
3062
+ end = property.range[1];
3063
+ }
3064
+ return (fixer) => fixer.removeRange([start, end]);
3065
+ }
3066
+ function checkObject(object, lookup, isPathKey) {
3067
+ for (const property of object.properties) {
3068
+ const name = keyName(property);
3069
+ const entry = lookup(name);
3070
+ if (entry === void 0) continue;
3071
+ const childValue = safeStaticValue(property.value);
3072
+ if (childValue !== void 0 && isRedundant(name, childValue.value, entry.value, isPathKey(name))) report(property, name, entry);
3073
+ }
3074
+ }
3075
+ return { Program() {
3076
+ const statement = sourceCode.ast.body.at(0);
3077
+ if (statement?.expression.type !== "JSONObjectExpression") return;
3078
+ const root = statement.expression;
3079
+ const extendsProperty = findProperty(root, "extends");
3080
+ if (extendsProperty === void 0) return;
3081
+ const extendsValue = safeStaticValue(extendsProperty.value);
3082
+ if (extendsValue === void 0) return;
3083
+ const inherited = buildInheritedConfig(filename, extendsValue.value);
3084
+ if (inherited === void 0) return;
3085
+ const compilerOptions = findProperty(root, "compilerOptions");
3086
+ if (compilerOptions?.value.type === "JSONObjectExpression") checkObject(compilerOptions.value, (name) => {
3087
+ return inherited.compilerOptions.get(name);
3088
+ }, (name) => {
3089
+ return PATH_KEYS.has(name);
3090
+ });
3091
+ checkObject(root, (name) => {
3092
+ return inherited.topLevel.get(name);
3093
+ }, () => {
3094
+ return true;
3095
+ });
3096
+ } };
3097
+ }
3098
+ const noRedundantTsconfigOptions = createEslintRule({
3099
+ name: RULE_NAME$9,
3100
+ create: create$3,
3101
+ defaultOptions: [],
3102
+ meta: {
3103
+ docs: {
3104
+ description: "Disallow tsconfig options that redundantly re-set a value already provided by an extended config",
3105
+ recommended: false,
3106
+ requiresTypeChecking: false
3107
+ },
3108
+ fixable: "code",
3109
+ hasSuggestions: false,
3110
+ messages: messages$9,
2346
3111
  schema: [],
2347
3112
  type: "suggestion"
2348
3113
  }
@@ -2585,15 +3350,15 @@ function resolveFactory(sourceCode, node) {
2585
3350
  }
2586
3351
  //#endregion
2587
3352
  //#region src/rules/no-unnecessary-use-callback/rule.ts
2588
- const RULE_NAME$7 = "no-unnecessary-use-callback";
3353
+ const RULE_NAME$8 = "no-unnecessary-use-callback";
2589
3354
  const MESSAGE_ID_DEFAULT$1 = "default";
2590
3355
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
2591
- const messages$7 = {
3356
+ const messages$8 = {
2592
3357
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
2593
3358
  [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
3359
  };
2595
3360
  const noUnnecessaryUseCallback = createFlawlessRule({
2596
- name: RULE_NAME$7,
3361
+ name: RULE_NAME$8,
2597
3362
  createOnce: createUnnecessaryHookRule({
2598
3363
  hook: "useCallback",
2599
3364
  messageIds: {
@@ -2609,22 +3374,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
2609
3374
  requiresTypeChecking: false
2610
3375
  },
2611
3376
  hasSuggestions: false,
2612
- messages: messages$7,
3377
+ messages: messages$8,
2613
3378
  schema: [],
2614
3379
  type: "suggestion"
2615
3380
  }
2616
3381
  });
2617
3382
  //#endregion
2618
3383
  //#region src/rules/no-unnecessary-use-memo/rule.ts
2619
- const RULE_NAME$6 = "no-unnecessary-use-memo";
3384
+ const RULE_NAME$7 = "no-unnecessary-use-memo";
2620
3385
  const MESSAGE_ID_DEFAULT = "default";
2621
3386
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
2622
- const messages$6 = {
3387
+ const messages$7 = {
2623
3388
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
2624
3389
  [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
3390
  };
2626
3391
  const noUnnecessaryUseMemo = createFlawlessRule({
2627
- name: RULE_NAME$6,
3392
+ name: RULE_NAME$7,
2628
3393
  createOnce: createUnnecessaryHookRule({
2629
3394
  hook: "useMemo",
2630
3395
  messageIds: {
@@ -2640,12 +3405,132 @@ const noUnnecessaryUseMemo = createFlawlessRule({
2640
3405
  requiresTypeChecking: false
2641
3406
  },
2642
3407
  hasSuggestions: false,
2643
- messages: messages$6,
3408
+ messages: messages$7,
2644
3409
  schema: [],
2645
3410
  type: "suggestion"
2646
3411
  }
2647
3412
  });
2648
3413
  //#endregion
3414
+ //#region src/rules/padding-after-expect-assertions/rule.ts
3415
+ const RULE_NAME$6 = "padding-after-expect-assertions";
3416
+ const MESSAGE_ID$4 = "missingPadding";
3417
+ const messages$6 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
3418
+ /**
3419
+ * Matches a statement that declares the expected assertion count at the top of
3420
+ * a test: `expect.assertions(n)` or `expect.hasAssertions()`.
3421
+ *
3422
+ * Detection is purely syntactic (a non-computed `expect.assertions` /
3423
+ * `expect.hasAssertions` call) with no scope analysis, mirroring the deliberate
3424
+ * looseness of `@vitest/eslint-plugin`'s own padding rules. A shadowed local
3425
+ * `expect` is vanishingly rare, and the only consequence is a harmless blank
3426
+ * line.
3427
+ *
3428
+ * @param node - The expression statement to inspect.
3429
+ * @returns The matched member name (`expect.assertions` / `expect.hasAssertions`)
3430
+ * for the message, or `undefined` when the statement is not an assertion count.
3431
+ */
3432
+ function getAssertionName({ expression }) {
3433
+ if (expression.type !== AST_NODE_TYPES.CallExpression) return;
3434
+ const { callee } = expression;
3435
+ 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;
3436
+ const { name } = callee.property;
3437
+ if (name !== "assertions" && name !== "hasAssertions") return;
3438
+ return `expect.${name}`;
3439
+ }
3440
+ /**
3441
+ * Returns the statement list that directly contains a node, so its following
3442
+ * sibling can be found. The assertion count opens an `it`/`test` callback body
3443
+ * (a `BlockStatement`) or, at worst, sits at the top level (`Program`).
3444
+ *
3445
+ * @param node - The node whose containing list is wanted.
3446
+ * @returns The sibling statements, or `undefined` when the parent holds no list.
3447
+ */
3448
+ function getContainingList({ parent }) {
3449
+ if (parent.type === AST_NODE_TYPES.BlockStatement || parent.type === AST_NODE_TYPES.Program) return parent.body;
3450
+ }
3451
+ /**
3452
+ * Locates the two tokens the padding is measured and inserted between: the last
3453
+ * token of the assertion statement (advanced past any trailing comment on the
3454
+ * same line) and the first token of the following statement (a leading comment
3455
+ * included). This mirrors ESLint core's `padding-line-between-statements`, so a
3456
+ * trailing `// note` does not defeat the rule and the fix lands in the right
3457
+ * place. Note that `yaml-block-key-blank-lines` deliberately takes the opposite
3458
+ * stance and bails on comments, since rewriting there would re-attach them.
3459
+ *
3460
+ * @param sourceCode - The source code, for token lookups.
3461
+ * @param node - The assertion statement.
3462
+ * @param nextNode - The statement that follows it.
3463
+ * @returns The anchor tokens, or `undefined` when either cannot be resolved.
3464
+ */
3465
+ function findPaddingAnchor(sourceCode, node, nextNode) {
3466
+ const lastToken = sourceCode.getLastToken(node);
3467
+ if (lastToken === null) return;
3468
+ let previousToken = lastToken;
3469
+ const nextToken = sourceCode.getFirstTokenBetween(previousToken, nextNode, {
3470
+ filter(token) {
3471
+ if (ASTUtils.isTokenOnSameLine(previousToken, token)) {
3472
+ previousToken = token;
3473
+ return false;
3474
+ }
3475
+ return true;
3476
+ },
3477
+ includeComments: true
3478
+ }) ?? sourceCode.getFirstToken(nextNode);
3479
+ if (nextToken === null) return;
3480
+ return {
3481
+ nextToken,
3482
+ previousToken
3483
+ };
3484
+ }
3485
+ /**
3486
+ * Requires a blank line after the assertion count that opens a test, keeping the
3487
+ * bookkeeping visually separate from the expectations that follow.
3488
+ *
3489
+ * @param context - The rule context.
3490
+ * @returns The rule listener.
3491
+ */
3492
+ function createOnce$3(context) {
3493
+ return { ExpressionStatement(node) {
3494
+ const name = getAssertionName(node);
3495
+ if (name === void 0) return;
3496
+ const list = getContainingList(node);
3497
+ if (list === void 0) return;
3498
+ const nextNode = list[list.indexOf(node) + 1];
3499
+ if (nextNode === void 0) return;
3500
+ const { sourceCode } = context;
3501
+ const anchor = findPaddingAnchor(sourceCode, node, nextNode);
3502
+ if (anchor === void 0) return;
3503
+ const { nextToken, previousToken } = anchor;
3504
+ const gap = nextToken.loc.start.line - previousToken.loc.end.line;
3505
+ if (gap > 1) return;
3506
+ context.report({
3507
+ data: { name },
3508
+ fix(fixer) {
3509
+ return fixer.insertTextAfter(previousToken, gap === 0 ? "\n\n" : "\n");
3510
+ },
3511
+ loc: node.loc,
3512
+ messageId: MESSAGE_ID$4
3513
+ });
3514
+ } };
3515
+ }
3516
+ const paddingAfterExpectAssertions = createFlawlessRule({
3517
+ name: RULE_NAME$6,
3518
+ createOnce: createOnce$3,
3519
+ defaultOptions: [],
3520
+ meta: {
3521
+ docs: {
3522
+ description: "Enforce a blank line after `expect.assertions` and `expect.hasAssertions`",
3523
+ recommended: false,
3524
+ requiresTypeChecking: false
3525
+ },
3526
+ fixable: "whitespace",
3527
+ hasSuggestions: false,
3528
+ messages: messages$6,
3529
+ schema: [],
3530
+ type: "layout"
3531
+ }
3532
+ });
3533
+ //#endregion
2649
3534
  //#region src/rules/prefer-destructuring-assignment/rule.ts
2650
3535
  const RULE_NAME$5 = "prefer-destructuring-assignment";
2651
3536
  const MESSAGE_ID$3 = "default";
@@ -4209,10 +5094,13 @@ const plugin = {
4209
5094
  "arrow-return-style": arrowReturnStyle,
4210
5095
  "jsx-shorthand-boolean": jsxShorthandBoolean,
4211
5096
  "jsx-shorthand-fragment": jsxShorthandFragment,
5097
+ "max-lines-per-function": maxLinesPerFunction,
4212
5098
  "naming-convention": namingConvention,
4213
5099
  "no-export-default-arrow": noExportDefaultArrow,
5100
+ "no-redundant-tsconfig-options": noRedundantTsconfigOptions,
4214
5101
  "no-unnecessary-use-callback": noUnnecessaryUseCallback,
4215
5102
  "no-unnecessary-use-memo": noUnnecessaryUseMemo,
5103
+ "padding-after-expect-assertions": paddingAfterExpectAssertions,
4216
5104
  "prefer-destructuring-assignment": preferDestructuringAssignment,
4217
5105
  "prefer-parameter-destructuring": preferParameterDestructuring,
4218
5106
  "prefer-read-only-props": preferReadOnlyProps,