eslint-plugin-flawless 1.0.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.
- package/README.md +1 -0
- package/dist/index.d.mts +53 -27
- package/dist/index.mjs +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/{plugin-DFLOyhkl.mjs → plugin-K2GOCG3Z.mjs} +895 -185
- package/package.json +1 -2
|
@@ -1,21 +1,20 @@
|
|
|
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";
|
|
6
6
|
import { createSyncFn } from "synckit";
|
|
7
7
|
import { RuleCreator, getParserServices } from "@typescript-eslint/utils/eslint-utils";
|
|
8
8
|
import { findVariable, isArrowToken, isOpeningParenToken } from "@typescript-eslint/utils/ast-utils";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import ts9, { ScriptTarget, TypeFlags, isIdentifierPart, isIdentifierStart } from "typescript";
|
|
10
|
+
import { DefinitionType, ImplicitLibVariable, ScopeType, Visitor } from "@typescript-eslint/scope-manager";
|
|
11
11
|
import assert from "node:assert";
|
|
12
12
|
import { getStaticJSONValue, parseForESLint } from "jsonc-eslint-parser";
|
|
13
13
|
import * as core from "@eslint-react/core";
|
|
14
|
-
import ts9 from "typescript";
|
|
15
14
|
import { getStaticTOMLValue } from "toml-eslint-parser";
|
|
16
15
|
//#region package.json
|
|
17
16
|
var name = "eslint-plugin-flawless";
|
|
18
|
-
var version = "1.
|
|
17
|
+
var version = "1.2.0";
|
|
19
18
|
var repository = {
|
|
20
19
|
"url": "git+https://github.com/christopher-buss/eslint-plugin-flawless.git",
|
|
21
20
|
"type": "git"
|
|
@@ -85,7 +84,7 @@ function createFlawlessRule({ createOnce, ...meta }) {
|
|
|
85
84
|
}
|
|
86
85
|
//#endregion
|
|
87
86
|
//#region src/rules/arrow-return-style/rule.ts
|
|
88
|
-
const RULE_NAME$
|
|
87
|
+
const RULE_NAME$16 = "arrow-return-style";
|
|
89
88
|
const IMPLICIT = "useImplicitReturn";
|
|
90
89
|
const EXPLICIT = "useExplicitReturn";
|
|
91
90
|
const COMPLEX_EXPLICIT = "useExplicitReturnComplex";
|
|
@@ -233,7 +232,7 @@ function collectArrows(root) {
|
|
|
233
232
|
return arrows.sort((a, b) => a.range[0] - b.range[0]);
|
|
234
233
|
}
|
|
235
234
|
const arrowReturnStyle = createFlawlessRule({
|
|
236
|
-
name: RULE_NAME$
|
|
235
|
+
name: RULE_NAME$16,
|
|
237
236
|
createOnce(context) {
|
|
238
237
|
let config = DEFAULTS$1;
|
|
239
238
|
let sourceCode;
|
|
@@ -599,10 +598,10 @@ const arrowReturnStyle = createFlawlessRule({
|
|
|
599
598
|
});
|
|
600
599
|
//#endregion
|
|
601
600
|
//#region src/rules/jsx-shorthand-boolean/rule.ts
|
|
602
|
-
const RULE_NAME$
|
|
601
|
+
const RULE_NAME$15 = "jsx-shorthand-boolean";
|
|
603
602
|
const MESSAGE_ID$7 = "setAttributeValue";
|
|
604
|
-
const messages$
|
|
605
|
-
function createOnce$
|
|
603
|
+
const messages$15 = { [MESSAGE_ID$7]: "Set an explicit value for boolean attribute '{{name}}'." };
|
|
604
|
+
function createOnce$10(context) {
|
|
606
605
|
return { JSXAttribute(node) {
|
|
607
606
|
if (node.value !== null) return;
|
|
608
607
|
context.report({
|
|
@@ -614,8 +613,8 @@ function createOnce$9(context) {
|
|
|
614
613
|
} };
|
|
615
614
|
}
|
|
616
615
|
const jsxShorthandBoolean = createFlawlessRule({
|
|
617
|
-
name: RULE_NAME$
|
|
618
|
-
createOnce: createOnce$
|
|
616
|
+
name: RULE_NAME$15,
|
|
617
|
+
createOnce: createOnce$10,
|
|
619
618
|
defaultOptions: [],
|
|
620
619
|
meta: {
|
|
621
620
|
docs: {
|
|
@@ -625,19 +624,19 @@ const jsxShorthandBoolean = createFlawlessRule({
|
|
|
625
624
|
},
|
|
626
625
|
fixable: "code",
|
|
627
626
|
hasSuggestions: false,
|
|
628
|
-
messages: messages$
|
|
627
|
+
messages: messages$15,
|
|
629
628
|
schema: [],
|
|
630
629
|
type: "suggestion"
|
|
631
630
|
}
|
|
632
631
|
});
|
|
633
632
|
//#endregion
|
|
634
633
|
//#region src/rules/jsx-shorthand-fragment/rule.ts
|
|
635
|
-
const RULE_NAME$
|
|
634
|
+
const RULE_NAME$14 = "jsx-shorthand-fragment";
|
|
636
635
|
const MESSAGE_ID_NAMED = "useNamedFragment";
|
|
637
636
|
const MESSAGE_ID_SHORTHAND = "useShorthandFragment";
|
|
638
637
|
const DEFAULT_MODE = "syntax";
|
|
639
638
|
const DEFAULT_FRAGMENT_NAME = "Fragment";
|
|
640
|
-
const messages$
|
|
639
|
+
const messages$14 = {
|
|
641
640
|
[MESSAGE_ID_NAMED]: "Use the '{{name}}' component instead of fragment shorthand syntax.",
|
|
642
641
|
[MESSAGE_ID_SHORTHAND]: "Use the fragment shorthand syntax '<>...</>' instead of the '{{name}}' component."
|
|
643
642
|
};
|
|
@@ -673,7 +672,7 @@ function jsxNameToString(node) {
|
|
|
673
672
|
}
|
|
674
673
|
return null;
|
|
675
674
|
}
|
|
676
|
-
function createOnce$
|
|
675
|
+
function createOnce$9(context) {
|
|
677
676
|
let mode;
|
|
678
677
|
let fragmentName;
|
|
679
678
|
let namedFragments;
|
|
@@ -720,8 +719,8 @@ function createOnce$8(context) {
|
|
|
720
719
|
};
|
|
721
720
|
}
|
|
722
721
|
const jsxShorthandFragment = createFlawlessRule({
|
|
723
|
-
name: RULE_NAME$
|
|
724
|
-
createOnce: createOnce$
|
|
722
|
+
name: RULE_NAME$14,
|
|
723
|
+
createOnce: createOnce$9,
|
|
725
724
|
defaultOptions: [{
|
|
726
725
|
fragmentName: DEFAULT_FRAGMENT_NAME,
|
|
727
726
|
mode: DEFAULT_MODE
|
|
@@ -738,7 +737,7 @@ const jsxShorthandFragment = createFlawlessRule({
|
|
|
738
737
|
},
|
|
739
738
|
fixable: "code",
|
|
740
739
|
hasSuggestions: false,
|
|
741
|
-
messages: messages$
|
|
740
|
+
messages: messages$14,
|
|
742
741
|
schema: schema$3,
|
|
743
742
|
type: "suggestion"
|
|
744
743
|
}
|
|
@@ -878,7 +877,7 @@ function getOpeningParenOfParameters(node, sourceCode) {
|
|
|
878
877
|
}
|
|
879
878
|
//#endregion
|
|
880
879
|
//#region src/rules/max-lines-per-function/rule.ts
|
|
881
|
-
const RULE_NAME$
|
|
880
|
+
const RULE_NAME$13 = "max-lines-per-function";
|
|
882
881
|
const MESSAGE_ID_EXCEED = "exceed";
|
|
883
882
|
/** A line that is empty or holds only whitespace. */
|
|
884
883
|
const BLANK_LINE = /^\s*$/u;
|
|
@@ -889,7 +888,7 @@ const DEFAULTS = {
|
|
|
889
888
|
skipBlankLines: false,
|
|
890
889
|
skipComments: false
|
|
891
890
|
};
|
|
892
|
-
const messages$
|
|
891
|
+
const messages$13 = { [MESSAGE_ID_EXCEED]: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." };
|
|
893
892
|
const schema$2 = [{
|
|
894
893
|
additionalProperties: false,
|
|
895
894
|
properties: {
|
|
@@ -992,7 +991,7 @@ function getCountedLoc(countFrom, funcNode, reportNode) {
|
|
|
992
991
|
* @param context - The rule context.
|
|
993
992
|
* @returns The visitors measuring each function.
|
|
994
993
|
*/
|
|
995
|
-
function createOnce$
|
|
994
|
+
function createOnce$8(context) {
|
|
996
995
|
let commentLineNumbers;
|
|
997
996
|
let config;
|
|
998
997
|
let lines;
|
|
@@ -1048,8 +1047,8 @@ function createOnce$7(context) {
|
|
|
1048
1047
|
};
|
|
1049
1048
|
}
|
|
1050
1049
|
const maxLinesPerFunction = createFlawlessRule({
|
|
1051
|
-
name: RULE_NAME$
|
|
1052
|
-
createOnce: createOnce$
|
|
1050
|
+
name: RULE_NAME$13,
|
|
1051
|
+
createOnce: createOnce$8,
|
|
1053
1052
|
defaultOptions: [DEFAULTS],
|
|
1054
1053
|
meta: {
|
|
1055
1054
|
defaultOptions: [DEFAULTS],
|
|
@@ -1059,7 +1058,7 @@ const maxLinesPerFunction = createFlawlessRule({
|
|
|
1059
1058
|
requiresTypeChecking: false
|
|
1060
1059
|
},
|
|
1061
1060
|
hasSuggestions: false,
|
|
1062
|
-
messages: messages$
|
|
1061
|
+
messages: messages$13,
|
|
1063
1062
|
schema: schema$2,
|
|
1064
1063
|
type: "suggestion"
|
|
1065
1064
|
}
|
|
@@ -1539,6 +1538,77 @@ function isUsedVariable(variable) {
|
|
|
1539
1538
|
});
|
|
1540
1539
|
}
|
|
1541
1540
|
//#endregion
|
|
1541
|
+
//#region src/rules/naming-convention/utils/contextual-type.ts
|
|
1542
|
+
/**
|
|
1543
|
+
* Determines if an object literal member's name is dictated by the contextual
|
|
1544
|
+
* type of the enclosing object literal (e.g. `{ ... } satisfies
|
|
1545
|
+
* Partial<Service>`). In that case the name is not the author's choice - it is
|
|
1546
|
+
* required by the declared type, which is itself validated at its declaration
|
|
1547
|
+
* site - so naming validation should be skipped.
|
|
1548
|
+
*
|
|
1549
|
+
* @param node - The non-computed object literal property or method node.
|
|
1550
|
+
* @param services - Parser services (may lack type information).
|
|
1551
|
+
* @param cache - Per-file cache of contextual types keyed by object literal.
|
|
1552
|
+
* @returns True if the member name is required by a contextual type.
|
|
1553
|
+
*/
|
|
1554
|
+
function isDictatedByContextualType(node, services, cache) {
|
|
1555
|
+
if (!services.program || node.parent.type !== AST_NODE_TYPES.ObjectExpression) return false;
|
|
1556
|
+
const checker = services.program.getTypeChecker();
|
|
1557
|
+
const tsObject = services.esTreeNodeToTSNodeMap.get(node.parent);
|
|
1558
|
+
const candidates = getCandidateTypes(node.parent, tsObject, checker, cache);
|
|
1559
|
+
if (candidates === null) return false;
|
|
1560
|
+
const name = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : String(node.key.value);
|
|
1561
|
+
return candidates.some((candidate) => {
|
|
1562
|
+
const property = checker.getPropertyOfType(candidate, name);
|
|
1563
|
+
return property !== void 0 && !isDeclaredWithinLiteral(property, tsObject);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Resolves the contextual type of an object literal into its candidate
|
|
1568
|
+
* constituent types, cached per object literal.
|
|
1569
|
+
*
|
|
1570
|
+
* Union arms are checked individually because `getPropertyOfType` on a union
|
|
1571
|
+
* only finds properties present in every arm, while a name dictated by a
|
|
1572
|
+
* single arm still isn't the author's choice.
|
|
1573
|
+
*
|
|
1574
|
+
* @param objectNode - The enclosing object literal (ESTree).
|
|
1575
|
+
* @param tsObject - The corresponding TypeScript node.
|
|
1576
|
+
* @param checker - The type checker.
|
|
1577
|
+
* @param cache - Per-file cache of contextual types keyed by object literal.
|
|
1578
|
+
* @returns The candidate types, or null if there is no contextual type.
|
|
1579
|
+
*/
|
|
1580
|
+
function getCandidateTypes(objectNode, tsObject, checker, cache) {
|
|
1581
|
+
const cached = cache.get(objectNode);
|
|
1582
|
+
if (cached !== void 0) return cached;
|
|
1583
|
+
const contextualType = checker.getContextualType(tsObject);
|
|
1584
|
+
let candidates = null;
|
|
1585
|
+
if (contextualType !== void 0) {
|
|
1586
|
+
const nonNullable = contextualType.getNonNullableType();
|
|
1587
|
+
candidates = (nonNullable.isUnion() ? nonNullable.types : [nonNullable]).map((arm) => checker.getApparentType(arm));
|
|
1588
|
+
}
|
|
1589
|
+
cache.set(objectNode, candidates);
|
|
1590
|
+
return candidates;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Guards against self-inference: in generic calls like `identity({ Name: 1 })`
|
|
1594
|
+
* the contextual type is inferred from the literal itself, so the property
|
|
1595
|
+
* symbol's declarations all live inside the literal - the name is still the
|
|
1596
|
+
* author's choice. Transient symbols without declarations (such as those from
|
|
1597
|
+
* mapped types like `Partial<T>`) originate from the contextual type and count
|
|
1598
|
+
* as dictated.
|
|
1599
|
+
*
|
|
1600
|
+
* @param symbol - The property symbol found on a candidate contextual type.
|
|
1601
|
+
* @param literal - The object literal being checked.
|
|
1602
|
+
* @returns True if every declaration of the symbol lies inside the literal.
|
|
1603
|
+
*/
|
|
1604
|
+
function isDeclaredWithinLiteral(symbol, literal) {
|
|
1605
|
+
const declarations = symbol.declarations ?? [];
|
|
1606
|
+
if (declarations.length === 0) return false;
|
|
1607
|
+
return declarations.every((declaration) => {
|
|
1608
|
+
return declaration.getSourceFile() === literal.getSourceFile() && declaration.pos >= literal.pos && declaration.end <= literal.end;
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1542
1612
|
//#region src/rules/naming-convention/utils/enums.ts
|
|
1543
1613
|
const Selector = {
|
|
1544
1614
|
variable: 1,
|
|
@@ -1588,7 +1658,8 @@ const Modifier = {
|
|
|
1588
1658
|
"override": 8192,
|
|
1589
1659
|
"async": 16384,
|
|
1590
1660
|
"default": 32768,
|
|
1591
|
-
"namespace": 65536
|
|
1661
|
+
"namespace": 65536,
|
|
1662
|
+
"constAsserted": 131072
|
|
1592
1663
|
};
|
|
1593
1664
|
const PredefinedFormat = {
|
|
1594
1665
|
camelCase: 1,
|
|
@@ -1600,13 +1671,15 @@ const PredefinedFormat = {
|
|
|
1600
1671
|
};
|
|
1601
1672
|
const PredefinedFormatValueToKey = Object.fromEntries(Object.entries(PredefinedFormat).map(([key, value]) => [value, key]));
|
|
1602
1673
|
const TypeModifier = {
|
|
1603
|
-
boolean:
|
|
1604
|
-
string:
|
|
1605
|
-
number:
|
|
1606
|
-
function:
|
|
1607
|
-
array:
|
|
1674
|
+
boolean: 262144,
|
|
1675
|
+
string: 524288,
|
|
1676
|
+
number: 1048576,
|
|
1677
|
+
function: 2097152,
|
|
1678
|
+
array: 4194304
|
|
1608
1679
|
};
|
|
1609
1680
|
const TypeModifierValueToKey = Object.fromEntries(Object.entries(TypeModifier).map(([key, value]) => [value, key]));
|
|
1681
|
+
const TYPE_REFERENCE_LOOSE_WEIGHT = 1 << 23;
|
|
1682
|
+
const TYPE_REFERENCE_STRICT_WEIGHT = 1 << 24;
|
|
1610
1683
|
const UnderscoreOption = {
|
|
1611
1684
|
forbid: 1,
|
|
1612
1685
|
allow: 2,
|
|
@@ -1701,7 +1774,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1701
1774
|
if (!aIsMethodOrProperty && bIsMethodOrProperty) return 1;
|
|
1702
1775
|
return b.selector - a.selector;
|
|
1703
1776
|
});
|
|
1704
|
-
return (node, modifiers = /* @__PURE__ */ new Set()) => {
|
|
1777
|
+
return (node, modifiers = /* @__PURE__ */ new Set(), isObjectStyleEnumName = false) => {
|
|
1705
1778
|
const originalName = node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`;
|
|
1706
1779
|
for (const config of configs) {
|
|
1707
1780
|
if (config.filter?.regex.test(originalName) !== config.filter?.match) continue;
|
|
@@ -1711,6 +1784,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1711
1784
|
name = validateUnderscore({
|
|
1712
1785
|
name,
|
|
1713
1786
|
config,
|
|
1787
|
+
isObjectStyleEnumName,
|
|
1714
1788
|
node,
|
|
1715
1789
|
originalName,
|
|
1716
1790
|
position: "leading"
|
|
@@ -1719,6 +1793,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1719
1793
|
name = validateUnderscore({
|
|
1720
1794
|
name,
|
|
1721
1795
|
config,
|
|
1796
|
+
isObjectStyleEnumName,
|
|
1722
1797
|
node,
|
|
1723
1798
|
originalName,
|
|
1724
1799
|
position: "trailing"
|
|
@@ -1727,6 +1802,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1727
1802
|
name = validateAffix({
|
|
1728
1803
|
name,
|
|
1729
1804
|
config,
|
|
1805
|
+
isObjectStyleEnumName,
|
|
1730
1806
|
node,
|
|
1731
1807
|
originalName,
|
|
1732
1808
|
position: "prefix"
|
|
@@ -1735,6 +1811,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1735
1811
|
name = validateAffix({
|
|
1736
1812
|
name,
|
|
1737
1813
|
config,
|
|
1814
|
+
isObjectStyleEnumName,
|
|
1738
1815
|
node,
|
|
1739
1816
|
originalName,
|
|
1740
1817
|
position: "suffix"
|
|
@@ -1743,12 +1820,14 @@ function createValidator(type, context, allConfigs) {
|
|
|
1743
1820
|
if (!validateCustom({
|
|
1744
1821
|
name,
|
|
1745
1822
|
config,
|
|
1823
|
+
isObjectStyleEnumName,
|
|
1746
1824
|
node,
|
|
1747
1825
|
originalName
|
|
1748
1826
|
})) return;
|
|
1749
1827
|
if (!validatePredefinedFormat({
|
|
1750
1828
|
name,
|
|
1751
1829
|
config,
|
|
1830
|
+
isObjectStyleEnumName,
|
|
1752
1831
|
modifiers,
|
|
1753
1832
|
node,
|
|
1754
1833
|
originalName
|
|
@@ -1772,7 +1851,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1772
1851
|
type: selectorTypeToMessageString(type)
|
|
1773
1852
|
};
|
|
1774
1853
|
}
|
|
1775
|
-
function validateUnderscore({ name, config, node, originalName, position }) {
|
|
1854
|
+
function validateUnderscore({ name, config, isObjectStyleEnumName, node, originalName, position }) {
|
|
1776
1855
|
const option = position === "leading" ? config.leadingUnderscore : config.trailingUnderscore;
|
|
1777
1856
|
if (!option) return name;
|
|
1778
1857
|
const hasSingleUnderscore = position === "leading" ? () => name.startsWith("_") : () => name.endsWith("_");
|
|
@@ -1798,7 +1877,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1798
1877
|
originalName,
|
|
1799
1878
|
position
|
|
1800
1879
|
}),
|
|
1801
|
-
messageId: "unexpectedUnderscore",
|
|
1880
|
+
messageId: pickMessageId("unexpectedUnderscore", isObjectStyleEnumName),
|
|
1802
1881
|
node
|
|
1803
1882
|
});
|
|
1804
1883
|
return;
|
|
@@ -1812,7 +1891,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1812
1891
|
originalName,
|
|
1813
1892
|
position
|
|
1814
1893
|
}),
|
|
1815
|
-
messageId: "missingUnderscore",
|
|
1894
|
+
messageId: pickMessageId("missingUnderscore", isObjectStyleEnumName),
|
|
1816
1895
|
node
|
|
1817
1896
|
});
|
|
1818
1897
|
return;
|
|
@@ -1826,7 +1905,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1826
1905
|
originalName,
|
|
1827
1906
|
position
|
|
1828
1907
|
}),
|
|
1829
|
-
messageId: "missingUnderscore",
|
|
1908
|
+
messageId: pickMessageId("missingUnderscore", isObjectStyleEnumName),
|
|
1830
1909
|
node
|
|
1831
1910
|
});
|
|
1832
1911
|
return;
|
|
@@ -1834,7 +1913,7 @@ function createValidator(type, context, allConfigs) {
|
|
|
1834
1913
|
return trimDoubleUnderscore();
|
|
1835
1914
|
}
|
|
1836
1915
|
}
|
|
1837
|
-
function validateAffix({ name, config, node, originalName, position }) {
|
|
1916
|
+
function validateAffix({ name, config, isObjectStyleEnumName, node, originalName, position }) {
|
|
1838
1917
|
const affixes = config[position];
|
|
1839
1918
|
if (!affixes || affixes.length === 0) return name;
|
|
1840
1919
|
for (const affix of affixes) {
|
|
@@ -1848,11 +1927,11 @@ function createValidator(type, context, allConfigs) {
|
|
|
1848
1927
|
originalName,
|
|
1849
1928
|
position
|
|
1850
1929
|
}),
|
|
1851
|
-
messageId: "missingAffix",
|
|
1930
|
+
messageId: pickMessageId("missingAffix", isObjectStyleEnumName),
|
|
1852
1931
|
node
|
|
1853
1932
|
});
|
|
1854
1933
|
}
|
|
1855
|
-
function validateCustom({ name, config, node, originalName }) {
|
|
1934
|
+
function validateCustom({ name, config, isObjectStyleEnumName, node, originalName }) {
|
|
1856
1935
|
const { custom } = config;
|
|
1857
1936
|
if (!custom) return true;
|
|
1858
1937
|
const result = custom.regex.test(name);
|
|
@@ -1863,12 +1942,12 @@ function createValidator(type, context, allConfigs) {
|
|
|
1863
1942
|
custom,
|
|
1864
1943
|
originalName
|
|
1865
1944
|
}),
|
|
1866
|
-
messageId: "satisfyCustom",
|
|
1945
|
+
messageId: pickMessageId("satisfyCustom", isObjectStyleEnumName),
|
|
1867
1946
|
node
|
|
1868
1947
|
});
|
|
1869
1948
|
return false;
|
|
1870
1949
|
}
|
|
1871
|
-
function validatePredefinedFormat({ name, config, modifiers, node, originalName }) {
|
|
1950
|
+
function validatePredefinedFormat({ name, config, isObjectStyleEnumName, modifiers, node, originalName }) {
|
|
1872
1951
|
const formats = config.format;
|
|
1873
1952
|
if (!formats || formats.length === 0) return true;
|
|
1874
1953
|
if (!modifiers.has(Modifier.requiresQuotes)) for (const format of formats) {
|
|
@@ -1881,37 +1960,150 @@ function createValidator(type, context, allConfigs) {
|
|
|
1881
1960
|
originalName,
|
|
1882
1961
|
processedName: name
|
|
1883
1962
|
}),
|
|
1884
|
-
messageId: originalName === name ? "doesNotMatchFormat" : "doesNotMatchFormatTrimmed",
|
|
1963
|
+
messageId: pickMessageId(originalName === name ? "doesNotMatchFormat" : "doesNotMatchFormatTrimmed", isObjectStyleEnumName),
|
|
1885
1964
|
node
|
|
1886
1965
|
});
|
|
1887
1966
|
return false;
|
|
1888
1967
|
}
|
|
1889
1968
|
}
|
|
1890
|
-
|
|
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
|
+
}
|
|
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;
|
|
1891
1984
|
function isAllTypesMatch(type, callback) {
|
|
1892
1985
|
if (type.isUnion()) return type.types.every((inner) => callback(inner));
|
|
1893
1986
|
return callback(type);
|
|
1894
1987
|
}
|
|
1988
|
+
function isAnyType(type) {
|
|
1989
|
+
return (type.flags & TypeFlags.Any) !== 0;
|
|
1990
|
+
}
|
|
1991
|
+
function symbolMatchesTypeReference(symbol, reference) {
|
|
1992
|
+
if (symbol === void 0 || reference.name === void 0 || symbol.name !== reference.name) return false;
|
|
1993
|
+
if (reference.from === void 0) return true;
|
|
1994
|
+
const { declarations } = symbol;
|
|
1995
|
+
if (!declarations || declarations.length === 0) return false;
|
|
1996
|
+
for (const declaration of declarations) {
|
|
1997
|
+
const { fileName } = declaration.getSourceFile();
|
|
1998
|
+
if (moduleSpecifierMatches(fileName, reference.from)) return true;
|
|
1999
|
+
}
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
function matchesNamedTypeReference(type, reference) {
|
|
2003
|
+
if (isAnyType(type)) return false;
|
|
2004
|
+
if (symbolMatchesTypeReference(type.aliasSymbol, reference)) return true;
|
|
2005
|
+
if (symbolMatchesTypeReference(type.symbol, reference)) return true;
|
|
2006
|
+
if (type.isIntersection() || type.isUnion()) {
|
|
2007
|
+
for (const inner of type.types) if (matchesNamedTypeReference(inner, reference)) return true;
|
|
2008
|
+
}
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
function matchesReturnTypeReference(type, reference) {
|
|
2012
|
+
if (isAnyType(type)) return false;
|
|
2013
|
+
for (const signature of type.getCallSignatures()) if (matchesTypeReference(signature.getReturnType(), reference)) return true;
|
|
2014
|
+
if (type.isIntersection() || type.isUnion()) {
|
|
2015
|
+
for (const inner of type.types) if (matchesReturnTypeReference(inner, reference)) return true;
|
|
2016
|
+
}
|
|
2017
|
+
return false;
|
|
2018
|
+
}
|
|
2019
|
+
function matchesTypeReference(type, reference) {
|
|
2020
|
+
if (isAnyType(type)) return false;
|
|
2021
|
+
if (reference.name === void 0 && reference.returns === void 0) return false;
|
|
2022
|
+
if (reference.name !== void 0 && !matchesNamedTypeReference(type, reference)) return false;
|
|
2023
|
+
if (reference.returns !== void 0 && !matchesReturnTypeReference(type, reference.returns)) return false;
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
1895
2026
|
function isCorrectType(node, config, context, selector) {
|
|
1896
2027
|
if (config.types === void 0) return true;
|
|
1897
2028
|
if ((SelectorsAllowedToHaveTypes & selector) === 0) return true;
|
|
1898
2029
|
const services = getParserServices(context);
|
|
1899
2030
|
const checker = services.program.getTypeChecker();
|
|
1900
2031
|
const type = services.getTypeAtLocation(node).getNonNullableType();
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2032
|
+
const predicates = [];
|
|
2033
|
+
for (const allowedType of config.types) {
|
|
2034
|
+
if (typeof allowedType === "object") {
|
|
2035
|
+
predicates.push((inner) => matchesTypeReference(inner, allowedType));
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
switch (allowedType) {
|
|
2039
|
+
case TypeModifier.array:
|
|
2040
|
+
predicates.push((inner) => checker.isArrayType(inner) || checker.isTupleType(inner));
|
|
2041
|
+
break;
|
|
2042
|
+
case TypeModifier.boolean:
|
|
2043
|
+
case TypeModifier.number:
|
|
2044
|
+
case TypeModifier.string:
|
|
2045
|
+
predicates.push((inner) => {
|
|
2046
|
+
return checker.typeToString(checker.getWidenedType(checker.getBaseTypeOfLiteralType(inner))) === TypeModifierValueToKey[allowedType];
|
|
2047
|
+
});
|
|
2048
|
+
break;
|
|
2049
|
+
case TypeModifier.function:
|
|
2050
|
+
predicates.push((inner) => inner.getCallSignatures().length > 0);
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
1913
2053
|
}
|
|
1914
|
-
return
|
|
2054
|
+
return isAllTypesMatch(type, (inner) => predicates.some((predicate) => predicate(inner)));
|
|
2055
|
+
}
|
|
2056
|
+
const BACKSLASH_PATTERN = /\\/gu;
|
|
2057
|
+
const TYPESCRIPT_EXTENSION_PATTERN = /\.d\.ts$|\.tsx?$/u;
|
|
2058
|
+
const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\//u;
|
|
2059
|
+
const LEADING_DOT_OR_SLASH_PATTERN = /^(\.\/|\/)/u;
|
|
2060
|
+
/**
|
|
2061
|
+
* Path-form specifiers start with `.`, `/`, or a Windows drive letter (e.g.
|
|
2062
|
+
* `C:/`).
|
|
2063
|
+
*
|
|
2064
|
+
* @param specifier - The module specifier to classify.
|
|
2065
|
+
* @returns True if the specifier should be treated as a filesystem path rather
|
|
2066
|
+
* than a package name.
|
|
2067
|
+
*/
|
|
2068
|
+
function looksLikePath(specifier) {
|
|
2069
|
+
if (specifier.startsWith(".")) return true;
|
|
2070
|
+
if (specifier.startsWith("/")) return true;
|
|
2071
|
+
return WINDOWS_DRIVE_PATTERN.test(specifier);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Checks whether a declaration's source file matches a module specifier.
|
|
2075
|
+
*
|
|
2076
|
+
* Two specifier shapes are supported:
|
|
2077
|
+
*
|
|
2078
|
+
* 1. **Bare package specifier** (e.g. `"@rbxts/jecs"`, `"lodash"`) — matches
|
|
2079
|
+
* when the declaration's file path contains `/node_modules/<specifier>/` as
|
|
2080
|
+
* a substring. Handles flat and pnpm-style layouts (pnpm paths still contain
|
|
2081
|
+
* a final `/node_modules/<specifier>/` segment after the virtual store
|
|
2082
|
+
* directory). **Not** supported: Yarn Plug'n'Play (no `node_modules` on
|
|
2083
|
+
* disk), vendored packages outside `node_modules`, or types provided by
|
|
2084
|
+
* separate `@types/*` packages.
|
|
2085
|
+
* 2. **Path specifier** (starts with `.`, `/`, or a Windows drive letter) —
|
|
2086
|
+
* matches against the normalized declaration path with `.d.ts` / `.tsx?`
|
|
2087
|
+
* stripped. Windows absolute paths require exact equality. POSIX-style
|
|
2088
|
+
* absolute or relative paths are normalized to a bare tail and matched as a
|
|
2089
|
+
* suffix; this means `"./shared/network"` matches a declaration at
|
|
2090
|
+
* `<root>/shared/network.ts`.
|
|
2091
|
+
*
|
|
2092
|
+
* @param declarationFile - Path of the file declaring the matched symbol.
|
|
2093
|
+
* @param specifier - The `from` module specifier from the rule config.
|
|
2094
|
+
* @returns True if the declaration file matches the specifier.
|
|
2095
|
+
*/
|
|
2096
|
+
function moduleSpecifierMatches(declarationFile, specifier) {
|
|
2097
|
+
const normalizedFile = declarationFile.replace(BACKSLASH_PATTERN, "/");
|
|
2098
|
+
const normalizedSpecifier = specifier.replace(BACKSLASH_PATTERN, "/");
|
|
2099
|
+
if (looksLikePath(normalizedSpecifier)) {
|
|
2100
|
+
const stripped = normalizedFile.replace(TYPESCRIPT_EXTENSION_PATTERN, "");
|
|
2101
|
+
if (WINDOWS_DRIVE_PATTERN.test(normalizedSpecifier)) return stripped === normalizedSpecifier;
|
|
2102
|
+
const tail = normalizedSpecifier.replace(LEADING_DOT_OR_SLASH_PATTERN, "");
|
|
2103
|
+
if (stripped === tail) return true;
|
|
2104
|
+
return stripped.endsWith(`/${tail}`);
|
|
2105
|
+
}
|
|
2106
|
+
return normalizedFile.includes(`/node_modules/${normalizedSpecifier}/`);
|
|
1915
2107
|
}
|
|
1916
2108
|
//#endregion
|
|
1917
2109
|
//#region src/rules/naming-convention/utils/parse-options.ts
|
|
@@ -1919,10 +2111,22 @@ function parseOptions(context) {
|
|
|
1919
2111
|
const normalizedOptions = context.options.flatMap(normalizeOption);
|
|
1920
2112
|
return Object.fromEntries(Object.keys(Selector).map((key) => [key, createValidator(key, context, normalizedOptions)]));
|
|
1921
2113
|
}
|
|
2114
|
+
/**
|
|
2115
|
+
* A type-reference matcher counts as "strict" for sorting when any level of it
|
|
2116
|
+
* (including nested `returns` matchers) constrains the declaring module.
|
|
2117
|
+
*
|
|
2118
|
+
* @param reference - The matcher to inspect.
|
|
2119
|
+
* @returns True if the matcher carries a `from` constraint at any depth.
|
|
2120
|
+
*/
|
|
2121
|
+
function hasFromConstraint(reference) {
|
|
2122
|
+
if (reference.from !== void 0) return true;
|
|
2123
|
+
return reference.returns !== void 0 && hasFromConstraint(reference.returns);
|
|
2124
|
+
}
|
|
1922
2125
|
function normalizeOption(option) {
|
|
1923
2126
|
let weight = 0;
|
|
1924
2127
|
if (option.modifiers) for (const modifier of option.modifiers) weight |= Modifier[modifier];
|
|
1925
|
-
if (option.types) for (const type of option.types) weight |= TypeModifier[type];
|
|
2128
|
+
if (option.types) for (const type of option.types) if (typeof type === "string") weight |= TypeModifier[type];
|
|
2129
|
+
else weight |= hasFromConstraint(type) ? TYPE_REFERENCE_STRICT_WEIGHT : TYPE_REFERENCE_LOOSE_WEIGHT;
|
|
1926
2130
|
if (option.filter !== void 0) weight |= 1 << 30;
|
|
1927
2131
|
const normalizedOption = {
|
|
1928
2132
|
custom: option.custom ? {
|
|
@@ -1943,7 +2147,7 @@ function normalizeOption(option) {
|
|
|
1943
2147
|
prefix: option.prefix && option.prefix.length > 0 ? option.prefix : void 0,
|
|
1944
2148
|
suffix: option.suffix && option.suffix.length > 0 ? option.suffix : void 0,
|
|
1945
2149
|
trailingUnderscore: option.trailingUnderscore !== void 0 ? UnderscoreOption[option.trailingUnderscore] : void 0,
|
|
1946
|
-
types: option.types?.map((type) => TypeModifier[type]) ?? void 0
|
|
2150
|
+
types: option.types?.map((type) => typeof type === "string" ? TypeModifier[type] : type) ?? void 0
|
|
1947
2151
|
};
|
|
1948
2152
|
return (Array.isArray(option.selector) ? option.selector : [option.selector]).map((selector) => {
|
|
1949
2153
|
return {
|
|
@@ -1981,9 +2185,39 @@ const $DEFS = {
|
|
|
1981
2185
|
},
|
|
1982
2186
|
type: "array"
|
|
1983
2187
|
},
|
|
1984
|
-
|
|
2188
|
+
typeMatcher: { oneOf: [{
|
|
2189
|
+
description: "Built-in type modifier matching by widened TS type.",
|
|
1985
2190
|
enum: Object.keys(TypeModifier),
|
|
1986
2191
|
type: "string"
|
|
2192
|
+
}, { $ref: "#/$defs/typeReferenceMatcher" }] },
|
|
2193
|
+
typeReferenceMatcher: {
|
|
2194
|
+
additionalProperties: false,
|
|
2195
|
+
anyOf: [{
|
|
2196
|
+
required: ["name"],
|
|
2197
|
+
type: "object"
|
|
2198
|
+
}, {
|
|
2199
|
+
required: ["returns"],
|
|
2200
|
+
type: "object"
|
|
2201
|
+
}],
|
|
2202
|
+
dependencies: { from: ["name"] },
|
|
2203
|
+
description: "Type-reference matcher. Matches when the variable's type resolves to a symbol with the given `name`; if `from` is supplied, the symbol's declaration must also live in a file the specifier resolves to. `returns` instead (or additionally) matches callable types by the return type of their call signatures.",
|
|
2204
|
+
properties: {
|
|
2205
|
+
name: {
|
|
2206
|
+
description: "Symbol name to match (compared against the type's `aliasSymbol` then `symbol`).",
|
|
2207
|
+
minLength: 1,
|
|
2208
|
+
type: "string"
|
|
2209
|
+
},
|
|
2210
|
+
from: {
|
|
2211
|
+
description: "Module specifier the type must originate from. Bare package name (e.g. `@rbxts/jecs`) matches `/node_modules/<from>/` in the declaration path. Path-form (starts with `.`, `/`, or a Windows drive letter) matches the declaration path with extension stripped. Omit to match any source.",
|
|
2212
|
+
minLength: 1,
|
|
2213
|
+
type: "string"
|
|
2214
|
+
},
|
|
2215
|
+
returns: {
|
|
2216
|
+
$ref: "#/$defs/typeReferenceMatcher",
|
|
2217
|
+
description: "Nested matcher applied to call-signature return types. The type matches when at least one call signature's return type satisfies it. Lets anonymous function types (which have no symbol name) be matched, e.g. React components typed `(props: P) => ReactNode`."
|
|
2218
|
+
}
|
|
2219
|
+
},
|
|
2220
|
+
type: "object"
|
|
1987
2221
|
},
|
|
1988
2222
|
underscoreOptions: {
|
|
1989
2223
|
enum: Object.keys(UnderscoreOption),
|
|
@@ -2023,7 +2257,7 @@ function selectorSchema(selectorString, allowType, modifiers) {
|
|
|
2023
2257
|
};
|
|
2024
2258
|
if (allowType) selector["types"] = {
|
|
2025
2259
|
additionalItems: false,
|
|
2026
|
-
items: { $ref: "#/$defs/
|
|
2260
|
+
items: { $ref: "#/$defs/typeMatcher" },
|
|
2027
2261
|
type: "array"
|
|
2028
2262
|
};
|
|
2029
2263
|
return [{
|
|
@@ -2065,7 +2299,7 @@ function selectorsSchema() {
|
|
|
2065
2299
|
},
|
|
2066
2300
|
types: {
|
|
2067
2301
|
additionalItems: false,
|
|
2068
|
-
items: { $ref: "#/$defs/
|
|
2302
|
+
items: { $ref: "#/$defs/typeMatcher" },
|
|
2069
2303
|
type: "array"
|
|
2070
2304
|
}
|
|
2071
2305
|
},
|
|
@@ -2082,13 +2316,14 @@ const SCHEMA = {
|
|
|
2082
2316
|
...selectorSchema("variableLike", false, ["unused", "async"]),
|
|
2083
2317
|
...selectorSchema("variable", true, [
|
|
2084
2318
|
"const",
|
|
2319
|
+
"constAsserted",
|
|
2085
2320
|
"destructured",
|
|
2086
2321
|
"exported",
|
|
2087
2322
|
"global",
|
|
2088
2323
|
"unused",
|
|
2089
2324
|
"async"
|
|
2090
2325
|
]),
|
|
2091
|
-
...selectorSchema("function",
|
|
2326
|
+
...selectorSchema("function", true, [
|
|
2092
2327
|
"exported",
|
|
2093
2328
|
"global",
|
|
2094
2329
|
"unused",
|
|
@@ -2148,7 +2383,7 @@ const SCHEMA = {
|
|
|
2148
2383
|
"override",
|
|
2149
2384
|
"async"
|
|
2150
2385
|
]),
|
|
2151
|
-
...selectorSchema("classMethod",
|
|
2386
|
+
...selectorSchema("classMethod", true, [
|
|
2152
2387
|
"abstract",
|
|
2153
2388
|
"private",
|
|
2154
2389
|
"#private",
|
|
@@ -2159,13 +2394,13 @@ const SCHEMA = {
|
|
|
2159
2394
|
"override",
|
|
2160
2395
|
"async"
|
|
2161
2396
|
]),
|
|
2162
|
-
...selectorSchema("objectLiteralMethod",
|
|
2397
|
+
...selectorSchema("objectLiteralMethod", true, [
|
|
2163
2398
|
"public",
|
|
2164
2399
|
"requiresQuotes",
|
|
2165
2400
|
"async"
|
|
2166
2401
|
]),
|
|
2167
|
-
...selectorSchema("typeMethod",
|
|
2168
|
-
...selectorSchema("method",
|
|
2402
|
+
...selectorSchema("typeMethod", true, ["public", "requiresQuotes"]),
|
|
2403
|
+
...selectorSchema("method", true, [
|
|
2169
2404
|
"abstract",
|
|
2170
2405
|
"private",
|
|
2171
2406
|
"#private",
|
|
@@ -2224,14 +2459,21 @@ const SCHEMA = {
|
|
|
2224
2459
|
};
|
|
2225
2460
|
//#endregion
|
|
2226
2461
|
//#region src/rules/naming-convention/rule.ts
|
|
2227
|
-
const RULE_NAME$
|
|
2228
|
-
const
|
|
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 = {
|
|
2229
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}`,
|
|
2230
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}`,
|
|
2231
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}`,
|
|
2232
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}`,
|
|
2233
2473
|
satisfyCustom: "{{type}} name `{{name}}` must {{regexMatch}} the RegExp: {{regex}}",
|
|
2234
|
-
|
|
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}`
|
|
2235
2477
|
};
|
|
2236
2478
|
const camelCaseNamingConfig = [
|
|
2237
2479
|
{
|
|
@@ -2258,22 +2500,13 @@ const camelCaseNamingConfig = [
|
|
|
2258
2500
|
function create$4(contextWithoutDefaults) {
|
|
2259
2501
|
const context = contextWithoutDefaults.options.length > 0 ? contextWithoutDefaults : Object.setPrototypeOf({ options: camelCaseNamingConfig }, contextWithoutDefaults);
|
|
2260
2502
|
const validators = parseOptions(context);
|
|
2261
|
-
const
|
|
2503
|
+
const services = getParserServices(context, true);
|
|
2504
|
+
const compilerOptions = services.program?.getCompilerOptions() ?? {};
|
|
2505
|
+
const contextualTypeCache = /* @__PURE__ */ new WeakMap();
|
|
2262
2506
|
function handleMember(validator, { key }, modifiers) {
|
|
2263
|
-
if (requiresQuoting
|
|
2507
|
+
if (requiresQuoting(key, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
2264
2508
|
validator(key, modifiers);
|
|
2265
2509
|
}
|
|
2266
|
-
function getMemberModifiers(node) {
|
|
2267
|
-
const modifiers = /* @__PURE__ */ new Set();
|
|
2268
|
-
if ("key" in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) modifiers.add(Modifier["#private"]);
|
|
2269
|
-
else if (node.accessibility) modifiers.add(Modifier[node.accessibility]);
|
|
2270
|
-
else modifiers.add(Modifier.public);
|
|
2271
|
-
if (node.static) modifiers.add(Modifier.static);
|
|
2272
|
-
if ("readonly" in node && node.readonly) modifiers.add(Modifier.readonly);
|
|
2273
|
-
if ("override" in node && node.override) modifiers.add(Modifier.override);
|
|
2274
|
-
if (node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition || node.type === AST_NODE_TYPES.TSAbstractAccessorProperty) modifiers.add(Modifier.abstract);
|
|
2275
|
-
return modifiers;
|
|
2276
|
-
}
|
|
2277
2510
|
const { unusedVariables } = collectVariables(context);
|
|
2278
2511
|
function isUnused(name, initialScope) {
|
|
2279
2512
|
let variable = null;
|
|
@@ -2286,52 +2519,6 @@ function create$4(contextWithoutDefaults) {
|
|
|
2286
2519
|
if (!variable) return false;
|
|
2287
2520
|
return unusedVariables.has(variable);
|
|
2288
2521
|
}
|
|
2289
|
-
function isDestructured(id) {
|
|
2290
|
-
return id.parent.type === AST_NODE_TYPES.Property && id.parent.shorthand || id.parent.type === AST_NODE_TYPES.AssignmentPattern && id.parent.parent.type === AST_NODE_TYPES.Property && id.parent.parent.shorthand;
|
|
2291
|
-
}
|
|
2292
|
-
function isAsyncMemberOrProperty(propertyOrMemberNode) {
|
|
2293
|
-
return Boolean("value" in propertyOrMemberNode && propertyOrMemberNode.value && "async" in propertyOrMemberNode.value && propertyOrMemberNode.value.async);
|
|
2294
|
-
}
|
|
2295
|
-
function isAsyncVariableIdentifier(id) {
|
|
2296
|
-
return Boolean("async" in id.parent && id.parent.async || "init" in id.parent && id.parent.init && "async" in id.parent.init && id.parent.init.async);
|
|
2297
|
-
}
|
|
2298
|
-
/**
|
|
2299
|
-
* Determines if a VariableDeclarator represents an object-style enum declaration.
|
|
2300
|
-
*
|
|
2301
|
-
* Object-style enums are const assertions applied to object expressions, commonly
|
|
2302
|
-
* used as an alternative to TypeScript enums. Examples:
|
|
2303
|
-
* - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
|
|
2304
|
-
* - `const Status = <const>{ OK: 200, ERROR: 500 }`.
|
|
2305
|
-
*
|
|
2306
|
-
* @param node - The VariableDeclarator AST node to check.
|
|
2307
|
-
* @param parent - The parent VariableDeclaration node.
|
|
2308
|
-
* @returns True if this represents an object-style enum declaration.
|
|
2309
|
-
*/
|
|
2310
|
-
function isObjectStyleEnumDeclaration(node, parent) {
|
|
2311
|
-
if (parent.kind !== "const") return false;
|
|
2312
|
-
if (!node.init) return false;
|
|
2313
|
-
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;
|
|
2314
|
-
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;
|
|
2315
|
-
return false;
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Checks if a method name exists in any of the implemented interfaces.
|
|
2319
|
-
*
|
|
2320
|
-
* @param implementsList - List of implemented interfaces.
|
|
2321
|
-
* @param methodName - Name of the method to check.
|
|
2322
|
-
* @param checker - TypeScript type checker.
|
|
2323
|
-
* @param services - Parser services.
|
|
2324
|
-
* @returns True if the method name is found in any interface.
|
|
2325
|
-
*/
|
|
2326
|
-
function checkInterfacesForMethod(implementsList, methodName, checker, services) {
|
|
2327
|
-
for (const implementsClause of implementsList) {
|
|
2328
|
-
const interfaceType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(implementsClause));
|
|
2329
|
-
if (!interfaceType.getSymbol()) continue;
|
|
2330
|
-
const interfaceMembers = checker.getPropertiesOfType(interfaceType);
|
|
2331
|
-
for (const member of interfaceMembers) if (member.name === methodName) return true;
|
|
2332
|
-
}
|
|
2333
|
-
return false;
|
|
2334
|
-
}
|
|
2335
2522
|
/**
|
|
2336
2523
|
* Determines if a class method is implementing an interface method.
|
|
2337
2524
|
*
|
|
@@ -2339,7 +2526,6 @@ function create$4(contextWithoutDefaults) {
|
|
|
2339
2526
|
* @returns True if the method is implementing an interface method.
|
|
2340
2527
|
*/
|
|
2341
2528
|
function isImplementingInterfaceMethod(node) {
|
|
2342
|
-
const services = getParserServices(context, true);
|
|
2343
2529
|
if (!services.program) return false;
|
|
2344
2530
|
const checker = services.program.getTypeChecker();
|
|
2345
2531
|
let parentClass = null;
|
|
@@ -2367,12 +2553,20 @@ function create$4(contextWithoutDefaults) {
|
|
|
2367
2553
|
},
|
|
2368
2554
|
":not(ObjectPattern) > Property[computed = false][kind = \"init\"][value.type != \"ArrowFunctionExpression\"][value.type != \"FunctionExpression\"][value.type != \"TSEmptyBodyFunctionExpression\"]": {
|
|
2369
2555
|
handler: (node, validator) => {
|
|
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
|
+
}
|
|
2370
2563
|
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
2371
2564
|
},
|
|
2372
2565
|
validator: validators.objectLiteralProperty
|
|
2373
2566
|
},
|
|
2374
2567
|
[["TSMethodSignature[computed = false]", "TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type = \"TSFunctionType\"]"].join(", ")]: {
|
|
2375
2568
|
handler: (node, validator) => {
|
|
2569
|
+
if (isExternalMember(node, context.sourceCode)) return;
|
|
2376
2570
|
handleMember(validator, node, /* @__PURE__ */ new Set([Modifier.public]));
|
|
2377
2571
|
},
|
|
2378
2572
|
validator: validators.typeMethod
|
|
@@ -2403,6 +2597,7 @@ function create$4(contextWithoutDefaults) {
|
|
|
2403
2597
|
"Property[computed = false][kind = \"init\"][value.type = \"TSEmptyBodyFunctionExpression\"]"
|
|
2404
2598
|
].join(", ")]: {
|
|
2405
2599
|
handler: (node, validator) => {
|
|
2600
|
+
if (node.type === AST_NODE_TYPES.Property && isDictatedByContextualType(node, services, contextualTypeCache)) return;
|
|
2406
2601
|
const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
|
|
2407
2602
|
if (isAsyncMemberOrProperty(node)) modifiers.add(Modifier.async);
|
|
2408
2603
|
handleMember(validator, node, modifiers);
|
|
@@ -2494,7 +2689,7 @@ function create$4(contextWithoutDefaults) {
|
|
|
2494
2689
|
"TSEnumMember": {
|
|
2495
2690
|
handler: ({ id }, validator) => {
|
|
2496
2691
|
const modifiers = /* @__PURE__ */ new Set();
|
|
2497
|
-
if (requiresQuoting
|
|
2692
|
+
if (requiresQuoting(id, compilerOptions.target)) modifiers.add(Modifier.requiresQuotes);
|
|
2498
2693
|
validator(id, modifiers);
|
|
2499
2694
|
},
|
|
2500
2695
|
validator: validators.enumMember
|
|
@@ -2519,6 +2714,7 @@ function create$4(contextWithoutDefaults) {
|
|
|
2519
2714
|
},
|
|
2520
2715
|
"TSPropertySignature[computed = false][typeAnnotation.typeAnnotation.type != \"TSFunctionType\"]": {
|
|
2521
2716
|
handler: (node, validator) => {
|
|
2717
|
+
if (isExternalMember(node, context.sourceCode)) return;
|
|
2522
2718
|
const modifiers = /* @__PURE__ */ new Set([Modifier.public]);
|
|
2523
2719
|
if (node.readonly) modifiers.add(Modifier.readonly);
|
|
2524
2720
|
handleMember(validator, node, modifiers);
|
|
@@ -2548,9 +2744,10 @@ function create$4(contextWithoutDefaults) {
|
|
|
2548
2744
|
handler: (node, validator) => {
|
|
2549
2745
|
const identifiers = getIdentifiersFromPattern(node.id);
|
|
2550
2746
|
const baseModifiers = /* @__PURE__ */ new Set();
|
|
2551
|
-
const { parent } = node;
|
|
2747
|
+
const { init, parent } = node;
|
|
2552
2748
|
if (parent.kind === "const") baseModifiers.add(Modifier.const);
|
|
2553
2749
|
if (isGlobal(context.sourceCode.getScope(node))) baseModifiers.add(Modifier.global);
|
|
2750
|
+
if (isConstAssertedInitializer(init)) baseModifiers.add(Modifier.constAsserted);
|
|
2554
2751
|
const isObjectStyleEnum = isObjectStyleEnumDeclaration(node, parent);
|
|
2555
2752
|
for (const id of identifiers) {
|
|
2556
2753
|
const modifiers = new Set(baseModifiers);
|
|
@@ -2559,7 +2756,7 @@ function create$4(contextWithoutDefaults) {
|
|
|
2559
2756
|
if (isExported(parent, id.name, scope)) modifiers.add(Modifier.exported);
|
|
2560
2757
|
if (isUnused(id.name, scope)) modifiers.add(Modifier.unused);
|
|
2561
2758
|
if (isAsyncVariableIdentifier(id)) modifiers.add(Modifier.async);
|
|
2562
|
-
if (isObjectStyleEnum) validators.objectStyleEnum(id, modifiers);
|
|
2759
|
+
if (isObjectStyleEnum) validators.objectStyleEnum(id, modifiers, true);
|
|
2563
2760
|
else validator(id, modifiers);
|
|
2564
2761
|
}
|
|
2565
2762
|
},
|
|
@@ -2573,7 +2770,7 @@ function create$4(contextWithoutDefaults) {
|
|
|
2573
2770
|
}));
|
|
2574
2771
|
}
|
|
2575
2772
|
const namingConvention = createEslintRule({
|
|
2576
|
-
name: RULE_NAME$
|
|
2773
|
+
name: RULE_NAME$12,
|
|
2577
2774
|
create: create$4,
|
|
2578
2775
|
defaultOptions: camelCaseNamingConfig,
|
|
2579
2776
|
meta: {
|
|
@@ -2584,18 +2781,75 @@ const namingConvention = createEslintRule({
|
|
|
2584
2781
|
},
|
|
2585
2782
|
fixable: void 0,
|
|
2586
2783
|
hasSuggestions: false,
|
|
2587
|
-
messages: messages$
|
|
2784
|
+
messages: messages$12,
|
|
2588
2785
|
schema: SCHEMA,
|
|
2589
2786
|
type: "suggestion"
|
|
2590
2787
|
}
|
|
2591
2788
|
});
|
|
2789
|
+
/**
|
|
2790
|
+
* Checks if a method name exists in any of the implemented interfaces.
|
|
2791
|
+
*
|
|
2792
|
+
* @param implementsList - List of implemented interfaces.
|
|
2793
|
+
* @param methodName - Name of the method to check.
|
|
2794
|
+
* @param checker - TypeScript type checker.
|
|
2795
|
+
* @param services - Parser services.
|
|
2796
|
+
* @returns True if the method name is found in any interface.
|
|
2797
|
+
*/
|
|
2798
|
+
function checkInterfacesForMethod(implementsList, methodName, checker, services) {
|
|
2799
|
+
for (const implementsClause of implementsList) {
|
|
2800
|
+
const interfaceType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(implementsClause));
|
|
2801
|
+
if (!interfaceType.getSymbol()) continue;
|
|
2802
|
+
const interfaceMembers = checker.getPropertiesOfType(interfaceType);
|
|
2803
|
+
for (const member of interfaceMembers) if (member.name === methodName) return true;
|
|
2804
|
+
}
|
|
2805
|
+
return false;
|
|
2806
|
+
}
|
|
2592
2807
|
function getIdentifiersFromPattern(pattern) {
|
|
2593
2808
|
const identifiers = [];
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2809
|
+
const stack = [pattern];
|
|
2810
|
+
for (const node of stack) switch (node.type) {
|
|
2811
|
+
case AST_NODE_TYPES.ArrayPattern:
|
|
2812
|
+
for (const element of node.elements) if (element !== null) stack.push(element);
|
|
2813
|
+
break;
|
|
2814
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
2815
|
+
stack.push(node.left);
|
|
2816
|
+
break;
|
|
2817
|
+
case AST_NODE_TYPES.Identifier:
|
|
2818
|
+
identifiers.push(node);
|
|
2819
|
+
break;
|
|
2820
|
+
case AST_NODE_TYPES.ObjectPattern:
|
|
2821
|
+
for (const property of node.properties) stack.push(property);
|
|
2822
|
+
break;
|
|
2823
|
+
case AST_NODE_TYPES.Property:
|
|
2824
|
+
stack.push(node.value);
|
|
2825
|
+
break;
|
|
2826
|
+
case AST_NODE_TYPES.RestElement:
|
|
2827
|
+
stack.push(node.argument);
|
|
2828
|
+
break;
|
|
2829
|
+
default: break;
|
|
2830
|
+
}
|
|
2597
2831
|
return identifiers;
|
|
2598
2832
|
}
|
|
2833
|
+
function getMemberModifiers(node) {
|
|
2834
|
+
const modifiers = /* @__PURE__ */ new Set();
|
|
2835
|
+
if ("key" in node && node.key.type === AST_NODE_TYPES.PrivateIdentifier) modifiers.add(Modifier["#private"]);
|
|
2836
|
+
else if (node.accessibility) modifiers.add(Modifier[node.accessibility]);
|
|
2837
|
+
else modifiers.add(Modifier.public);
|
|
2838
|
+
if (node.static) modifiers.add(Modifier.static);
|
|
2839
|
+
if ("readonly" in node && node.readonly) modifiers.add(Modifier.readonly);
|
|
2840
|
+
if ("override" in node && node.override) modifiers.add(Modifier.override);
|
|
2841
|
+
if (node.type === AST_NODE_TYPES.TSAbstractPropertyDefinition || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition || node.type === AST_NODE_TYPES.TSAbstractAccessorProperty) modifiers.add(Modifier.abstract);
|
|
2842
|
+
return modifiers;
|
|
2843
|
+
}
|
|
2844
|
+
function isAsyncMemberOrProperty(propertyOrMemberNode) {
|
|
2845
|
+
return Boolean("value" in propertyOrMemberNode && propertyOrMemberNode.value && "async" in propertyOrMemberNode.value && propertyOrMemberNode.value.async);
|
|
2846
|
+
}
|
|
2847
|
+
function isAsyncVariableIdentifier(id) {
|
|
2848
|
+
return Boolean("async" in id.parent && id.parent.async || "init" in id.parent && id.parent.init && "async" in id.parent.init && id.parent.init.async);
|
|
2849
|
+
}
|
|
2850
|
+
function isDestructured(id) {
|
|
2851
|
+
return id.parent.type === AST_NODE_TYPES.Property && id.parent.shorthand || id.parent.type === AST_NODE_TYPES.AssignmentPattern && id.parent.parent.type === AST_NODE_TYPES.Property && id.parent.parent.shorthand;
|
|
2852
|
+
}
|
|
2599
2853
|
function isExported(node, name, scope) {
|
|
2600
2854
|
if (node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration || node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) return true;
|
|
2601
2855
|
if (scope === null) return false;
|
|
@@ -2610,14 +2864,167 @@ function isGlobal(scope) {
|
|
|
2610
2864
|
if (scope === null) return false;
|
|
2611
2865
|
return scope.type === TSESLint.Scope.ScopeType.global || scope.type === TSESLint.Scope.ScopeType.module;
|
|
2612
2866
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2867
|
+
/**
|
|
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`).
|
|
2884
|
+
*
|
|
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:
|
|
2898
|
+
* - `const Colors = { RED: 'red', BLUE: 'blue' } as const`
|
|
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.
|
|
2907
|
+
*
|
|
2908
|
+
* @param node - The VariableDeclarator AST node to check.
|
|
2909
|
+
* @param parent - The parent VariableDeclaration node.
|
|
2910
|
+
* @returns True if this represents an object-style enum declaration.
|
|
2911
|
+
*/
|
|
2912
|
+
function isObjectStyleEnumDeclaration(node, parent) {
|
|
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);
|
|
3007
|
+
}
|
|
3008
|
+
function isValidIdentifierText(name, languageVersion) {
|
|
3009
|
+
if (name.length === 0) return false;
|
|
3010
|
+
const firstCodePoint = name.codePointAt(0);
|
|
3011
|
+
if (firstCodePoint === void 0 || !isIdentifierStart(firstCodePoint, languageVersion)) return false;
|
|
3012
|
+
let index = firstCodePoint > 65535 ? 2 : 1;
|
|
3013
|
+
while (index < name.length) {
|
|
3014
|
+
const codePoint = name.codePointAt(index);
|
|
3015
|
+
if (codePoint === void 0 || !isIdentifierPart(codePoint, languageVersion)) return false;
|
|
3016
|
+
index += codePoint > 65535 ? 2 : 1;
|
|
3017
|
+
}
|
|
3018
|
+
return true;
|
|
3019
|
+
}
|
|
3020
|
+
function requiresQuoting(node, target) {
|
|
3021
|
+
return !isValidIdentifierText(node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier ? node.name : `${node.value}`, target ?? ScriptTarget.Latest);
|
|
2615
3022
|
}
|
|
2616
3023
|
//#endregion
|
|
2617
3024
|
//#region src/rules/no-export-default-arrow/rule.ts
|
|
2618
|
-
const RULE_NAME$
|
|
3025
|
+
const RULE_NAME$11 = "no-export-default-arrow";
|
|
2619
3026
|
const MESSAGE_ID$6 = "disallowExportDefaultArrow";
|
|
2620
|
-
const messages$
|
|
3027
|
+
const messages$11 = { [MESSAGE_ID$6]: "Assign the arrow function to a named constant instead of exporting it anonymously." };
|
|
2621
3028
|
/**
|
|
2622
3029
|
* Converts a filename stem to camelCase, treating `-`, `_`, and whitespace as
|
|
2623
3030
|
* word separators (`use-mouse` -> `useMouse`).
|
|
@@ -2695,7 +3102,7 @@ function createFixFunction({ arrowFunction, context, exportDeclaration, sourceCo
|
|
|
2695
3102
|
* @param context - The rule context.
|
|
2696
3103
|
* @returns The rule listener.
|
|
2697
3104
|
*/
|
|
2698
|
-
function createOnce$
|
|
3105
|
+
function createOnce$7(context) {
|
|
2699
3106
|
return { ArrowFunctionExpression(node) {
|
|
2700
3107
|
const { parent } = node;
|
|
2701
3108
|
if (parent.type !== AST_NODE_TYPES.ExportDefaultDeclaration) return;
|
|
@@ -2712,8 +3119,8 @@ function createOnce$6(context) {
|
|
|
2712
3119
|
} };
|
|
2713
3120
|
}
|
|
2714
3121
|
const noExportDefaultArrow = createFlawlessRule({
|
|
2715
|
-
name: RULE_NAME$
|
|
2716
|
-
createOnce: createOnce$
|
|
3122
|
+
name: RULE_NAME$11,
|
|
3123
|
+
createOnce: createOnce$7,
|
|
2717
3124
|
defaultOptions: [],
|
|
2718
3125
|
meta: {
|
|
2719
3126
|
docs: {
|
|
@@ -2723,7 +3130,7 @@ const noExportDefaultArrow = createFlawlessRule({
|
|
|
2723
3130
|
},
|
|
2724
3131
|
fixable: "code",
|
|
2725
3132
|
hasSuggestions: false,
|
|
2726
|
-
messages: messages$
|
|
3133
|
+
messages: messages$11,
|
|
2727
3134
|
schema: [],
|
|
2728
3135
|
type: "suggestion"
|
|
2729
3136
|
}
|
|
@@ -2888,7 +3295,7 @@ function flatten(file, context) {
|
|
|
2888
3295
|
}
|
|
2889
3296
|
//#endregion
|
|
2890
3297
|
//#region src/rules/no-redundant-tsconfig-options/rule.ts
|
|
2891
|
-
const RULE_NAME$
|
|
3298
|
+
const RULE_NAME$10 = "no-redundant-tsconfig-options";
|
|
2892
3299
|
const MESSAGE_ID$5 = "redundant";
|
|
2893
3300
|
/**
|
|
2894
3301
|
* Compiler options TypeScript resolves case-insensitively; a child that re-sets
|
|
@@ -2924,7 +3331,7 @@ const PATH_KEYS = /* @__PURE__ */ new Set([
|
|
|
2924
3331
|
"tsBuildInfoFile",
|
|
2925
3332
|
"typeRoots"
|
|
2926
3333
|
]);
|
|
2927
|
-
const messages$
|
|
3334
|
+
const messages$10 = { [MESSAGE_ID$5]: "'{{option}}' is redundant: it is already set to this value by {{source}}. Remove it." };
|
|
2928
3335
|
/**
|
|
2929
3336
|
* Structural equality between two static JSON values. Strings compare
|
|
2930
3337
|
* case-insensitively when `caseInsensitive` is set (used for enum-valued
|
|
@@ -3096,7 +3503,7 @@ function create$3(context) {
|
|
|
3096
3503
|
} };
|
|
3097
3504
|
}
|
|
3098
3505
|
const noRedundantTsconfigOptions = createEslintRule({
|
|
3099
|
-
name: RULE_NAME$
|
|
3506
|
+
name: RULE_NAME$10,
|
|
3100
3507
|
create: create$3,
|
|
3101
3508
|
defaultOptions: [],
|
|
3102
3509
|
meta: {
|
|
@@ -3107,7 +3514,7 @@ const noRedundantTsconfigOptions = createEslintRule({
|
|
|
3107
3514
|
},
|
|
3108
3515
|
fixable: "code",
|
|
3109
3516
|
hasSuggestions: false,
|
|
3110
|
-
messages: messages$
|
|
3517
|
+
messages: messages$10,
|
|
3111
3518
|
schema: [],
|
|
3112
3519
|
type: "suggestion"
|
|
3113
3520
|
}
|
|
@@ -3350,15 +3757,15 @@ function resolveFactory(sourceCode, node) {
|
|
|
3350
3757
|
}
|
|
3351
3758
|
//#endregion
|
|
3352
3759
|
//#region src/rules/no-unnecessary-use-callback/rule.ts
|
|
3353
|
-
const RULE_NAME$
|
|
3760
|
+
const RULE_NAME$9 = "no-unnecessary-use-callback";
|
|
3354
3761
|
const MESSAGE_ID_DEFAULT$1 = "default";
|
|
3355
3762
|
const MESSAGE_ID_INSIDE_USE_EFFECT$1 = "noUnnecessaryUseCallbackInsideUseEffect";
|
|
3356
|
-
const messages$
|
|
3763
|
+
const messages$9 = {
|
|
3357
3764
|
[MESSAGE_ID_DEFAULT$1]: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary.",
|
|
3358
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."
|
|
3359
3766
|
};
|
|
3360
3767
|
const noUnnecessaryUseCallback = createFlawlessRule({
|
|
3361
|
-
name: RULE_NAME$
|
|
3768
|
+
name: RULE_NAME$9,
|
|
3362
3769
|
createOnce: createUnnecessaryHookRule({
|
|
3363
3770
|
hook: "useCallback",
|
|
3364
3771
|
messageIds: {
|
|
@@ -3374,22 +3781,22 @@ const noUnnecessaryUseCallback = createFlawlessRule({
|
|
|
3374
3781
|
requiresTypeChecking: false
|
|
3375
3782
|
},
|
|
3376
3783
|
hasSuggestions: false,
|
|
3377
|
-
messages: messages$
|
|
3784
|
+
messages: messages$9,
|
|
3378
3785
|
schema: [],
|
|
3379
3786
|
type: "suggestion"
|
|
3380
3787
|
}
|
|
3381
3788
|
});
|
|
3382
3789
|
//#endregion
|
|
3383
3790
|
//#region src/rules/no-unnecessary-use-memo/rule.ts
|
|
3384
|
-
const RULE_NAME$
|
|
3791
|
+
const RULE_NAME$8 = "no-unnecessary-use-memo";
|
|
3385
3792
|
const MESSAGE_ID_DEFAULT = "default";
|
|
3386
3793
|
const MESSAGE_ID_INSIDE_USE_EFFECT = "noUnnecessaryUseMemoInsideUseEffect";
|
|
3387
|
-
const messages$
|
|
3794
|
+
const messages$8 = {
|
|
3388
3795
|
[MESSAGE_ID_DEFAULT]: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary.",
|
|
3389
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."
|
|
3390
3797
|
};
|
|
3391
3798
|
const noUnnecessaryUseMemo = createFlawlessRule({
|
|
3392
|
-
name: RULE_NAME$
|
|
3799
|
+
name: RULE_NAME$8,
|
|
3393
3800
|
createOnce: createUnnecessaryHookRule({
|
|
3394
3801
|
hook: "useMemo",
|
|
3395
3802
|
messageIds: {
|
|
@@ -3405,16 +3812,16 @@ const noUnnecessaryUseMemo = createFlawlessRule({
|
|
|
3405
3812
|
requiresTypeChecking: false
|
|
3406
3813
|
},
|
|
3407
3814
|
hasSuggestions: false,
|
|
3408
|
-
messages: messages$
|
|
3815
|
+
messages: messages$8,
|
|
3409
3816
|
schema: [],
|
|
3410
3817
|
type: "suggestion"
|
|
3411
3818
|
}
|
|
3412
3819
|
});
|
|
3413
3820
|
//#endregion
|
|
3414
3821
|
//#region src/rules/padding-after-expect-assertions/rule.ts
|
|
3415
|
-
const RULE_NAME$
|
|
3822
|
+
const RULE_NAME$7 = "padding-after-expect-assertions";
|
|
3416
3823
|
const MESSAGE_ID$4 = "missingPadding";
|
|
3417
|
-
const messages$
|
|
3824
|
+
const messages$7 = { [MESSAGE_ID$4]: "Expected a blank line after '{{name}}'." };
|
|
3418
3825
|
/**
|
|
3419
3826
|
* Matches a statement that declares the expected assertion count at the top of
|
|
3420
3827
|
* a test: `expect.assertions(n)` or `expect.hasAssertions()`.
|
|
@@ -3489,7 +3896,7 @@ function findPaddingAnchor(sourceCode, node, nextNode) {
|
|
|
3489
3896
|
* @param context - The rule context.
|
|
3490
3897
|
* @returns The rule listener.
|
|
3491
3898
|
*/
|
|
3492
|
-
function createOnce$
|
|
3899
|
+
function createOnce$4(context) {
|
|
3493
3900
|
return { ExpressionStatement(node) {
|
|
3494
3901
|
const name = getAssertionName(node);
|
|
3495
3902
|
if (name === void 0) return;
|
|
@@ -3514,8 +3921,8 @@ function createOnce$3(context) {
|
|
|
3514
3921
|
} };
|
|
3515
3922
|
}
|
|
3516
3923
|
const paddingAfterExpectAssertions = createFlawlessRule({
|
|
3517
|
-
name: RULE_NAME$
|
|
3518
|
-
createOnce: createOnce$
|
|
3924
|
+
name: RULE_NAME$7,
|
|
3925
|
+
createOnce: createOnce$4,
|
|
3519
3926
|
defaultOptions: [],
|
|
3520
3927
|
meta: {
|
|
3521
3928
|
docs: {
|
|
@@ -3525,16 +3932,16 @@ const paddingAfterExpectAssertions = createFlawlessRule({
|
|
|
3525
3932
|
},
|
|
3526
3933
|
fixable: "whitespace",
|
|
3527
3934
|
hasSuggestions: false,
|
|
3528
|
-
messages: messages$
|
|
3935
|
+
messages: messages$7,
|
|
3529
3936
|
schema: [],
|
|
3530
3937
|
type: "layout"
|
|
3531
3938
|
}
|
|
3532
3939
|
});
|
|
3533
3940
|
//#endregion
|
|
3534
3941
|
//#region src/rules/prefer-destructuring-assignment/rule.ts
|
|
3535
|
-
const RULE_NAME$
|
|
3942
|
+
const RULE_NAME$6 = "prefer-destructuring-assignment";
|
|
3536
3943
|
const MESSAGE_ID$3 = "default";
|
|
3537
|
-
const messages$
|
|
3944
|
+
const messages$6 = { [MESSAGE_ID$3]: "Use destructuring assignment for component props." };
|
|
3538
3945
|
/**
|
|
3539
3946
|
* Identifiers that cannot be used as a binding name in strict-mode module code.
|
|
3540
3947
|
* A property may be accessed with such a name (`props.default`), but a shorthand
|
|
@@ -3696,7 +4103,7 @@ function reportComponent(context, scope, propsParameter, propertyVariable) {
|
|
|
3696
4103
|
* @param context - The rule context.
|
|
3697
4104
|
* @returns The rule listener.
|
|
3698
4105
|
*/
|
|
3699
|
-
function createOnce$
|
|
4106
|
+
function createOnce$3(context) {
|
|
3700
4107
|
let collector = core.getFunctionComponentCollector(context);
|
|
3701
4108
|
const selectorKeys = Object.keys(collector.visitor);
|
|
3702
4109
|
const listener = {
|
|
@@ -3722,8 +4129,8 @@ function createOnce$2(context) {
|
|
|
3722
4129
|
return listener;
|
|
3723
4130
|
}
|
|
3724
4131
|
const preferDestructuringAssignment = createFlawlessRule({
|
|
3725
|
-
name: RULE_NAME$
|
|
3726
|
-
createOnce: createOnce$
|
|
4132
|
+
name: RULE_NAME$6,
|
|
4133
|
+
createOnce: createOnce$3,
|
|
3727
4134
|
defaultOptions: [],
|
|
3728
4135
|
meta: {
|
|
3729
4136
|
docs: {
|
|
@@ -3733,16 +4140,16 @@ const preferDestructuringAssignment = createFlawlessRule({
|
|
|
3733
4140
|
},
|
|
3734
4141
|
fixable: "code",
|
|
3735
4142
|
hasSuggestions: false,
|
|
3736
|
-
messages: messages$
|
|
4143
|
+
messages: messages$6,
|
|
3737
4144
|
schema: [],
|
|
3738
4145
|
type: "problem"
|
|
3739
4146
|
}
|
|
3740
4147
|
});
|
|
3741
4148
|
//#endregion
|
|
3742
4149
|
//#region src/rules/prefer-parameter-destructuring/rule.ts
|
|
3743
|
-
const RULE_NAME$
|
|
4150
|
+
const RULE_NAME$5 = "prefer-parameter-destructuring";
|
|
3744
4151
|
const MESSAGE_ID$2 = "default";
|
|
3745
|
-
const messages$
|
|
4152
|
+
const messages$5 = { [MESSAGE_ID$2]: "Destructure parameter '{{name}}' in the function signature instead of the body." };
|
|
3746
4153
|
/**
|
|
3747
4154
|
* Collects the binding names introduced by a pattern (or parameter), including
|
|
3748
4155
|
* names nested in object/array patterns, defaults, and rest elements.
|
|
@@ -4017,7 +4424,7 @@ function planFix(query) {
|
|
|
4017
4424
|
* @param context - The rule context.
|
|
4018
4425
|
* @returns The rule listener.
|
|
4019
4426
|
*/
|
|
4020
|
-
function createOnce$
|
|
4427
|
+
function createOnce$2(context) {
|
|
4021
4428
|
let allowSideEffectReordering = true;
|
|
4022
4429
|
function checkFunction(node) {
|
|
4023
4430
|
const { body, params } = node;
|
|
@@ -4068,8 +4475,8 @@ function createOnce$1(context) {
|
|
|
4068
4475
|
};
|
|
4069
4476
|
}
|
|
4070
4477
|
const preferParameterDestructuring = createFlawlessRule({
|
|
4071
|
-
name: RULE_NAME$
|
|
4072
|
-
createOnce: createOnce$
|
|
4478
|
+
name: RULE_NAME$5,
|
|
4479
|
+
createOnce: createOnce$2,
|
|
4073
4480
|
defaultOptions: [{ allowSideEffectReordering: true }],
|
|
4074
4481
|
meta: {
|
|
4075
4482
|
defaultOptions: [{ allowSideEffectReordering: true }],
|
|
@@ -4080,7 +4487,7 @@ const preferParameterDestructuring = createFlawlessRule({
|
|
|
4080
4487
|
},
|
|
4081
4488
|
fixable: "code",
|
|
4082
4489
|
hasSuggestions: false,
|
|
4083
|
-
messages: messages$
|
|
4490
|
+
messages: messages$5,
|
|
4084
4491
|
schema: [{
|
|
4085
4492
|
additionalProperties: false,
|
|
4086
4493
|
properties: { allowSideEffectReordering: {
|
|
@@ -4362,9 +4769,9 @@ function isReadonlyPropertiesProperty(checker, type, property) {
|
|
|
4362
4769
|
}
|
|
4363
4770
|
//#endregion
|
|
4364
4771
|
//#region src/rules/prefer-read-only-props/rule.ts
|
|
4365
|
-
const RULE_NAME$
|
|
4772
|
+
const RULE_NAME$4 = "prefer-read-only-props";
|
|
4366
4773
|
const DEFAULT_WRAPPER_TYPE = "Readonly";
|
|
4367
|
-
const messages$
|
|
4774
|
+
const messages$4 = { preferReadOnlyProps: "A function component's props should be read-only." };
|
|
4368
4775
|
/** Names of the `React.forwardRef` wrapper whose props are its second type argument. */
|
|
4369
4776
|
const FORWARD_REF_NAMES = /* @__PURE__ */ new Set(["forwardRef"]);
|
|
4370
4777
|
/** Names of the `React.memo` wrapper whose props are its first type argument. */
|
|
@@ -4573,7 +4980,7 @@ function create$2(context) {
|
|
|
4573
4980
|
};
|
|
4574
4981
|
}
|
|
4575
4982
|
const preferReadOnlyProps = createEslintRule({
|
|
4576
|
-
name: RULE_NAME$
|
|
4983
|
+
name: RULE_NAME$4,
|
|
4577
4984
|
create: create$2,
|
|
4578
4985
|
defaultOptions: [{}],
|
|
4579
4986
|
meta: {
|
|
@@ -4585,7 +4992,7 @@ const preferReadOnlyProps = createEslintRule({
|
|
|
4585
4992
|
},
|
|
4586
4993
|
fixable: "code",
|
|
4587
4994
|
hasSuggestions: false,
|
|
4588
|
-
messages: messages$
|
|
4995
|
+
messages: messages$4,
|
|
4589
4996
|
schema: [{
|
|
4590
4997
|
additionalProperties: false,
|
|
4591
4998
|
properties: {
|
|
@@ -4610,7 +5017,7 @@ const preferReadOnlyProps = createEslintRule({
|
|
|
4610
5017
|
});
|
|
4611
5018
|
//#endregion
|
|
4612
5019
|
//#region src/rules/purity/rule.ts
|
|
4613
|
-
const RULE_NAME$
|
|
5020
|
+
const RULE_NAME$3 = "purity";
|
|
4614
5021
|
const MESSAGE_ID$1 = "impureCall";
|
|
4615
5022
|
/**
|
|
4616
5023
|
* Non-deterministic Luau / Roblox calls, written as dotted paths matching how
|
|
@@ -4643,7 +5050,7 @@ const GUARDED_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
4643
5050
|
"tick",
|
|
4644
5051
|
"time"
|
|
4645
5052
|
]);
|
|
4646
|
-
const messages$
|
|
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." };
|
|
4647
5054
|
const schema$1 = [{
|
|
4648
5055
|
additionalProperties: false,
|
|
4649
5056
|
properties: {
|
|
@@ -4743,7 +5150,7 @@ function isRenderContext(reactContext, func) {
|
|
|
4743
5150
|
const { parent } = func;
|
|
4744
5151
|
return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments[0] === func && core.isUseMemoCall(reactContext, parent);
|
|
4745
5152
|
}
|
|
4746
|
-
function createOnce(context) {
|
|
5153
|
+
function createOnce$1(context) {
|
|
4747
5154
|
const reactContext = context;
|
|
4748
5155
|
let signatures;
|
|
4749
5156
|
function handle(node, match, path) {
|
|
@@ -4776,8 +5183,8 @@ function createOnce(context) {
|
|
|
4776
5183
|
};
|
|
4777
5184
|
}
|
|
4778
5185
|
const purity = createFlawlessRule({
|
|
4779
|
-
name: RULE_NAME$
|
|
4780
|
-
createOnce,
|
|
5186
|
+
name: RULE_NAME$3,
|
|
5187
|
+
createOnce: createOnce$1,
|
|
4781
5188
|
defaultOptions: [{}],
|
|
4782
5189
|
meta: {
|
|
4783
5190
|
defaultOptions: [{}],
|
|
@@ -4787,12 +5194,314 @@ const purity = createFlawlessRule({
|
|
|
4787
5194
|
requiresTypeChecking: false
|
|
4788
5195
|
},
|
|
4789
5196
|
hasSuggestions: false,
|
|
4790
|
-
messages: messages$
|
|
5197
|
+
messages: messages$3,
|
|
4791
5198
|
schema: schema$1,
|
|
4792
5199
|
type: "problem"
|
|
4793
5200
|
}
|
|
4794
5201
|
});
|
|
4795
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
|
|
4796
5505
|
//#region src/rules/toml-sort-keys/rule.ts
|
|
4797
5506
|
const RULE_NAME$1 = "toml-sort-keys";
|
|
4798
5507
|
const MESSAGE_ID = "unsorted";
|
|
@@ -5105,6 +5814,7 @@ const plugin = {
|
|
|
5105
5814
|
"prefer-parameter-destructuring": preferParameterDestructuring,
|
|
5106
5815
|
"prefer-read-only-props": preferReadOnlyProps,
|
|
5107
5816
|
"purity": purity,
|
|
5817
|
+
"react-namespace": reactNamespace,
|
|
5108
5818
|
"toml-sort-keys": tomlSortKeys,
|
|
5109
5819
|
"yaml-block-key-blank-lines": yamlBlockKeyBlankLines
|
|
5110
5820
|
}
|