eslint-plugin-flawless 1.1.0 → 1.2.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,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { ASTUtils, AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
2
+ import { ASTUtils, AST_NODE_TYPES, AST_TOKEN_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
3
3
  import { existsSync, readFileSync, statSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -14,7 +14,7 @@ import * as core from "@eslint-react/core";
14
14
  import { getStaticTOMLValue } from "toml-eslint-parser";
15
15
  //#region package.json
16
16
  var name = "eslint-plugin-flawless";
17
- var version = "1.1.0";
17
+ var version = "1.2.0";
18
18
  var repository = {
19
19
  "url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
20
20
  "type": "git"
@@ -84,7 +84,7 @@ function createFlawlessRule({ createOnce, ...meta }) {
84
84
  }
85
85
  //#endregion
86
86
  //#region src/rules/arrow-return-style/rule.ts
87
- const RULE_NAME$15 = "arrow-return-style";
87
+ const RULE_NAME$16 = "arrow-return-style";
88
88
  const IMPLICIT = "useImplicitReturn";
89
89
  const EXPLICIT = "useExplicitReturn";
90
90
  const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
@@ -232,7 +232,7 @@ function collectArrows(root) {
232
232
  return arrows.sort((a, b) => a.range[0] - b.range[0]);
233
233
  }
234
234
  const arrowReturnStyle = createFlawlessRule({
235
- name: RULE_NAME$15,
235
+ name: RULE_NAME$16,
236
236
  createOnce(context) {
237
237
  let config = DEFAULTS$1;
238
238
  let sourceCode;
@@ -598,10 +598,10 @@ const arrowReturnStyle = createFlawlessRule({
598
598
  });
599
599
  //#endregion
600
600
  //#region src/rules/jsx-shorthand-boolean/rule.ts
601
- const RULE_NAME$14 = "jsx-shorthand-boolean";
601
+ const RULE_NAME$15 = "jsx-shorthand-boolean";
602
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) {
603
+ const messages$15 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
604
+ function createOnce$10(context) {
605
605
  return { JSXAttribute(node) {
606
606
  if (node.value !== null) return;
607
607
  context.report({
@@ -613,8 +613,8 @@ function createOnce$9(context) {
613
613
  } };
614
614
  }
615
615
  const jsxShorthandBoolean = createFlawlessRule({
616
- name: RULE_NAME$14,
617
- createOnce: createOnce$9,
616
+ name: RULE_NAME$15,
617
+ createOnce: createOnce$10,
618
618
  defaultOptions: [],
619
619
  meta: {
620
620
  docs: {
@@ -624,19 +624,19 @@ const jsxShorthandBoolean = createFlawlessRule({
624
624
  },
625
625
  fixable: "code",
626
626
  hasSuggestions: false,
627
- messages: messages$14,
627
+ messages: messages$15,
628
628
  schema: [],
629
629
  type: "suggestion"
630
630
  }
631
631
  });
632
632
  //#endregion
633
633
  //#region src/rules/jsx-shorthand-fragment/rule.ts
634
- const RULE_NAME$13 = "jsx-shorthand-fragment";
634
+ const RULE_NAME$14 = "jsx-shorthand-fragment";
635
635
  const MESSAGE_ID_NAMED = "useNamedFragment";
636
636
  const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
637
637
  const DEFAULT_MODE = "syntax";
638
638
  const DEFAULT_FRAGMENT_NAME = "Fragment";
639
- const messages$13 = {
639
+ const messages$14 = {
640
640
  [MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
641
641
  [MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
642
642
  };
@@ -672,7 +672,7 @@ function jsxNameToString(node) {
672
672
  }
673
673
  return null;
674
674
  }
675
- function createOnce$8(context) {
675
+ function createOnce$9(context) {
676
676
  let mode;
677
677
  let fragmentName;
678
678
  let namedFragments;
@@ -719,8 +719,8 @@ function createOnce$8(context) {
719
719
  };
720
720
  }
721
721
  const jsxShorthandFragment = createFlawlessRule({
722
- name: RULE_NAME$13,
723
- createOnce: createOnce$8,
722
+ name: RULE_NAME$14,
723
+ createOnce: createOnce$9,
724
724
  defaultOptions: [{
725
725
  fragmentName: DEFAULT_FRAGMENT_NAME,
726
726
  mode: DEFAULT_MODE
@@ -737,7 +737,7 @@ const jsxShorthandFragment = createFlawlessRule({
737
737
  },
738
738
  fixable: "code",
739
739
  hasSuggestions: false,
740
- messages: messages$13,
740
+ messages: messages$14,
741
741
  schema: schema$3,
742
742
  type: "suggestion"
743
743
  }
@@ -877,7 +877,7 @@ function getOpeningParenOfParameters(node, sourceCode) {
877
877
  }
878
878
  //#endregion
879
879
  //#region src/rules/max-lines-per-function/rule.ts
880
- const RULE_NAME$12 = "max-lines-per-function";
880
+ const RULE_NAME$13 = "max-lines-per-function";
881
881
  const MESSAGE_ID_EXCEED = "exceed";
882
882
  /** A line that is empty or holds only whitespace. */
883
883
  const BLANK_LINE = /^\s*$/u;
@@ -888,7 +888,7 @@ const DEFAULTS = {
888
888
  skipBlankLines: false,
889
889
  skipComments: false
890
890
  };
891
- const messages$12 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
891
+ const messages$13 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
892
892
  const schema$2 = [{
893
893
  additionalProperties: false,
894
894
  properties: {
@@ -991,7 +991,7 @@ function getCountedLoc(countFrom, funcNode, reportNode) {
991
991
  * @param context - The rule context.
992
992
  * @returns The visitors measuring each function.
993
993
  */
994
- function createOnce$7(context) {
994
+ function createOnce$8(context) {
995
995
  let commentLineNumbers;
996
996
  let config;
997
997
  let lines;
@@ -1047,8 +1047,8 @@ function createOnce$7(context) {
1047
1047
  };
1048
1048
  }
1049
1049
  const maxLinesPerFunction = createFlawlessRule({
1050
- name: RULE_NAME$12,
1051
- createOnce: createOnce$7,
1050
+ name: RULE_NAME$13,
1051
+ createOnce: createOnce$8,
1052
1052
  defaultOptions: [DEFAULTS],
1053
1053
  meta: {
1054
1054
  defaultOptions: [DEFAULTS],
@@ -1058,7 +1058,7 @@ const maxLinesPerFunction = createFlawlessRule({
1058
1058
  requiresTypeChecking: false
1059
1059
  },
1060
1060
  hasSuggestions: false,
1061
- messages: messages$12,
1061
+ messages: messages$13,
1062
1062
  schema: schema$2,
1063
1063
  type: "suggestion"
1064
1064
  }
@@ -1658,7 +1658,8 @@ const Modifier = {
1658
1658
  "override": 8192,
1659
1659
  "async": 16384,
1660
1660
  "default": 32768,
1661
- "namespace": 65536
1661
+ "namespace": 65536,
1662
+ "constAsserted": 131072
1662
1663
  };
1663
1664
  const PredefinedFormat = {
1664
1665
  camelCase: 1,
@@ -1670,15 +1671,15 @@ const PredefinedFormat = {
1670
1671
  };
1671
1672
  const PredefinedFormatValueToKey = Object.fromEntries(Object.entries(PredefinedFormat).map(([key, value]) => [value, key]));
1672
1673
  const TypeModifier = {
1673
- boolean: 131072,
1674
- string: 262144,
1675
- number: 524288,
1676
- function: 1048576,
1677
- array: 2097152
1674
+ boolean: 262144,
1675
+ string: 524288,
1676
+ number: 1048576,
1677
+ function: 2097152,
1678
+ array: 4194304
1678
1679
  };
1679
1680
  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;
1681
+ const TYPE_REFERENCE_LOOSE_WEIGHT = 1 << 23;
1682
+ const TYPE_REFERENCE_STRICT_WEIGHT = 1 << 24;
1682
1683
  const UnderscoreOption = {
1683
1684
  forbid: 1,
1684
1685
  allow: 2,
@@ -1773,7 +1774,7 @@ function createValidator(type, context, allConfigs) {
1773
1774
  if (!aIsMethodOrProperty && bIsMethodOrProperty) return 1;
1774
1775
  return b.selector - a.selector;
1775
1776
  });
1776
- return (node, modifiers = /* @__PURE__ */ new Set()) => {
1777
+ return (node, modifiers = /* @__PURE__ */ new Set(), isObjectStyleEnumName = false) => {
1777
1778
  const originalName = node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`;
1778
1779
  for (const config of configs) {
1779
1780
  if (config.filter?.regex.test(originalName) !== config.filter?.match) continue;
@@ -1783,6 +1784,7 @@ function createValidator(type, context, allConfigs) {
1783
1784
  name = validateUnderscore({
1784
1785
  name,
1785
1786
  config,
1787
+ isObjectStyleEnumName,
1786
1788
  node,
1787
1789
  originalName,
1788
1790
  position: "leading"
@@ -1791,6 +1793,7 @@ function createValidator(type, context, allConfigs) {
1791
1793
  name = validateUnderscore({
1792
1794
  name,
1793
1795
  config,
1796
+ isObjectStyleEnumName,
1794
1797
  node,
1795
1798
  originalName,
1796
1799
  position: "trailing"
@@ -1799,6 +1802,7 @@ function createValidator(type, context, allConfigs) {
1799
1802
  name = validateAffix({
1800
1803
  name,
1801
1804
  config,
1805
+ isObjectStyleEnumName,
1802
1806
  node,
1803
1807
  originalName,
1804
1808
  position: "prefix"
@@ -1807,6 +1811,7 @@ function createValidator(type, context, allConfigs) {
1807
1811
  name = validateAffix({
1808
1812
  name,
1809
1813
  config,
1814
+ isObjectStyleEnumName,
1810
1815
  node,
1811
1816
  originalName,
1812
1817
  position: "suffix"
@@ -1815,12 +1820,14 @@ function createValidator(type, context, allConfigs) {
1815
1820
  if (!validateCustom({
1816
1821
  name,
1817
1822
  config,
1823
+ isObjectStyleEnumName,
1818
1824
  node,
1819
1825
  originalName
1820
1826
  })) return;
1821
1827
  if (!validatePredefinedFormat({
1822
1828
  name,
1823
1829
  config,
1830
+ isObjectStyleEnumName,
1824
1831
  modifiers,
1825
1832
  node,
1826
1833
  originalName
@@ -1844,7 +1851,7 @@ function createValidator(type, context, allConfigs) {
1844
1851
  type: selectorTypeToMessageString(type)
1845
1852
  };
1846
1853
  }
1847
- function validateUnderscore({ name, config, node, originalName, position }) {
1854
+ function validateUnderscore({ name, config, isObjectStyleEnumName, node, originalName, position }) {
1848
1855
  const option = position === "leading" ? config.leadingUnderscore : config.trailingUnderscore;
1849
1856
  if (!option) return name;
1850
1857
  const hasSingleUnderscore = position === "leading" ? () => name.startsWith("_") : () => name.endsWith("_");
@@ -1870,7 +1877,7 @@ function createValidator(type, context, allConfigs) {
1870
1877
  originalName,
1871
1878
  position
1872
1879
  }),
1873
- messageId: "unexpectedUnderscore",
1880
+ messageId: pickMessageId("unexpectedUnderscore", isObjectStyleEnumName),
1874
1881
  node
1875
1882
  });
1876
1883
  return;
@@ -1884,7 +1891,7 @@ function createValidator(type, context, allConfigs) {
1884
1891
  originalName,
1885
1892
  position
1886
1893
  }),
1887
- messageId: "missingUnderscore",
1894
+ messageId: pickMessageId("missingUnderscore", isObjectStyleEnumName),
1888
1895
  node
1889
1896
  });
1890
1897
  return;
@@ -1898,7 +1905,7 @@ function createValidator(type, context, allConfigs) {
1898
1905
  originalName,
1899
1906
  position
1900
1907
  }),
1901
- messageId: "missingUnderscore",
1908
+ messageId: pickMessageId("missingUnderscore", isObjectStyleEnumName),
1902
1909
  node
1903
1910
  });
1904
1911
  return;
@@ -1906,7 +1913,7 @@ function createValidator(type, context, allConfigs) {
1906
1913
  return trimDoubleUnderscore();
1907
1914
  }
1908
1915
  }
1909
- function validateAffix({ name, config, node, originalName, position }) {
1916
+ function validateAffix({ name, config, isObjectStyleEnumName, node, originalName, position }) {
1910
1917
  const affixes = config[position];
1911
1918
  if (!affixes || affixes.length === 0) return name;
1912
1919
  for (const affix of affixes) {
@@ -1920,11 +1927,11 @@ function createValidator(type, context, allConfigs) {
1920
1927
  originalName,
1921
1928
  position
1922
1929
  }),
1923
- messageId: "missingAffix",
1930
+ messageId: pickMessageId("missingAffix", isObjectStyleEnumName),
1924
1931
  node
1925
1932
  });
1926
1933
  }
1927
- function validateCustom({ name, config, node, originalName }) {
1934
+ function validateCustom({ name, config, isObjectStyleEnumName, node, originalName }) {
1928
1935
  const { custom } = config;
1929
1936
  if (!custom) return true;
1930
1937
  const result = custom.regex.test(name);
@@ -1935,12 +1942,12 @@ function createValidator(type, context, allConfigs) {
1935
1942
  custom,
1936
1943
  originalName
1937
1944
  }),
1938
- messageId: "satisfyCustom",
1945
+ messageId: pickMessageId("satisfyCustom", isObjectStyleEnumName),
1939
1946
  node
1940
1947
  });
1941
1948
  return false;
1942
1949
  }
1943
- function validatePredefinedFormat({ name, config, modifiers, node, originalName }) {
1950
+ function validatePredefinedFormat({ name, config, isObjectStyleEnumName, modifiers, node, originalName }) {
1944
1951
  const formats = config.format;
1945
1952
  if (!formats || formats.length === 0) return true;
1946
1953
  if (!modifiers.has(Modifier.requiresQuotes)) for (const format of formats) {
@@ -1953,12 +1960,26 @@ function createValidator(type, context, allConfigs) {
1953
1960
  originalName,
1954
1961
  processedName: name
1955
1962
  }),
1956
- messageId: originalName === name ? "doesNotMatchFormat" : "doesNotMatchFormatTrimmed",
1963
+ messageId: pickMessageId(originalName === name ? "doesNotMatchFormat" : "doesNotMatchFormatTrimmed", isObjectStyleEnumName),
1957
1964
  node
1958
1965
  });
1959
1966
  return false;
1960
1967
  }
1961
1968
  }
1969
+ /**
1970
+ * Picks the `*ForeignContract` variant of a message id when the name belongs
1971
+ * to an `objectStyleEnum` - a plain object literal, not a real enum, so the
1972
+ * violation message should point at the `satisfies` escape instead of a
1973
+ * rename.
1974
+ *
1975
+ * @param baseMessageId - The base message id for the violation kind.
1976
+ * @param isObjectStyleEnumName - Whether the name is an objectStyleEnum
1977
+ * container or key.
1978
+ * @returns The message id to report.
1979
+ */
1980
+ function pickMessageId(baseMessageId, isObjectStyleEnumName) {
1981
+ return isObjectStyleEnumName ? `${baseMessageId}ForeignContract` : baseMessageId;
1982
+ }
1962
1983
  const SelectorsAllowedToHaveTypes = Selector.variable | Selector.function | Selector.parameter | Selector.classProperty | Selector.objectLiteralProperty | Selector.typeProperty | Selector.parameterProperty | Selector.classicAccessor | Selector.classMethod | Selector.objectLiteralMethod | Selector.typeMethod;
1963
1984
  function isAllTypesMatch(type, callback) {
1964
1985
  if (type.isUnion()) return type.types.every((inner) => callback(inner));
@@ -2295,6 +2316,7 @@ const SCHEMA = {
2295
2316
  ...selectorSchema("variableLike", false, ["unused", "async"]),
2296
2317
  ...selectorSchema("variable", true, [
2297
2318
  "const",
2319
+ "constAsserted",
2298
2320
  "destructured",
2299
2321
  "exported",
2300
2322
  "global",
@@ -2437,14 +2459,21 @@ const SCHEMA = {
2437
2459
  };
2438
2460
  //#endregion
2439
2461
  //#region src/rules/naming-convention/rule.ts
2440
- const RULE_NAME$11 = "naming-convention";
2441
- const messages$11 = {
2462
+ const RULE_NAME$12 = "naming-convention";
2463
+ const FOREIGN_CONTRACT_HINT = " If this is data conforming to an external shape, declare it with `satisfies` instead.";
2464
+ const messages$12 = {
2442
2465
  doesNotMatchFormat: "{{type}} name `{{name}}` must match one of the following formats: {{formats}}",
2466
+ doesNotMatchFormatForeignContract: `{{type}} name \`{{name}}\` must match one of the following formats: {{formats}}${FOREIGN_CONTRACT_HINT}`,
2443
2467
  doesNotMatchFormatTrimmed: "{{type}} name `{{name}}` trimmed as `{{processedName}}` must match one of the following formats: {{formats}}",
2468
+ doesNotMatchFormatTrimmedForeignContract: `{{type}} name \`{{name}}\` trimmed as \`{{processedName}}\` must match one of the following formats: {{formats}}${FOREIGN_CONTRACT_HINT}`,
2444
2469
  missingAffix: "{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}",
2470
+ missingAffixForeignContract: `{{type}} name \`{{name}}\` must have one of the following {{position}}es: {{affixes}}${FOREIGN_CONTRACT_HINT}`,
2445
2471
  missingUnderscore: "{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).",
2472
+ missingUnderscoreForeignContract: `{{type}} name \`{{name}}\` must have {{count}} {{position}} underscore(s).${FOREIGN_CONTRACT_HINT}`,
2446
2473
  satisfyCustom: "{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}",
2447
- unexpectedUnderscore: "{{type}} name `{{name}}` must not have a {{position}} underscore."
2474
+ satisfyCustomForeignContract: `{{type}} name \`{{name}}\` must {{regexMatch}} the RegExp: {{regex}}${FOREIGN_CONTRACT_HINT}`,
2475
+ unexpectedUnderscore: "{{type}} name `{{name}}` must not have a {{position}} underscore.",
2476
+ unexpectedUnderscoreForeignContract: `{{type}} name \`{{name}}\` must not have a {{position}} underscore.${FOREIGN_CONTRACT_HINT}`
2448
2477
  };
2449
2478
  const camelCaseNamingConfig = [
2450
2479
  {
@@ -2525,12 +2554,19 @@ function create$4(contextWithoutDefaults) {
2525
2554
  ":not(ObjectPattern) > Property[computed = false][kind = \"init\"][value.type != \"ArrowFunctionExpression\"][value.type != \"FunctionExpression\"][value.type != \"TSEmptyBodyFunctionExpression\"]": {
2526
2555
  handler: (node, validator) => {
2527
2556
  if (isDictatedByContextualType(node, services, contextualTypeCache)) return;
2557
+ if (isObjectStyleEnumKey(node)) {
2558
+ const enumModifiers = /* @__PURE__ */ new Set();
2559
+ if (requiresQuoting(node.key, compilerOptions.target)) enumModifiers.add(Modifier.requiresQuotes);
2560
+ validators.enumMember(node.key, enumModifiers, true);
2561
+ return;
2562
+ }
2528
2563
  handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
2529
2564
  },
2530
2565
  validator: validators.objectLiteralProperty
2531
2566
  },
2532
2567
  [["TSMethodSignature[computed = false]", "TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type = \"TSFunctionType\"]"].join(", ")]: {
2533
2568
  handler: (node, validator) => {
2569
+ if (isExternalMember(node, context.sourceCode)) return;
2534
2570
  handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
2535
2571
  },
2536
2572
  validator: validators.typeMethod
@@ -2678,6 +2714,7 @@ function create$4(contextWithoutDefaults) {
2678
2714
  },
2679
2715
  "TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type != \"TSFunctionType\"]": {
2680
2716
  handler: (node, validator) => {
2717
+ if (isExternalMember(node, context.sourceCode)) return;
2681
2718
  const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
2682
2719
  if (node.readonly) modifiers.add(Modifier.readonly);
2683
2720
  handleMember(validator, node, modifiers);
@@ -2707,9 +2744,10 @@ function create$4(contextWithoutDefaults) {
2707
2744
  handler: (node, validator) => {
2708
2745
  const identifiers = getIdentifiersFromPattern(node.id);
2709
2746
  const baseModifiers = /* @__PURE__ */ new Set();
2710
- const { parent } = node;
2747
+ const { init, parent } = node;
2711
2748
  if (parent.kind === "const") baseModifiers.add(Modifier.const);
2712
2749
  if (isGlobal(context.sourceCode.getScope(node))) baseModifiers.add(Modifier.global);
2750
+ if (isConstAssertedInitializer(init)) baseModifiers.add(Modifier.constAsserted);
2713
2751
  const isObjectStyleEnum = isObjectStyleEnumDeclaration(node, parent);
2714
2752
  for (const id of identifiers) {
2715
2753
  const modifiers = new Set(baseModifiers);
@@ -2718,7 +2756,7 @@ function create$4(contextWithoutDefaults) {
2718
2756
  if (isExported(parent, id.name, scope)) modifiers.add(Modifier.exported);
2719
2757
  if (isUnused(id.name, scope)) modifiers.add(Modifier.unused);
2720
2758
  if (isAsyncVariableIdentifier(id)) modifiers.add(Modifier.async);
2721
- if (isObjectStyleEnum) validators.objectStyleEnum(id, modifiers);
2759
+ if (isObjectStyleEnum) validators.objectStyleEnum(id, modifiers, true);
2722
2760
  else validator(id, modifiers);
2723
2761
  }
2724
2762
  },
@@ -2732,7 +2770,7 @@ function create$4(contextWithoutDefaults) {
2732
2770
  }));
2733
2771
  }
2734
2772
  const namingConvention = createEslintRule({
2735
- name: RULE_NAME$11,
2773
+ name: RULE_NAME$12,
2736
2774
  create: create$4,
2737
2775
  defaultOptions: camelCaseNamingConfig,
2738
2776
  meta: {
@@ -2743,7 +2781,7 @@ const namingConvention = createEslintRule({
2743
2781
  },
2744
2782
  fixable: void 0,
2745
2783
  hasSuggestions: false,
2746
- messages: messages$11,
2784
+ messages: messages$12,
2747
2785
  schema: SCHEMA,
2748
2786
  type: "suggestion"
2749
2787
  }
@@ -2827,23 +2865,145 @@ function isGlobal(scope) {
2827
2865
  return scope.type === TSESLint.Scope.ScopeType.global || scope.type === TSESLint.Scope.ScopeType.module;
2828
2866
  }
2829
2867
  /**
2830
- * Determines if a VariableDeclarator represents an object-style enum declaration.
2868
+ * Checks whether a type annotation is the bare `const` type reference used by
2869
+ * const assertions (`as const`, `<const>`).
2870
+ *
2871
+ * @param typeAnnotation - The type node to check.
2872
+ * @returns True if the type node is the `const` reference.
2873
+ */
2874
+ function isConstTypeReference(typeAnnotation) {
2875
+ return typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier && typeAnnotation.typeName.name === "const";
2876
+ }
2877
+ /**
2878
+ * Unwraps a *bare* const-asserted object expression - `{...} as const` or
2879
+ * `<const>{...}` - directly, with no `satisfies` wrapper. A `satisfies`
2880
+ * wrapper is a distinct, intentional escape hatch (declaring conformance to
2881
+ * an externally-owned type) and must never be treated as the same thing by
2882
+ * this helper; callers that also want to recognize the `satisfies`-wrapped
2883
+ * form unwrap it themselves first (see `isConstAssertedInitializer`).
2831
2884
  *
2832
- * Object-style enums are const assertions applied to object expressions, commonly
2833
- * used as an alternative to TypeScript enums. Examples:
2885
+ * @param expression - The expression to unwrap.
2886
+ * @returns The inner object expression, or undefined if `expression` isn't a
2887
+ * bare const-asserted object literal.
2888
+ */
2889
+ function getConstAssertedObject(expression) {
2890
+ if ((expression.type === AST_NODE_TYPES.TSAsExpression || expression.type === AST_NODE_TYPES.TSTypeAssertion) && isConstTypeReference(expression.typeAnnotation) && expression.expression.type === AST_NODE_TYPES.ObjectExpression) return expression.expression;
2891
+ }
2892
+ /**
2893
+ * Determines if a VariableDeclarator represents an object-style enum
2894
+ * declaration.
2895
+ *
2896
+ * Object-style enums are *bare* const assertions applied to object
2897
+ * expressions, commonly used as an alternative to TypeScript enums. Examples:
2834
2898
  * - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
2835
- * - `const Status = <const>{ OK: 200, ERROR: 500 }`.
2899
+ * - `const Status = <const>{ OK: 200, ERROR: 500 }`
2900
+ * - `const Config: Options = { RED: 'red' } as const` (a type annotation on
2901
+ * the binding doesn't opt out - `satisfies` is the intended escape).
2902
+ *
2903
+ * A const assertion wrapped in `satisfies` (`{...} as const satisfies T`) is
2904
+ * explicitly excluded: it declares conformance to an externally-owned type,
2905
+ * which is the foreign-contract escape hatch this rule wants authors to
2906
+ * reach for instead of renaming keys to fit.
2836
2907
  *
2837
2908
  * @param node - The VariableDeclarator AST node to check.
2838
2909
  * @param parent - The parent VariableDeclaration node.
2839
2910
  * @returns True if this represents an object-style enum declaration.
2840
2911
  */
2841
2912
  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;
2913
+ if (parent.kind !== "const" || !node.init) return false;
2914
+ if (node.init.type === AST_NODE_TYPES.TSSatisfiesExpression) return false;
2915
+ return getConstAssertedObject(node.init) !== void 0;
2916
+ }
2917
+ /**
2918
+ * True when `init` is a const-asserted object expression, bare (`{...} as
2919
+ * const`) or `satisfies`-wrapped (`{...} as const satisfies T`). Used for the
2920
+ * `constAsserted` variable modifier, which - unlike `isObjectStyleEnumDeclaration`
2921
+ * - deliberately covers both forms: it exists so consumers can pin a format on
2922
+ * const-asserted data objects regardless of whether they carry a `satisfies`
2923
+ * clause.
2924
+ *
2925
+ * @param init - The VariableDeclarator's initializer, if any.
2926
+ * @returns True if `init` is a const-asserted object expression.
2927
+ */
2928
+ function isConstAssertedInitializer(init) {
2929
+ if (!init) return false;
2930
+ return getConstAssertedObject(init.type === AST_NODE_TYPES.TSSatisfiesExpression ? init.expression : init) !== void 0;
2931
+ }
2932
+ /**
2933
+ * Determines if an object literal property is a direct key of an
2934
+ * object-style-enum declaration's object literal - i.e. `RED` in `const
2935
+ * Colors = { RED: 'red' } as const`, but not a key of a nested object value.
2936
+ * Nested object values aren't reached because their enclosing ObjectExpression
2937
+ * is parented by a Property, not the const-assertion node this checks for.
2938
+ *
2939
+ * @param node - The non-computed object literal property node.
2940
+ * @returns True if this property is a top-level object-style-enum key.
2941
+ */
2942
+ function isObjectStyleEnumKey(node) {
2943
+ const objectExpression = node.parent;
2944
+ if (objectExpression.type !== AST_NODE_TYPES.ObjectExpression) return false;
2945
+ const assertion = objectExpression.parent;
2946
+ if (assertion.type !== AST_NODE_TYPES.TSAsExpression && assertion.type !== AST_NODE_TYPES.TSTypeAssertion || getConstAssertedObject(assertion) !== objectExpression) return false;
2947
+ const declarator = assertion.parent;
2948
+ return declarator.type === AST_NODE_TYPES.VariableDeclarator && declarator.parent.kind === "const";
2949
+ }
2950
+ const EXTERNAL_JSDOC_TAG_PATTERN = /@external\b/u;
2951
+ /**
2952
+ * Resolves the node whose leading comments should be inspected for JSDoc -
2953
+ * for an exported declaration, the JSDoc block precedes the `export` keyword,
2954
+ * not the declaration itself.
2955
+ *
2956
+ * @param node - The declaration node.
2957
+ * @returns The export wrapper if present, otherwise `node` itself.
2958
+ */
2959
+ function getJsDocumentTargetNode(node) {
2960
+ const { parent } = node;
2961
+ if (parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) return parent;
2962
+ return node;
2963
+ }
2964
+ /**
2965
+ * Checks whether `node`'s immediately preceding JSDoc block comment carries
2966
+ * an `@external` tag, marking the name(s) it documents as coming from a
2967
+ * foreign wire format.
2968
+ *
2969
+ * @param node - The node to check.
2970
+ * @param sourceCode - Used to read the comments preceding `node`.
2971
+ * @returns True if the leading JSDoc comment contains `@external`.
2972
+ */
2973
+ function hasExternalJsDocumentTag(node, sourceCode) {
2974
+ const lastComment = sourceCode.getCommentsBefore(getJsDocumentTargetNode(node)).at(-1);
2975
+ return lastComment?.type === AST_TOKEN_TYPES.Block && lastComment.value.startsWith("*") && EXTERNAL_JSDOC_TAG_PATTERN.test(lastComment.value);
2976
+ }
2977
+ /**
2978
+ * Walks up from a type member to the nearest enclosing interface or type
2979
+ * alias declaration, passing through any nested type literals along the way
2980
+ * - foreign formats nest, so an `@external` tag on the outer declaration
2981
+ * covers members at any depth.
2982
+ *
2983
+ * @param node - The member node to walk up from.
2984
+ * @returns The enclosing declaration, or undefined if none is found.
2985
+ */
2986
+ function findEnclosingTypeDeclaration(node) {
2987
+ let current = node.parent;
2988
+ while (current) {
2989
+ if (current.type === AST_NODE_TYPES.TSInterfaceDeclaration || current.type === AST_NODE_TYPES.TSTypeAliasDeclaration) return current;
2990
+ current = current.parent;
2991
+ }
2992
+ }
2993
+ /**
2994
+ * Determines if a typeProperty/typeMethod member should skip naming
2995
+ * validation because it (or its enclosing interface/type-alias declaration)
2996
+ * is marked `@external` - a foreign wire-format name, not the author's
2997
+ * choice. The type's own name (validated via `typeLike`) is unaffected.
2998
+ *
2999
+ * @param node - The typeProperty/typeMethod node to check.
3000
+ * @param sourceCode - Used to read leading comments for the JSDoc check.
3001
+ * @returns True if the member should be skipped.
3002
+ */
3003
+ function isExternalMember(node, sourceCode) {
3004
+ if (hasExternalJsDocumentTag(node, sourceCode)) return true;
3005
+ const enclosing = findEnclosingTypeDeclaration(node);
3006
+ return enclosing !== void 0 && hasExternalJsDocumentTag(enclosing, sourceCode);
2847
3007
  }
2848
3008
  function isValidIdentifierText(name, languageVersion) {
2849
3009
  if (name.length === 0) return false;
@@ -2862,9 +3022,9 @@ function requiresQuoting(node, target) {
2862
3022
  }
2863
3023
  //#endregion
2864
3024
  //#region src/rules/no-export-default-arrow/rule.ts
2865
- const RULE_NAME$10 = "no-export-default-arrow";
3025
+ const RULE_NAME$11 = "no-export-default-arrow";
2866
3026
  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." };
3027
+ const messages$11 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
2868
3028
  /**
2869
3029
  * Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
2870
3030
  * word separators (`use-mouse` -> `useMouse`).
@@ -2942,7 +3102,7 @@ function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCo
2942
3102
  * @param context - The rule context.
2943
3103
  * @returns The rule listener.
2944
3104
  */
2945
- function createOnce$6(context) {
3105
+ function createOnce$7(context) {
2946
3106
  return { ArrowFunctionExpression(node) {
2947
3107
  const { parent } = node;
2948
3108
  if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
@@ -2959,8 +3119,8 @@ function createOnce$6(context) {
2959
3119
  } };
2960
3120
  }
2961
3121
  const noExportDefaultArrow = createFlawlessRule({
2962
- name: RULE_NAME$10,
2963
- createOnce: createOnce$6,
3122
+ name: RULE_NAME$11,
3123
+ createOnce: createOnce$7,
2964
3124
  defaultOptions: [],
2965
3125
  meta: {
2966
3126
  docs: {
@@ -2970,7 +3130,7 @@ const noExportDefaultArrow = createFlawlessRule({
2970
3130
  },
2971
3131
  fixable: "code",
2972
3132
  hasSuggestions: false,
2973
- messages: messages$10,
3133
+ messages: messages$11,
2974
3134
  schema: [],
2975
3135
  type: "suggestion"
2976
3136
  }
@@ -3135,7 +3295,7 @@ function flatten(file, context) {
3135
3295
  }
3136
3296
  //#endregion
3137
3297
  //#region src/rules/no-redundant-tsconfig-options/rule.ts
3138
- const RULE_NAME$9 = "no-redundant-tsconfig-options";
3298
+ const RULE_NAME$10 = "no-redundant-tsconfig-options";
3139
3299
  const MESSAGE_ID$5 = "redundant";
3140
3300
  /**
3141
3301
  * Compiler options TypeScript resolves case-insensitively; a child that re-sets
@@ -3171,7 +3331,7 @@ const PATH_KEYS = /* @__PURE__ */ new Set([
3171
3331
  "tsBuildInfoFile",
3172
3332
  "typeRoots"
3173
3333
  ]);
3174
- const messages$9 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
3334
+ const messages$10 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
3175
3335
  /**
3176
3336
  * Structural equality between two static JSON values. Strings compare
3177
3337
  * case-insensitively when `caseInsensitive` is set (used for enum-valued
@@ -3343,7 +3503,7 @@ function create$3(context) {
3343
3503
  } };
3344
3504
  }
3345
3505
  const noRedundantTsconfigOptions = createEslintRule({
3346
- name: RULE_NAME$9,
3506
+ name: RULE_NAME$10,
3347
3507
  create: create$3,
3348
3508
  defaultOptions: [],
3349
3509
  meta: {
@@ -3354,7 +3514,7 @@ const noRedundantTsconfigOptions = createEslintRule({
3354
3514
  },
3355
3515
  fixable: "code",
3356
3516
  hasSuggestions: false,
3357
- messages: messages$9,
3517
+ messages: messages$10,
3358
3518
  schema: [],
3359
3519
  type: "suggestion"
3360
3520
  }
@@ -3597,15 +3757,15 @@ function resolveFactory(sourceCode, node) {
3597
3757
  }
3598
3758
  //#endregion
3599
3759
  //#region src/rules/no-unnecessary-use-callback/rule.ts
3600
- const RULE_NAME$8 = "no-unnecessary-use-callback";
3760
+ const RULE_NAME$9 = "no-unnecessary-use-callback";
3601
3761
  const MESSAGE_ID_DEFAULT$1 = "default";
3602
3762
  const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
3603
- const messages$8 = {
3763
+ const messages$9 = {
3604
3764
  [MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
3605
3765
  [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."
3606
3766
  };
3607
3767
  const noUnnecessaryUseCallback = createFlawlessRule({
3608
- name: RULE_NAME$8,
3768
+ name: RULE_NAME$9,
3609
3769
  createOnce: createUnnecessaryHookRule({
3610
3770
  hook: "useCallback",
3611
3771
  messageIds: {
@@ -3621,22 +3781,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
3621
3781
  requiresTypeChecking: false
3622
3782
  },
3623
3783
  hasSuggestions: false,
3624
- messages: messages$8,
3784
+ messages: messages$9,
3625
3785
  schema: [],
3626
3786
  type: "suggestion"
3627
3787
  }
3628
3788
  });
3629
3789
  //#endregion
3630
3790
  //#region src/rules/no-unnecessary-use-memo/rule.ts
3631
- const RULE_NAME$7 = "no-unnecessary-use-memo";
3791
+ const RULE_NAME$8 = "no-unnecessary-use-memo";
3632
3792
  const MESSAGE_ID_DEFAULT = "default";
3633
3793
  const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
3634
- const messages$7 = {
3794
+ const messages$8 = {
3635
3795
  [MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
3636
3796
  [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."
3637
3797
  };
3638
3798
  const noUnnecessaryUseMemo = createFlawlessRule({
3639
- name: RULE_NAME$7,
3799
+ name: RULE_NAME$8,
3640
3800
  createOnce: createUnnecessaryHookRule({
3641
3801
  hook: "useMemo",
3642
3802
  messageIds: {
@@ -3652,16 +3812,16 @@ const noUnnecessaryUseMemo = createFlawlessRule({
3652
3812
  requiresTypeChecking: false
3653
3813
  },
3654
3814
  hasSuggestions: false,
3655
- messages: messages$7,
3815
+ messages: messages$8,
3656
3816
  schema: [],
3657
3817
  type: "suggestion"
3658
3818
  }
3659
3819
  });
3660
3820
  //#endregion
3661
3821
  //#region src/rules/padding-after-expect-assertions/rule.ts
3662
- const RULE_NAME$6 = "padding-after-expect-assertions";
3822
+ const RULE_NAME$7 = "padding-after-expect-assertions";
3663
3823
  const MESSAGE_ID$4 = "missingPadding";
3664
- const messages$6 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
3824
+ const messages$7 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
3665
3825
  /**
3666
3826
  * Matches a statement that declares the expected assertion count at the top of
3667
3827
  * a test: `expect.assertions(n)` or `expect.hasAssertions()`.
@@ -3736,7 +3896,7 @@ function findPaddingAnchor(sourceCode, node, nextNode) {
3736
3896
  * @param context - The rule context.
3737
3897
  * @returns The rule listener.
3738
3898
  */
3739
- function createOnce$3(context) {
3899
+ function createOnce$4(context) {
3740
3900
  return { ExpressionStatement(node) {
3741
3901
  const name = getAssertionName(node);
3742
3902
  if (name === void 0) return;
@@ -3761,8 +3921,8 @@ function createOnce$3(context) {
3761
3921
  } };
3762
3922
  }
3763
3923
  const paddingAfterExpectAssertions = createFlawlessRule({
3764
- name: RULE_NAME$6,
3765
- createOnce: createOnce$3,
3924
+ name: RULE_NAME$7,
3925
+ createOnce: createOnce$4,
3766
3926
  defaultOptions: [],
3767
3927
  meta: {
3768
3928
  docs: {
@@ -3772,16 +3932,16 @@ const paddingAfterExpectAssertions = createFlawlessRule({
3772
3932
  },
3773
3933
  fixable: "whitespace",
3774
3934
  hasSuggestions: false,
3775
- messages: messages$6,
3935
+ messages: messages$7,
3776
3936
  schema: [],
3777
3937
  type: "layout"
3778
3938
  }
3779
3939
  });
3780
3940
  //#endregion
3781
3941
  //#region src/rules/prefer-destructuring-assignment/rule.ts
3782
- const RULE_NAME$5 = "prefer-destructuring-assignment";
3942
+ const RULE_NAME$6 = "prefer-destructuring-assignment";
3783
3943
  const MESSAGE_ID$3 = "default";
3784
- const messages$5 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
3944
+ const messages$6 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
3785
3945
  /**
3786
3946
  * Identifiers that cannot be used as a binding name in strict-mode module code.
3787
3947
  * A property may be accessed with such a name (`props.default`), but a shorthand
@@ -3943,7 +4103,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
3943
4103
  * @param context - The rule context.
3944
4104
  * @returns The rule listener.
3945
4105
  */
3946
- function createOnce$2(context) {
4106
+ function createOnce$3(context) {
3947
4107
  let collector = core.getFunctionComponentCollector(context);
3948
4108
  const selectorKeys = Object.keys(collector.visitor);
3949
4109
  const listener = {
@@ -3969,8 +4129,8 @@ function createOnce$2(context) {
3969
4129
  return listener;
3970
4130
  }
3971
4131
  const preferDestructuringAssignment = createFlawlessRule({
3972
- name: RULE_NAME$5,
3973
- createOnce: createOnce$2,
4132
+ name: RULE_NAME$6,
4133
+ createOnce: createOnce$3,
3974
4134
  defaultOptions: [],
3975
4135
  meta: {
3976
4136
  docs: {
@@ -3980,16 +4140,16 @@ const preferDestructuringAssignment = createFlawlessRule({
3980
4140
  },
3981
4141
  fixable: "code",
3982
4142
  hasSuggestions: false,
3983
- messages: messages$5,
4143
+ messages: messages$6,
3984
4144
  schema: [],
3985
4145
  type: "problem"
3986
4146
  }
3987
4147
  });
3988
4148
  //#endregion
3989
4149
  //#region src/rules/prefer-parameter-destructuring/rule.ts
3990
- const RULE_NAME$4 = "prefer-parameter-destructuring";
4150
+ const RULE_NAME$5 = "prefer-parameter-destructuring";
3991
4151
  const MESSAGE_ID$2 = "default";
3992
- const messages$4 = { [MESSAGE_ID$2]: "Destructure parameter '{{name}}' in the function signature instead of the body." };
4152
+ const messages$5 = { [MESSAGE_ID$2]: "Destructure parameter '{{name}}' in the function signature instead of the body." };
3993
4153
  /**
3994
4154
  * Collects the binding names introduced by a pattern (or parameter), including
3995
4155
  * names nested in object/array patterns, defaults, and rest elements.
@@ -4264,7 +4424,7 @@ function planFix(query) {
4264
4424
  * @param context - The rule context.
4265
4425
  * @returns The rule listener.
4266
4426
  */
4267
- function createOnce$1(context) {
4427
+ function createOnce$2(context) {
4268
4428
  let allowSideEffectReordering = true;
4269
4429
  function checkFunction(node) {
4270
4430
  const { body, params } = node;
@@ -4315,8 +4475,8 @@ function createOnce$1(context) {
4315
4475
  };
4316
4476
  }
4317
4477
  const preferParameterDestructuring = createFlawlessRule({
4318
- name: RULE_NAME$4,
4319
- createOnce: createOnce$1,
4478
+ name: RULE_NAME$5,
4479
+ createOnce: createOnce$2,
4320
4480
  defaultOptions: [{ allowSideEffectReordering: true }],
4321
4481
  meta: {
4322
4482
  defaultOptions: [{ allowSideEffectReordering: true }],
@@ -4327,7 +4487,7 @@ const preferParameterDestructuring = createFlawlessRule({
4327
4487
  },
4328
4488
  fixable: "code",
4329
4489
  hasSuggestions: false,
4330
- messages: messages$4,
4490
+ messages: messages$5,
4331
4491
  schema: [{
4332
4492
  additionalProperties: false,
4333
4493
  properties: { allowSideEffectReordering: {
@@ -4609,9 +4769,9 @@ function isReadonlyPropertiesProperty(checker, type, property) {
4609
4769
  }
4610
4770
  //#endregion
4611
4771
  //#region src/rules/prefer-read-only-props/rule.ts
4612
- const RULE_NAME$3 = "prefer-read-only-props";
4772
+ const RULE_NAME$4 = "prefer-read-only-props";
4613
4773
  const DEFAULT_WRAPPER_TYPE = "Readonly";
4614
- const messages$3 = { preferReadOnlyProps: "A function component's props should be read-only." };
4774
+ const messages$4 = { preferReadOnlyProps: "A function component's props should be read-only." };
4615
4775
  /** Names of the `React.forwardRef` wrapper whose props are its second type argument. */
4616
4776
  const FORWARD_REF_NAMES = /* @__PURE__ */ new Set(["forwardRef"]);
4617
4777
  /** Names of the `React.memo` wrapper whose props are its first type argument. */
@@ -4820,7 +4980,7 @@ function create$2(context) {
4820
4980
  };
4821
4981
  }
4822
4982
  const preferReadOnlyProps = createEslintRule({
4823
- name: RULE_NAME$3,
4983
+ name: RULE_NAME$4,
4824
4984
  create: create$2,
4825
4985
  defaultOptions: [{}],
4826
4986
  meta: {
@@ -4832,7 +4992,7 @@ const preferReadOnlyProps = createEslintRule({
4832
4992
  },
4833
4993
  fixable: "code",
4834
4994
  hasSuggestions: false,
4835
- messages: messages$3,
4995
+ messages: messages$4,
4836
4996
  schema: [{
4837
4997
  additionalProperties: false,
4838
4998
  properties: {
@@ -4857,7 +5017,7 @@ const preferReadOnlyProps = createEslintRule({
4857
5017
  });
4858
5018
  //#endregion
4859
5019
  //#region src/rules/purity/rule.ts
4860
- const RULE_NAME$2 = "purity";
5020
+ const RULE_NAME$3 = "purity";
4861
5021
  const MESSAGE_ID$1 = "impureCall";
4862
5022
  /**
4863
5023
  * Non-deterministic Luau / Roblox calls, written as dotted paths matching how
@@ -4890,7 +5050,7 @@ const GUARDED_GLOBALS = /* @__PURE__ */ new Set([
4890
5050
  "tick",
4891
5051
  "time"
4892
5052
  ]);
4893
- const messages$2 = { [MESSAGE_ID$1]: "Do not call '{{name}}' during render. Components and hooks must be pure. Move this call into an event handler, effect, or state initializer." };
5053
+ const messages$3 = { [MESSAGE_ID$1]: "Do not call '{{name}}' during render. Components and hooks must be pure. Move this call into an event handler, effect, or state initializer." };
4894
5054
  const schema$1 = [{
4895
5055
  additionalProperties: false,
4896
5056
  properties: {
@@ -4990,7 +5150,7 @@ function isRenderContext(reactContext, func) {
4990
5150
  const { parent } = func;
4991
5151
  return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments[0] === func && core.isUseMemoCall(reactContext, parent);
4992
5152
  }
4993
- function createOnce(context) {
5153
+ function createOnce$1(context) {
4994
5154
  const reactContext = context;
4995
5155
  let signatures;
4996
5156
  function handle(node, match, path) {
@@ -5023,8 +5183,8 @@ function createOnce(context) {
5023
5183
  };
5024
5184
  }
5025
5185
  const purity = createFlawlessRule({
5026
- name: RULE_NAME$2,
5027
- createOnce,
5186
+ name: RULE_NAME$3,
5187
+ createOnce: createOnce$1,
5028
5188
  defaultOptions: [{}],
5029
5189
  meta: {
5030
5190
  defaultOptions: [{}],
@@ -5034,12 +5194,314 @@ const purity = createFlawlessRule({
5034
5194
  requiresTypeChecking: false
5035
5195
  },
5036
5196
  hasSuggestions: false,
5037
- messages: messages$2,
5197
+ messages: messages$3,
5038
5198
  schema: schema$1,
5039
5199
  type: "problem"
5040
5200
  }
5041
5201
  });
5042
5202
  //#endregion
5203
+ //#region src/rules/react-namespace/imports.ts
5204
+ /**
5205
+ * Whether a module specifier refers to the configured React import source. Matches
5206
+ * the exact source or any subpath (`@rbxts/react` and `@rbxts/react/jsx-runtime`).
5207
+ *
5208
+ * @param value - The `source.value` of an import declaration.
5209
+ * @param importSource - The configured React import source.
5210
+ * @returns True when the specifier resolves to the React package.
5211
+ */
5212
+ function matchesSource(value, importSource) {
5213
+ return value === importSource || value.startsWith(`${importSource}/`);
5214
+ }
5215
+ /**
5216
+ * Collects the import declarations in a program that come from the React source.
5217
+ *
5218
+ * @param program - The program node whose top-level statements are scanned.
5219
+ * @param importSource - The configured React import source.
5220
+ * @returns The matching import declarations, in source order.
5221
+ */
5222
+ function getReactImportDeclarations(program, importSource) {
5223
+ return program.body.filter((statement) => {
5224
+ return statement.type === AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && matchesSource(statement.source.value, importSource);
5225
+ });
5226
+ }
5227
+ /**
5228
+ * Classifies an import specifier by the kind of React binding it introduces:
5229
+ * named for `ImportSpecifier`, namespace for default and namespace specifiers.
5230
+ *
5231
+ * @param specifier - The specifier to classify.
5232
+ * @returns `"named"` or `"namespace"`.
5233
+ */
5234
+ function classifySpecifier(specifier) {
5235
+ return specifier.type === AST_NODE_TYPES.ImportSpecifier ? "named" : "namespace";
5236
+ }
5237
+ /**
5238
+ * Finds the local name of a default or namespace React import, used to qualify a
5239
+ * bare type reference (`React.ReactNode`). Returns `null` when no such import
5240
+ * exists, in which case the caller ensures one under the name `React`.
5241
+ *
5242
+ * @param declarations - The React import declarations of the file.
5243
+ * @returns The local namespace name, or `null`.
5244
+ */
5245
+ function findReactNamespaceLocal(declarations) {
5246
+ for (const declaration of declarations) for (const specifier of declaration.specifiers) if (specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier) return specifier.local.name;
5247
+ return null;
5248
+ }
5249
+ /**
5250
+ * Whether the file already imports React as a default or namespace binding (so a
5251
+ * namespace type qualification resolves without adding an import).
5252
+ *
5253
+ * @param declarations - The React import declarations of the file.
5254
+ * @returns True when a default or namespace React import is present.
5255
+ */
5256
+ function hasReactNamespaceImport(declarations) {
5257
+ return findReactNamespaceLocal(declarations) !== null;
5258
+ }
5259
+ /**
5260
+ * Inserts a fresh import statement above the first statement of the program.
5261
+ *
5262
+ * @param fixer - The fixer used to build the edit.
5263
+ * @param program - The program whose head receives the import.
5264
+ * @param text - The full import statement (including trailing newline).
5265
+ * @returns The insertion fix.
5266
+ */
5267
+ function insertImportStatement(fixer, program, text) {
5268
+ const first = program.body.at(0);
5269
+ if (first === void 0) return fixer.insertTextAfterRange([0, 0], text);
5270
+ return fixer.insertTextBefore(first, text);
5271
+ }
5272
+ /**
5273
+ * Builds the fix that ensures a named value import of `name` from the React
5274
+ * source, or `null` when it is already present. Prefers extending an existing
5275
+ * named import, then appending to a default import, and otherwise inserts a new
5276
+ * statement. Never touched for namespace-only imports, which cannot carry named
5277
+ * siblings and instead receive a fresh statement.
5278
+ *
5279
+ * @param fixer - The fixer used to build the edit.
5280
+ * @param parameters - The declarations, program, name, and source to import.
5281
+ * @returns The fix, or `null` when the import already exists.
5282
+ */
5283
+ function ensureNamedValueImport(fixer, { name, declarations, importSource, program }) {
5284
+ const valueDeclarations = declarations.filter((declaration) => declaration.importKind !== "type");
5285
+ for (const declaration of valueDeclarations) for (const specifier of declaration.specifiers) if (isNamedSpecifier(specifier) && specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === name) return null;
5286
+ for (const declaration of valueDeclarations) {
5287
+ const last = declaration.specifiers.filter(isNamedSpecifier).at(-1);
5288
+ if (last !== void 0) return fixer.insertTextAfter(last, `, ${name}`);
5289
+ }
5290
+ for (const declaration of valueDeclarations) {
5291
+ const defaultSpecifier = declaration.specifiers.find((specifier) => specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier);
5292
+ if (defaultSpecifier !== void 0) return fixer.insertTextAfter(defaultSpecifier, `, { ${name} }`);
5293
+ }
5294
+ return insertImportStatement(fixer, program, `import { ${name} } from "${importSource}";\n`);
5295
+ }
5296
+ /**
5297
+ * Removes a single named specifier from a React import, dropping the whole
5298
+ * declaration when it was the only binding. Handles the comma and braces that
5299
+ * surround the specifier so the remaining import stays well-formed.
5300
+ *
5301
+ * @param fixer - The fixer used to build the edit.
5302
+ * @param sourceCode - The source code, used to locate surrounding tokens.
5303
+ * @param declaration - The declaration owning the specifier.
5304
+ * @param specifier - The named specifier to remove.
5305
+ * @returns The removal fix.
5306
+ */
5307
+ function removeNamedSpecifier(fixer, sourceCode, declaration, specifier) {
5308
+ const named = declaration.specifiers.filter(isNamedSpecifier);
5309
+ const others = declaration.specifiers.filter((entry) => !isNamedSpecifier(entry));
5310
+ if (named.length === 1) {
5311
+ const lastOther = others.at(-1);
5312
+ if (lastOther === void 0) return removeWholeDeclaration(fixer, sourceCode, declaration);
5313
+ const comma = sourceCode.getTokenAfter(lastOther, (token) => token.value === ",");
5314
+ const closeBrace = sourceCode.getTokenAfter(specifier, (token) => token.value === "}");
5315
+ if (comma !== null && closeBrace !== null) return fixer.removeRange([comma.range[0], closeBrace.range[1]]);
5316
+ return fixer.remove(specifier);
5317
+ }
5318
+ if (named.indexOf(specifier) === named.length - 1) {
5319
+ const comma = sourceCode.getTokenBefore(specifier, (token) => token.value === ",");
5320
+ if (comma !== null) return fixer.removeRange([comma.range[0], specifier.range[1]]);
5321
+ return fixer.remove(specifier);
5322
+ }
5323
+ const comma = sourceCode.getTokenAfter(specifier, (token) => token.value === ",");
5324
+ if (comma !== null) return fixer.removeRange([specifier.range[0], comma.range[1]]);
5325
+ return fixer.remove(specifier);
5326
+ }
5327
+ /**
5328
+ * Finds the named specifier that binds `name` across the React import
5329
+ * declarations, or `null` when none does.
5330
+ *
5331
+ * @param declarations - The React import declarations of the file.
5332
+ * @param name - The local name of the binding to find.
5333
+ * @returns A specifier together with its declaration, or `null`.
5334
+ */
5335
+ function findNamedSpecifier(declarations, name) {
5336
+ for (const declaration of declarations) for (const specifier of declaration.specifiers) if (isNamedSpecifier(specifier) && specifier.local.name === name) return {
5337
+ declaration,
5338
+ specifier
5339
+ };
5340
+ return null;
5341
+ }
5342
+ function isNamedSpecifier(specifier) {
5343
+ return specifier.type === AST_NODE_TYPES.ImportSpecifier;
5344
+ }
5345
+ /**
5346
+ * Removes a whole import declaration together with the line break that follows
5347
+ * it, so dropping the sole specifier of an import leaves no blank line.
5348
+ *
5349
+ * @param fixer - The fixer used to build the edit.
5350
+ * @param sourceCode - The source code, used to inspect trailing characters.
5351
+ * @param declaration - The declaration to remove.
5352
+ * @returns The removal fix.
5353
+ */
5354
+ function removeWholeDeclaration(fixer, { text }, declaration) {
5355
+ const [start] = declaration.range;
5356
+ let end = declaration.range[1];
5357
+ if (text[end] === "\r") end += 1;
5358
+ if (text[end] === "\n") end += 1;
5359
+ return fixer.removeRange([start, end]);
5360
+ }
5361
+ //#endregion
5362
+ //#region src/rules/react-namespace/rule.ts
5363
+ const RULE_NAME$2 = "react-namespace";
5364
+ const MESSAGE_ID_RUNTIME = "runtimeNamespace";
5365
+ const MESSAGE_ID_TYPE = "typeNamedImport";
5366
+ const DEFAULT_IMPORT_SOURCE = "react";
5367
+ const messages$2 = {
5368
+ [MESSAGE_ID_RUNTIME]: "Use a named import of '{{name}}' instead of accessing it through the React namespace.",
5369
+ [MESSAGE_ID_TYPE]: "Access the '{{name}}' type through the React namespace instead of importing it by name."
5370
+ };
5371
+ /**
5372
+ * Whether a member access is the target of an assignment (`React.x = …`), which
5373
+ * must be left alone — rewriting it to a bare name would create an assignment to
5374
+ * an import binding.
5375
+ *
5376
+ * @param node - The member expression under consideration.
5377
+ * @returns True when the member is an assignment target.
5378
+ */
5379
+ function isAssignmentTarget(node) {
5380
+ return node.parent.type === AST_NODE_TYPES.AssignmentExpression && node.parent.left === node;
5381
+ }
5382
+ /**
5383
+ * Scans a binding's references once, recording whether all of them are bare
5384
+ * type references (`typeName` is the identifier itself) and where the first one
5385
+ * starts. The named import can be removed only when every reference is
5386
+ * convertible, and only the *first* reference carries the import edits.
5387
+ *
5388
+ * The import edits (adding the namespace import, removing the redundant
5389
+ * specifier) sit at the top of the file. ESLint merges a report's fix array into
5390
+ * one edit spanning `[min, max]`, so anchoring those edits to the first
5391
+ * reference keeps that span from the file head to the first usage — later
5392
+ * references stay disjoint and their own qualify edits survive a single pass.
5393
+ *
5394
+ * @param references - The references recorded for the binding.
5395
+ * @returns The convertibility and first-reference facts for the binding.
5396
+ */
5397
+ function computeRemovableInfo(references) {
5398
+ let allConvertible = true;
5399
+ let firstStart = Number.POSITIVE_INFINITY;
5400
+ for (const { identifier } of references) {
5401
+ firstStart = Math.min(firstStart, identifier.range[0]);
5402
+ if (identifier.parent.type !== AST_NODE_TYPES.TSTypeReference || identifier.parent.typeName !== identifier) allConvertible = false;
5403
+ }
5404
+ return {
5405
+ allConvertible,
5406
+ firstStart
5407
+ };
5408
+ }
5409
+ function createOnce(context) {
5410
+ let importSource;
5411
+ let program;
5412
+ let declarations;
5413
+ let sourceCode;
5414
+ let resolutions;
5415
+ let removableInfoByVariable;
5416
+ function getRemovableInfo(variable) {
5417
+ let info = removableInfoByVariable.get(variable);
5418
+ if (info === void 0) {
5419
+ info = computeRemovableInfo(variable.references);
5420
+ removableInfoByVariable.set(variable, info);
5421
+ }
5422
+ return info;
5423
+ }
5424
+ return {
5425
+ before() {
5426
+ ({sourceCode} = context);
5427
+ importSource = context.settings["react-x"]?.importSource ?? DEFAULT_IMPORT_SOURCE;
5428
+ program = sourceCode.ast;
5429
+ declarations = getReactImportDeclarations(program, importSource);
5430
+ resolutions = /* @__PURE__ */ new Map();
5431
+ removableInfoByVariable = /* @__PURE__ */ new Map();
5432
+ for (const declaration of declarations) for (const specifier of declaration.specifiers) {
5433
+ const kind = classifySpecifier(specifier);
5434
+ const [variable] = sourceCode.getDeclaredVariables(specifier);
5435
+ if (variable === void 0) continue;
5436
+ for (const reference of variable.references) resolutions.set(reference.identifier, {
5437
+ kind,
5438
+ variable
5439
+ });
5440
+ }
5441
+ },
5442
+ MemberExpression(node) {
5443
+ if (node.computed || node.object.type !== AST_NODE_TYPES.Identifier || node.property.type !== AST_NODE_TYPES.Identifier || isAssignmentTarget(node)) return;
5444
+ const { object, property } = node;
5445
+ if (resolutions.get(object)?.kind !== "namespace") return;
5446
+ context.report({
5447
+ data: { name: property.name },
5448
+ fix: (fixer) => {
5449
+ const fixes = [fixer.removeRange([object.range[0], property.range[0]])];
5450
+ const ensure = ensureNamedValueImport(fixer, {
5451
+ name: property.name,
5452
+ declarations,
5453
+ importSource,
5454
+ program
5455
+ });
5456
+ if (ensure !== null) fixes.push(ensure);
5457
+ return fixes;
5458
+ },
5459
+ messageId: MESSAGE_ID_RUNTIME,
5460
+ node
5461
+ });
5462
+ },
5463
+ TSTypeReference(node) {
5464
+ if (node.typeName.type !== AST_NODE_TYPES.Identifier) return;
5465
+ const { typeName } = node;
5466
+ const resolution = resolutions.get(typeName);
5467
+ if (resolution?.kind !== "named") return;
5468
+ context.report({
5469
+ data: { name: typeName.name },
5470
+ fix: (fixer) => {
5471
+ const reactLocal = findReactNamespaceLocal(declarations) ?? "React";
5472
+ const fixes = [fixer.insertTextBefore(typeName, `${reactLocal}.`)];
5473
+ const info = getRemovableInfo(resolution.variable);
5474
+ if (info.allConvertible && typeName.range[0] === info.firstStart) {
5475
+ if (!hasReactNamespaceImport(declarations)) fixes.push(insertImportStatement(fixer, program, `import ${reactLocal} from "${importSource}";\n`));
5476
+ const found = findNamedSpecifier(declarations, typeName.name);
5477
+ if (found !== null) fixes.push(removeNamedSpecifier(fixer, sourceCode, found.declaration, found.specifier));
5478
+ }
5479
+ return fixes;
5480
+ },
5481
+ messageId: MESSAGE_ID_TYPE,
5482
+ node
5483
+ });
5484
+ }
5485
+ };
5486
+ }
5487
+ const reactNamespace = createFlawlessRule({
5488
+ name: RULE_NAME$2,
5489
+ createOnce,
5490
+ defaultOptions: [],
5491
+ meta: {
5492
+ docs: {
5493
+ description: "Prefer named imports for React runtime values and the React namespace for React types",
5494
+ recommended: false,
5495
+ requiresTypeChecking: false
5496
+ },
5497
+ fixable: "code",
5498
+ hasSuggestions: false,
5499
+ messages: messages$2,
5500
+ schema: [],
5501
+ type: "problem"
5502
+ }
5503
+ });
5504
+ //#endregion
5043
5505
  //#region src/rules/toml-sort-keys/rule.ts
5044
5506
  const RULE_NAME$1 = "toml-sort-keys";
5045
5507
  const MESSAGE_ID = "unsorted";
@@ -5352,6 +5814,7 @@ const plugin = {
5352
5814
  "prefer-parameter-destructuring": preferParameterDestructuring,
5353
5815
  "prefer-read-only-props": preferReadOnlyProps,
5354
5816
  "purity": purity,
5817
+ "react-namespace": reactNamespace,
5355
5818
  "toml-sort-keys": tomlSortKeys,
5356
5819
  "yaml-block-key-blank-lines": yamlBlockKeyBlankLines
5357
5820
  }