exadev-eslint-config 2.19.4 → 2.20.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 +4 -0
- package/dist/index.cjs +200 -18
- package/dist/index.js +201 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ export default defineConfig(
|
|
|
57
57
|
- `exadev/prefer-readonly-array-param`
|
|
58
58
|
- `exadev/prefer-readonly-object-param`
|
|
59
59
|
- `exadev/prefer-numeric-sort-compare`
|
|
60
|
+
- `exadev/prefer-options-object-param`
|
|
60
61
|
- `exadev/no-pointless-reassignment`
|
|
61
62
|
- `exadev/test-file-kind`
|
|
62
63
|
|
|
@@ -84,6 +85,8 @@ export default defineConfig(
|
|
|
84
85
|
- **[`no-magic-numbers`](https://typescript-eslint.io/rules/no-magic-numbers/)** — tuned to exempt array indexes, enum members, readonly class properties, default parameter values, numeric literal types (e.g. `type Indent = 2 | 4`), and the handful of universally-idiomatic bare numbers (`-1`, `0`, `1`, `2`).
|
|
85
86
|
- **[`max-lines`](https://eslint.org/docs/latest/rules/max-lines)** set to `{ max: 800, skipBlankLines: true, skipComments: true }`.
|
|
86
87
|
- *Why:* counting only real code means a file isn't pushed over the limit by whitespace or its own WHY-explanation comments.
|
|
88
|
+
- **[`max-params`](https://eslint.org/docs/latest/rules/max-params)** set to `{ max: 4 }`, one above the rule's own default of `3`. Needs no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export, like `exadev/prefer-readonly-array-param` above.
|
|
89
|
+
- *Why:* a deliberately loose backstop against a genuinely excessive number of REQUIRED parameters, distinct from `exadev/prefer-options-object-param` above, which already offers a real fix for the more common shape (a run of 2+ trailing OPTIONAL parameters) this rule structurally cannot see until the total count crosses its own threshold.
|
|
87
90
|
- **[`no-warning-comments`](https://eslint.org/docs/latest/rules/no-warning-comments)** — bans any comment containing `Stryker disable`.
|
|
88
91
|
- *Why:* that's Stryker's own mutation-testing suppression directive, invisible to `noInlineConfig` above since it isn't an eslint-disable comment.
|
|
89
92
|
|
|
@@ -281,6 +284,7 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
281
284
|
| `no-enum-number-widening` | | **A bare `number` is accepted anywhere a numeric enum is expected**, without checking it is actually one of the enum's members — only a numeric *literal* gets range-checked by `tsc`. No autofix: the only provably safe fix is a genuine runtime membership check against the enum's own values, which is a behavioural choice a mechanical fix cannot responsibly make. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
282
285
|
| `no-enum-reverse-lookup-widening` | suggestion | **A numeric enum's reverse lookup can silently type as `string` for an out-of-range index.** Indexing a numeric enum's reverse mapping (`Direction[n]`) with a bare (non-literal) `number`, or with a different enum's member, types as plain `string` for any index, including one outside the enum's actual members, where it genuinely returns `undefined` at runtime — `tsc` does not range-check even a numeric literal index here. When the indexed expression is the init of a variable with an explicit `: string` annotation, a suggestion widens it to `: string \| undefined`, forcing later uses as a bare `string` to surface as real compile errors; every other syntactic position gets a plain report with no fix, and no case gets a full `--fix` autofix. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`. |
|
|
283
286
|
| `prefer-numeric-sort-compare` | suggestion | **`.sort()` on a number array sorts lexicographically by default.** A deliberately narrow addition alongside [`@typescript-eslint/require-array-sort-compare`](https://typescript-eslint.io/rules/require-array-sort-compare/) (which already flags any bare `.sort()`/`.toSorted()` except on a plain string array, with no fix): when the array's element type is definitively `number`, a suggestion offers an ascending compare function (`(a, b) => a - b`), since the default comparator sorts lexicographically (`[1, 2, 10].sort()` becomes `[1, 10, 2]`). Not a full autofix — descending order is a real, if less common, alternative intent. Requires type information — only in the default (type-checked) export, not `plugin.configs.recommended`, since it needs the checker to confirm the array's element type. |
|
|
287
|
+
| `prefer-options-object-param` | suggestion | **A run of 2+ trailing optional parameters (`?`-marked or default-valued) should be bundled into one destructured `options` parameter.** Without this, a caller needing only the last optional parameter must still pass `undefined` for every optional parameter before it (the motivating case: a 10-parameter constructor with 8 trailing optional ones). The threshold is configurable (`{ minTrailingOptional }`, default `2`). A suggestion (not a full autofix) rewrites the parameter list and inserts a `const { ... } = options ?? {};` destructure as the body's first statement; call sites are never rewritten, since the signature edit alone turns every stale positional call site into a real compile error. Still reported, but with no suggestion offered, whenever collapsing the run would not be safe/mechanical: a parameter property in the run, a parameter with no simple resolvable name and explicit type of its own (including a destructured parameter, or one typed only through a separately-declared function-type alias), a decorated parameter, a rest parameter anywhere in the full parameter list, no `BlockStatement` body at all (an arrow's expression body, or any declaration-only signature — including an ambient `.d.ts` function, an interface method/construct signature, or an abstract/ambient class method), a `@param` JSDoc tag naming a parameter in the run, or a parameter/function-scope-local variable already named `options`. Complements `max-params` (see "Individual rule tuning" above), which catches the different shape (excessive REQUIRED parameters) this rule is deliberately blind to. Requires no type information, but is registered only in the default (type-checked) export — alongside `prefer-readonly-object-param` and `prefer-numeric-sort-compare` above, for a consistent "every rewrite-suggestion `prefer-*` rule lives here" story, not because it needs the checker for anything. |
|
|
284
288
|
| `package-json-key-order` | ✓ | **Requires `package.json`'s keys to match `syncpack format`'s order.** See [Optional package.json key ordering](#optional-packagejson-key-ordering) — opt-in via `exadevConfig({ packageJsonKeyOrder: true })`, not part of `recommended`/`barrel`. A JSON-language rule (`@eslint/json`'s `json/json`), not a TSESLint one — needs no type information and doesn't apply to any `.ts`/`.js` file. |
|
|
285
289
|
| `test-file-kind` | | **A test file's name must declare its own test kind.** A filename suffix immediately before `.test`/`.spec` (e.g. `foo.unit.test.ts`), one of a configurable `{ kinds }` set (default: `unit`, `integration`, `e2e`). A naming-discipline rule, not a content classifier — it checks only the filename, never what the file actually tests. Self-scoped to real test/spec files (`context.filename`), so it never misfires when applied unscoped and never relies on a consumer's own `files` config. Requires no type information. |
|
|
286
290
|
|
package/dist/index.cjs
CHANGED
|
@@ -295,7 +295,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
295
295
|
}
|
|
296
296
|
//#endregion
|
|
297
297
|
//#region package.json
|
|
298
|
-
var version = "2.
|
|
298
|
+
var version = "2.20.0";
|
|
299
299
|
//#endregion
|
|
300
300
|
//#region src/react.ts
|
|
301
301
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -653,7 +653,7 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
|
653
653
|
"fill",
|
|
654
654
|
"copyWithin"
|
|
655
655
|
]);
|
|
656
|
-
const createRule$
|
|
656
|
+
const createRule$8 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
657
657
|
function isArrayIsArrayCall(node) {
|
|
658
658
|
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && node.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
659
659
|
}
|
|
@@ -665,7 +665,7 @@ function definitelyExits$2(statement) {
|
|
|
665
665
|
}
|
|
666
666
|
return false;
|
|
667
667
|
}
|
|
668
|
-
const noArrayIsArrayMutation = createRule$
|
|
668
|
+
const noArrayIsArrayMutation = createRule$8({
|
|
669
669
|
name: "no-array-isarray-mutation",
|
|
670
670
|
meta: {
|
|
671
671
|
type: "problem",
|
|
@@ -822,6 +822,11 @@ function lastTokenOrThrow(sourceCode, node) {
|
|
|
822
822
|
if (token === null) throw new Error("Unreachable: getLastToken returned null for a node expected to always have at least one token.");
|
|
823
823
|
return token;
|
|
824
824
|
}
|
|
825
|
+
function firstTokenOrThrow(sourceCode, node) {
|
|
826
|
+
const token = sourceCode.getFirstToken(node);
|
|
827
|
+
if (token === null) throw new Error("Unreachable: getFirstToken returned null for a node expected to always have at least one token.");
|
|
828
|
+
return token;
|
|
829
|
+
}
|
|
825
830
|
//#endregion
|
|
826
831
|
//#region src/rules/no-enum-number-widening.ts
|
|
827
832
|
const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
@@ -944,7 +949,7 @@ const noIndexFiles = {
|
|
|
944
949
|
};
|
|
945
950
|
//#endregion
|
|
946
951
|
//#region src/rules/no-map-instanceof-mutation.ts
|
|
947
|
-
const createRule$
|
|
952
|
+
const createRule$7 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
948
953
|
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
949
954
|
"set",
|
|
950
955
|
"delete",
|
|
@@ -961,7 +966,7 @@ function definitelyExits$1(statement) {
|
|
|
961
966
|
}
|
|
962
967
|
return false;
|
|
963
968
|
}
|
|
964
|
-
const noMapInstanceofMutation = createRule$
|
|
969
|
+
const noMapInstanceofMutation = createRule$7({
|
|
965
970
|
name: "no-map-instanceof-mutation",
|
|
966
971
|
meta: {
|
|
967
972
|
type: "problem",
|
|
@@ -1042,7 +1047,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
1042
1047
|
"fill",
|
|
1043
1048
|
"copyWithin"
|
|
1044
1049
|
]);
|
|
1045
|
-
const createRule$
|
|
1050
|
+
const createRule$6 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1046
1051
|
function buildReadonlyArrayFix(annotated, fixer) {
|
|
1047
1052
|
if (annotated.type === _typescript_eslint_utils.AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
1048
1053
|
if (annotated.type !== _typescript_eslint_utils.AST_NODE_TYPES.TSTypeReference) throw new Error(`Unreachable: expected a TSArrayType or TSTypeReference, got ${annotated.type} instead.`);
|
|
@@ -1055,7 +1060,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
1055
1060
|
if (firstParam?.type === _typescript_eslint_utils.AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
1056
1061
|
}
|
|
1057
1062
|
}
|
|
1058
|
-
const noMutableUnionArrayParam = createRule$
|
|
1063
|
+
const noMutableUnionArrayParam = createRule$6({
|
|
1059
1064
|
name: "no-mutable-union-array-param",
|
|
1060
1065
|
meta: {
|
|
1061
1066
|
type: "problem",
|
|
@@ -1108,7 +1113,7 @@ const noNonBarrelIndex = {
|
|
|
1108
1113
|
};
|
|
1109
1114
|
//#endregion
|
|
1110
1115
|
//#region src/rules/no-non-barrel-reexport.ts
|
|
1111
|
-
function removeListMember(fixer, sourceCode, declaration, members, target) {
|
|
1116
|
+
function removeListMember({ fixer, sourceCode }, declaration, members, target) {
|
|
1112
1117
|
if (members.length === 1) return fixer.remove(declaration);
|
|
1113
1118
|
const targetIndex = members.indexOf(target);
|
|
1114
1119
|
const isLast = targetIndex === members.length - 1;
|
|
@@ -1156,8 +1161,14 @@ const noNonBarrelReexport = {
|
|
|
1156
1161
|
messageId: "splitStatementReexport",
|
|
1157
1162
|
data: { name },
|
|
1158
1163
|
fix(fixer) {
|
|
1159
|
-
const fixes = [removeListMember(
|
|
1160
|
-
|
|
1164
|
+
const fixes = [removeListMember({
|
|
1165
|
+
fixer,
|
|
1166
|
+
sourceCode
|
|
1167
|
+
}, declaration, declaration.specifiers, specifier)];
|
|
1168
|
+
if (specifier.local.type === "Identifier" && importIsOnlyUsedByThisExport(sourceCode, trackedImport, specifier.local)) fixes.push(removeListMember({
|
|
1169
|
+
fixer,
|
|
1170
|
+
sourceCode
|
|
1171
|
+
}, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
1161
1172
|
return fixes;
|
|
1162
1173
|
}
|
|
1163
1174
|
});
|
|
@@ -1169,7 +1180,10 @@ const noNonBarrelReexport = {
|
|
|
1169
1180
|
data: { name },
|
|
1170
1181
|
fix(fixer) {
|
|
1171
1182
|
const fixes = [fixer.remove(declaration)];
|
|
1172
|
-
if (importIsOnlyUsedByThisExport(sourceCode, trackedImport, identifierNode)) fixes.push(removeListMember(
|
|
1183
|
+
if (importIsOnlyUsedByThisExport(sourceCode, trackedImport, identifierNode)) fixes.push(removeListMember({
|
|
1184
|
+
fixer,
|
|
1185
|
+
sourceCode
|
|
1186
|
+
}, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
1173
1187
|
return fixes;
|
|
1174
1188
|
}
|
|
1175
1189
|
});
|
|
@@ -1180,14 +1194,14 @@ const noNonBarrelReexport = {
|
|
|
1180
1194
|
};
|
|
1181
1195
|
//#endregion
|
|
1182
1196
|
//#region src/rules/no-object-assign.ts
|
|
1183
|
-
const createRule$
|
|
1197
|
+
const createRule$5 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1184
1198
|
function resolveFrom$1(scope, name) {
|
|
1185
1199
|
for (let current = scope; current; current = current.upper) {
|
|
1186
1200
|
const found = current.set.get(name);
|
|
1187
1201
|
if (found) return found;
|
|
1188
1202
|
}
|
|
1189
1203
|
}
|
|
1190
|
-
const noObjectAssign = createRule$
|
|
1204
|
+
const noObjectAssign = createRule$5({
|
|
1191
1205
|
name: "no-object-assign",
|
|
1192
1206
|
meta: {
|
|
1193
1207
|
type: "problem",
|
|
@@ -1333,7 +1347,7 @@ const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
|
1333
1347
|
"delete",
|
|
1334
1348
|
"clear"
|
|
1335
1349
|
]);
|
|
1336
|
-
const createRule$
|
|
1350
|
+
const createRule$4 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1337
1351
|
function isSetInstanceofExpression(node) {
|
|
1338
1352
|
return node.type === _typescript_eslint_utils.AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && node.right.name === "Set";
|
|
1339
1353
|
}
|
|
@@ -1345,7 +1359,7 @@ function definitelyExits(statement) {
|
|
|
1345
1359
|
}
|
|
1346
1360
|
return false;
|
|
1347
1361
|
}
|
|
1348
|
-
const noSetInstanceofMutation = createRule$
|
|
1362
|
+
const noSetInstanceofMutation = createRule$4({
|
|
1349
1363
|
name: "no-set-instanceof-mutation",
|
|
1350
1364
|
meta: {
|
|
1351
1365
|
type: "problem",
|
|
@@ -1639,13 +1653,13 @@ const packageJsonKeyOrder = {
|
|
|
1639
1653
|
};
|
|
1640
1654
|
//#endregion
|
|
1641
1655
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1642
|
-
const createRule$
|
|
1656
|
+
const createRule$3 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1643
1657
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
1644
1658
|
function isDefinitelyNumberType(type) {
|
|
1645
1659
|
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
1646
1660
|
return (type.flags & typescript.TypeFlags.NumberLike) !== 0;
|
|
1647
1661
|
}
|
|
1648
|
-
const preferNumericSortCompare = createRule$
|
|
1662
|
+
const preferNumericSortCompare = createRule$3({
|
|
1649
1663
|
name: "prefer-numeric-sort-compare",
|
|
1650
1664
|
meta: {
|
|
1651
1665
|
type: "suggestion",
|
|
@@ -1687,6 +1701,170 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1687
1701
|
}
|
|
1688
1702
|
});
|
|
1689
1703
|
//#endregion
|
|
1704
|
+
//#region src/rules/prefer-options-object-param.ts
|
|
1705
|
+
const createRule$2 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1706
|
+
const FUNCTION_LIKE_SELECTOR$2 = [
|
|
1707
|
+
"ArrowFunctionExpression",
|
|
1708
|
+
"FunctionDeclaration",
|
|
1709
|
+
"FunctionExpression",
|
|
1710
|
+
"TSCallSignatureDeclaration",
|
|
1711
|
+
"TSConstructSignatureDeclaration",
|
|
1712
|
+
"TSDeclareFunction",
|
|
1713
|
+
"TSEmptyBodyFunctionExpression",
|
|
1714
|
+
"TSFunctionType",
|
|
1715
|
+
"TSMethodSignature"
|
|
1716
|
+
].join(", ");
|
|
1717
|
+
function isOptionalParam(param) {
|
|
1718
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return isOptionalParam(param.parameter);
|
|
1719
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern) return true;
|
|
1720
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return param.optional;
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
function getTrailingOptionalRun(params) {
|
|
1724
|
+
const hasTrailingRest = params.length > 0 && params[params.length - 1]?.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement;
|
|
1725
|
+
const run = [];
|
|
1726
|
+
for (let i = params.length - (hasTrailingRest ? 2 : 1); i >= 0; i--) {
|
|
1727
|
+
const param = params[i];
|
|
1728
|
+
if (param === void 0 || !isOptionalParam(param)) break;
|
|
1729
|
+
run.unshift(param);
|
|
1730
|
+
}
|
|
1731
|
+
return run;
|
|
1732
|
+
}
|
|
1733
|
+
function resolveFixableParam(param) {
|
|
1734
|
+
if (param.type === _typescript_eslint_utils.AST_NODE_TYPES.TSParameterProperty) return void 0;
|
|
1735
|
+
const identifierNode = param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern ? param.left.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param.left : void 0 : param.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier ? param : void 0;
|
|
1736
|
+
if (identifierNode === void 0) return void 0;
|
|
1737
|
+
const typeNode = identifierNode.typeAnnotation?.typeAnnotation;
|
|
1738
|
+
if (typeNode === void 0) return void 0;
|
|
1739
|
+
if (identifierNode.decorators.length > 0) return void 0;
|
|
1740
|
+
return {
|
|
1741
|
+
identifierNode,
|
|
1742
|
+
typeNode,
|
|
1743
|
+
defaultExpression: param.type === _typescript_eslint_utils.AST_NODE_TYPES.AssignmentPattern ? param.right : void 0
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function isDefined(value) {
|
|
1747
|
+
return value !== void 0;
|
|
1748
|
+
}
|
|
1749
|
+
function firstAndLastOrThrow(items) {
|
|
1750
|
+
const first = items[0];
|
|
1751
|
+
const last = items.at(-1);
|
|
1752
|
+
if (first === void 0 || last === void 0) throw new Error("Unreachable: expected at least one element (the caller already confirmed a minimum run length).");
|
|
1753
|
+
return [first, last];
|
|
1754
|
+
}
|
|
1755
|
+
function describeFunctionKind(node) {
|
|
1756
|
+
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSConstructSignatureDeclaration) return "constructor";
|
|
1757
|
+
if (node.type === _typescript_eslint_utils.AST_NODE_TYPES.TSMethodSignature) return "method";
|
|
1758
|
+
const { parent } = node;
|
|
1759
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.MethodDefinition || parent.type === _typescript_eslint_utils.AST_NODE_TYPES.TSAbstractMethodDefinition) return parent.kind === "constructor" ? "constructor" : "method";
|
|
1760
|
+
if (parent.type === _typescript_eslint_utils.AST_NODE_TYPES.Property && parent.method) return "method";
|
|
1761
|
+
return "function";
|
|
1762
|
+
}
|
|
1763
|
+
function getBlockBody(node) {
|
|
1764
|
+
return "body" in node && node.body?.type === _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement ? node.body : void 0;
|
|
1765
|
+
}
|
|
1766
|
+
const LIFTABLE_JSDOC_PARENTS = /* @__PURE__ */ new Set([
|
|
1767
|
+
_typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator,
|
|
1768
|
+
_typescript_eslint_utils.AST_NODE_TYPES.VariableDeclaration,
|
|
1769
|
+
_typescript_eslint_utils.AST_NODE_TYPES.MethodDefinition,
|
|
1770
|
+
_typescript_eslint_utils.AST_NODE_TYPES.TSAbstractMethodDefinition,
|
|
1771
|
+
_typescript_eslint_utils.AST_NODE_TYPES.PropertyDefinition,
|
|
1772
|
+
_typescript_eslint_utils.AST_NODE_TYPES.ExportNamedDeclaration,
|
|
1773
|
+
_typescript_eslint_utils.AST_NODE_TYPES.ExportDefaultDeclaration
|
|
1774
|
+
]);
|
|
1775
|
+
function getLeadingJSDocComment(sourceCode, node) {
|
|
1776
|
+
let current = node;
|
|
1777
|
+
for (;;) {
|
|
1778
|
+
const jsdocComment = sourceCode.getCommentsBefore(current).findLast((comment) => comment.type === _typescript_eslint_utils.AST_TOKEN_TYPES.Block && comment.value.startsWith("*"));
|
|
1779
|
+
if (jsdocComment) return jsdocComment;
|
|
1780
|
+
const { parent } = current;
|
|
1781
|
+
if (parent === void 0 || !LIFTABLE_JSDOC_PARENTS.has(parent.type)) return void 0;
|
|
1782
|
+
current = parent;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function jsDocMentionsParam(commentValue, name) {
|
|
1786
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1787
|
+
return new RegExp(`@param\\s+(?:\\{[^}]*\\}\\s+)?\\[?${escapedName}\\b`).test(commentValue);
|
|
1788
|
+
}
|
|
1789
|
+
function hasOptionsNameCollision(scope, exemptIdentifiers) {
|
|
1790
|
+
return scope.variables.some((variable) => variable.name === "options" && !variable.identifiers.every((identifier) => exemptIdentifiers.has(identifier)));
|
|
1791
|
+
}
|
|
1792
|
+
const preferOptionsObjectParam = createRule$2({
|
|
1793
|
+
name: "prefer-options-object-param",
|
|
1794
|
+
meta: {
|
|
1795
|
+
type: "suggestion",
|
|
1796
|
+
hasSuggestions: true,
|
|
1797
|
+
docs: { description: "Suggest bundling a run of 2+ trailing optional parameters into a single destructured 'options' parameter — without this, a caller needing only the last optional parameter must still pass 'undefined' for every optional parameter before it." },
|
|
1798
|
+
schema: [{
|
|
1799
|
+
type: "object",
|
|
1800
|
+
properties: { minTrailingOptional: {
|
|
1801
|
+
type: "integer",
|
|
1802
|
+
minimum: 2
|
|
1803
|
+
} },
|
|
1804
|
+
additionalProperties: false
|
|
1805
|
+
}],
|
|
1806
|
+
messages: {
|
|
1807
|
+
tooManyTrailingOptional: "This {{ kind }} has {{ count }} trailing optional parameters ({{ names }}) — a caller needing only the last one must still pass 'undefined' for every parameter before it. Bundle the trailing optional run into a single destructured 'options' parameter instead.",
|
|
1808
|
+
wrapInOptionsObject: "Bundle the trailing optional parameters into a single 'options' parameter."
|
|
1809
|
+
},
|
|
1810
|
+
defaultOptions: [{ minTrailingOptional: 2 }]
|
|
1811
|
+
},
|
|
1812
|
+
create(context, [{ minTrailingOptional }]) {
|
|
1813
|
+
const { sourceCode } = context;
|
|
1814
|
+
function checkParams(node) {
|
|
1815
|
+
const run = getTrailingOptionalRun(node.params);
|
|
1816
|
+
if (run.length < minTrailingOptional) return;
|
|
1817
|
+
const resolved = run.map(resolveFixableParam);
|
|
1818
|
+
const names = run.map((param, index) => resolved[index]?.identifierNode.name ?? sourceCode.getText(param)).join(", ");
|
|
1819
|
+
const data = {
|
|
1820
|
+
kind: describeFunctionKind(node),
|
|
1821
|
+
count: run.length,
|
|
1822
|
+
names
|
|
1823
|
+
};
|
|
1824
|
+
const resolvedParams = resolved.filter(isDefined);
|
|
1825
|
+
const allResolvable = resolvedParams.length === run.length;
|
|
1826
|
+
const hasRestParam = node.params.some((param) => param.type === _typescript_eslint_utils.AST_NODE_TYPES.RestElement);
|
|
1827
|
+
const body = getBlockBody(node);
|
|
1828
|
+
let isFixable = allResolvable && !hasRestParam && body !== void 0;
|
|
1829
|
+
if (isFixable) {
|
|
1830
|
+
const jsdocComment = getLeadingJSDocComment(sourceCode, node);
|
|
1831
|
+
const jsDocBail = jsdocComment !== void 0 && resolvedParams.some((info) => jsDocMentionsParam(jsdocComment.value, info.identifierNode.name));
|
|
1832
|
+
const exemptIdentifiers = new Set(resolvedParams.map((info) => info.identifierNode));
|
|
1833
|
+
const optionsCollision = hasOptionsNameCollision(sourceCode.getScope(node), exemptIdentifiers);
|
|
1834
|
+
isFixable = !jsDocBail && !optionsCollision;
|
|
1835
|
+
}
|
|
1836
|
+
if (!isFixable || body === void 0) {
|
|
1837
|
+
context.report({
|
|
1838
|
+
node,
|
|
1839
|
+
messageId: "tooManyTrailingOptional",
|
|
1840
|
+
data
|
|
1841
|
+
});
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
context.report({
|
|
1845
|
+
node,
|
|
1846
|
+
messageId: "tooManyTrailingOptional",
|
|
1847
|
+
data,
|
|
1848
|
+
suggest: [{
|
|
1849
|
+
messageId: "wrapInOptionsObject",
|
|
1850
|
+
fix(fixer) {
|
|
1851
|
+
const properties = resolvedParams.map((info) => `${info.identifierNode.name}?: ${sourceCode.getText(info.typeNode)}`);
|
|
1852
|
+
const destructureEntries = resolvedParams.map((info) => info.defaultExpression === void 0 ? info.identifierNode.name : `${info.identifierNode.name} = ${sourceCode.getText(info.defaultExpression)}`);
|
|
1853
|
+
const optionsParamText = `options?: { ${properties.join("; ")} }`;
|
|
1854
|
+
const destructureText = `const { ${destructureEntries.join(", ")} } = options ?? {};`;
|
|
1855
|
+
const [firstParam, lastParam] = firstAndLastOrThrow(run);
|
|
1856
|
+
const openBrace = firstTokenOrThrow(sourceCode, body);
|
|
1857
|
+
return [fixer.replaceTextRange([firstParam.range[0], lastParam.range[1]], optionsParamText), fixer.insertTextAfter(openBrace, `\n ${destructureText}`)];
|
|
1858
|
+
}
|
|
1859
|
+
}]
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
return { [FUNCTION_LIKE_SELECTOR$2](node) {
|
|
1863
|
+
checkParams(node);
|
|
1864
|
+
} };
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
//#endregion
|
|
1690
1868
|
//#region src/rules/prefer-readonly-array-param.ts
|
|
1691
1869
|
const createRule$1 = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1692
1870
|
function getFixableArrayOrTupleType(typeNode) {
|
|
@@ -1904,6 +2082,7 @@ const plugin = {
|
|
|
1904
2082
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1905
2083
|
"package-json-key-order": packageJsonKeyOrder,
|
|
1906
2084
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
2085
|
+
"prefer-options-object-param": preferOptionsObjectParam,
|
|
1907
2086
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1908
2087
|
"prefer-readonly-object-param": preferReadonlyObjectParam,
|
|
1909
2088
|
"test-file-kind": {
|
|
@@ -1966,7 +2145,8 @@ const plugin = {
|
|
|
1966
2145
|
"exadev/no-object-assign": "error",
|
|
1967
2146
|
"exadev/no-pointless-reassignment": "error",
|
|
1968
2147
|
"exadev/prefer-readonly-array-param": "error",
|
|
1969
|
-
"exadev/test-file-kind": "error"
|
|
2148
|
+
"exadev/test-file-kind": "error",
|
|
2149
|
+
"max-params": ["error", { max: 4 }]
|
|
1970
2150
|
}
|
|
1971
2151
|
};
|
|
1972
2152
|
},
|
|
@@ -2078,6 +2258,7 @@ const recommendedTypeChecked = [
|
|
|
2078
2258
|
"exadev/no-pointless-reassignment": "error",
|
|
2079
2259
|
"exadev/no-set-instanceof-mutation": "error",
|
|
2080
2260
|
"exadev/prefer-numeric-sort-compare": "error",
|
|
2261
|
+
"exadev/prefer-options-object-param": "error",
|
|
2081
2262
|
"exadev/prefer-readonly-array-param": "error",
|
|
2082
2263
|
"exadev/prefer-readonly-object-param": "error",
|
|
2083
2264
|
"exadev/test-file-kind": "error",
|
|
@@ -2115,6 +2296,7 @@ const recommendedTypeChecked = [
|
|
|
2115
2296
|
skipBlankLines: true,
|
|
2116
2297
|
skipComments: true
|
|
2117
2298
|
}],
|
|
2299
|
+
"max-params": ["error", { max: 4 }],
|
|
2118
2300
|
"no-warning-comments": ["error", {
|
|
2119
2301
|
terms: ["stryker disable"],
|
|
2120
2302
|
location: "anywhere"
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import jsdoc from "eslint-plugin-jsdoc";
|
|
|
4
4
|
import tsdoc from "eslint-plugin-tsdoc";
|
|
5
5
|
import jsonCanonical from "eslint-plugin-json-canonical";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
7
|
+
import { AST_NODE_TYPES, AST_TOKEN_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
8
8
|
import * as ts from "typescript";
|
|
9
9
|
import { isPropertyReadonlyInType, isTypeReference } from "ts-api-utils";
|
|
10
10
|
import js from "@eslint/js";
|
|
@@ -261,7 +261,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
263
|
//#region package.json
|
|
264
|
-
var version = "2.
|
|
264
|
+
var version = "2.20.0";
|
|
265
265
|
//#endregion
|
|
266
266
|
//#region src/react.ts
|
|
267
267
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -619,7 +619,7 @@ const MUTATING_INSERT_METHODS$1 = /* @__PURE__ */ new Set([
|
|
|
619
619
|
"fill",
|
|
620
620
|
"copyWithin"
|
|
621
621
|
]);
|
|
622
|
-
const createRule$
|
|
622
|
+
const createRule$8 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
623
623
|
function isArrayIsArrayCall(node) {
|
|
624
624
|
return node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES.Identifier && node.callee.object.name === "Array" && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "isArray";
|
|
625
625
|
}
|
|
@@ -631,7 +631,7 @@ function definitelyExits$2(statement) {
|
|
|
631
631
|
}
|
|
632
632
|
return false;
|
|
633
633
|
}
|
|
634
|
-
const noArrayIsArrayMutation = createRule$
|
|
634
|
+
const noArrayIsArrayMutation = createRule$8({
|
|
635
635
|
name: "no-array-isarray-mutation",
|
|
636
636
|
meta: {
|
|
637
637
|
type: "problem",
|
|
@@ -788,6 +788,11 @@ function lastTokenOrThrow(sourceCode, node) {
|
|
|
788
788
|
if (token === null) throw new Error("Unreachable: getLastToken returned null for a node expected to always have at least one token.");
|
|
789
789
|
return token;
|
|
790
790
|
}
|
|
791
|
+
function firstTokenOrThrow(sourceCode, node) {
|
|
792
|
+
const token = sourceCode.getFirstToken(node);
|
|
793
|
+
if (token === null) throw new Error("Unreachable: getFirstToken returned null for a node expected to always have at least one token.");
|
|
794
|
+
return token;
|
|
795
|
+
}
|
|
791
796
|
//#endregion
|
|
792
797
|
//#region src/rules/no-enum-number-widening.ts
|
|
793
798
|
const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
|
|
@@ -910,7 +915,7 @@ const noIndexFiles = {
|
|
|
910
915
|
};
|
|
911
916
|
//#endregion
|
|
912
917
|
//#region src/rules/no-map-instanceof-mutation.ts
|
|
913
|
-
const createRule$
|
|
918
|
+
const createRule$7 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
914
919
|
const MUTATING_MAP_METHODS = /* @__PURE__ */ new Set([
|
|
915
920
|
"set",
|
|
916
921
|
"delete",
|
|
@@ -927,7 +932,7 @@ function definitelyExits$1(statement) {
|
|
|
927
932
|
}
|
|
928
933
|
return false;
|
|
929
934
|
}
|
|
930
|
-
const noMapInstanceofMutation = createRule$
|
|
935
|
+
const noMapInstanceofMutation = createRule$7({
|
|
931
936
|
name: "no-map-instanceof-mutation",
|
|
932
937
|
meta: {
|
|
933
938
|
type: "problem",
|
|
@@ -1008,7 +1013,7 @@ const MUTATING_INSERT_METHODS = /* @__PURE__ */ new Set([
|
|
|
1008
1013
|
"fill",
|
|
1009
1014
|
"copyWithin"
|
|
1010
1015
|
]);
|
|
1011
|
-
const createRule$
|
|
1016
|
+
const createRule$6 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1012
1017
|
function buildReadonlyArrayFix(annotated, fixer) {
|
|
1013
1018
|
if (annotated.type === AST_NODE_TYPES.TSArrayType) return fixer.insertTextBefore(annotated, "readonly ");
|
|
1014
1019
|
if (annotated.type !== AST_NODE_TYPES.TSTypeReference) throw new Error(`Unreachable: expected a TSArrayType or TSTypeReference, got ${annotated.type} instead.`);
|
|
@@ -1021,7 +1026,7 @@ function isUnionArrayType(typeAnnotation) {
|
|
|
1021
1026
|
if (firstParam?.type === AST_NODE_TYPES.TSUnionType) return firstParam;
|
|
1022
1027
|
}
|
|
1023
1028
|
}
|
|
1024
|
-
const noMutableUnionArrayParam = createRule$
|
|
1029
|
+
const noMutableUnionArrayParam = createRule$6({
|
|
1025
1030
|
name: "no-mutable-union-array-param",
|
|
1026
1031
|
meta: {
|
|
1027
1032
|
type: "problem",
|
|
@@ -1074,7 +1079,7 @@ const noNonBarrelIndex = {
|
|
|
1074
1079
|
};
|
|
1075
1080
|
//#endregion
|
|
1076
1081
|
//#region src/rules/no-non-barrel-reexport.ts
|
|
1077
|
-
function removeListMember(fixer, sourceCode, declaration, members, target) {
|
|
1082
|
+
function removeListMember({ fixer, sourceCode }, declaration, members, target) {
|
|
1078
1083
|
if (members.length === 1) return fixer.remove(declaration);
|
|
1079
1084
|
const targetIndex = members.indexOf(target);
|
|
1080
1085
|
const isLast = targetIndex === members.length - 1;
|
|
@@ -1122,8 +1127,14 @@ const noNonBarrelReexport = {
|
|
|
1122
1127
|
messageId: "splitStatementReexport",
|
|
1123
1128
|
data: { name },
|
|
1124
1129
|
fix(fixer) {
|
|
1125
|
-
const fixes = [removeListMember(
|
|
1126
|
-
|
|
1130
|
+
const fixes = [removeListMember({
|
|
1131
|
+
fixer,
|
|
1132
|
+
sourceCode
|
|
1133
|
+
}, declaration, declaration.specifiers, specifier)];
|
|
1134
|
+
if (specifier.local.type === "Identifier" && importIsOnlyUsedByThisExport(sourceCode, trackedImport, specifier.local)) fixes.push(removeListMember({
|
|
1135
|
+
fixer,
|
|
1136
|
+
sourceCode
|
|
1137
|
+
}, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
1127
1138
|
return fixes;
|
|
1128
1139
|
}
|
|
1129
1140
|
});
|
|
@@ -1135,7 +1146,10 @@ const noNonBarrelReexport = {
|
|
|
1135
1146
|
data: { name },
|
|
1136
1147
|
fix(fixer) {
|
|
1137
1148
|
const fixes = [fixer.remove(declaration)];
|
|
1138
|
-
if (importIsOnlyUsedByThisExport(sourceCode, trackedImport, identifierNode)) fixes.push(removeListMember(
|
|
1149
|
+
if (importIsOnlyUsedByThisExport(sourceCode, trackedImport, identifierNode)) fixes.push(removeListMember({
|
|
1150
|
+
fixer,
|
|
1151
|
+
sourceCode
|
|
1152
|
+
}, trackedImport.declaration, trackedImport.declaration.specifiers, trackedImport.specifier));
|
|
1139
1153
|
return fixes;
|
|
1140
1154
|
}
|
|
1141
1155
|
});
|
|
@@ -1146,14 +1160,14 @@ const noNonBarrelReexport = {
|
|
|
1146
1160
|
};
|
|
1147
1161
|
//#endregion
|
|
1148
1162
|
//#region src/rules/no-object-assign.ts
|
|
1149
|
-
const createRule$
|
|
1163
|
+
const createRule$5 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1150
1164
|
function resolveFrom$1(scope, name) {
|
|
1151
1165
|
for (let current = scope; current; current = current.upper) {
|
|
1152
1166
|
const found = current.set.get(name);
|
|
1153
1167
|
if (found) return found;
|
|
1154
1168
|
}
|
|
1155
1169
|
}
|
|
1156
|
-
const noObjectAssign = createRule$
|
|
1170
|
+
const noObjectAssign = createRule$5({
|
|
1157
1171
|
name: "no-object-assign",
|
|
1158
1172
|
meta: {
|
|
1159
1173
|
type: "problem",
|
|
@@ -1299,7 +1313,7 @@ const MUTATING_SET_METHODS = /* @__PURE__ */ new Set([
|
|
|
1299
1313
|
"delete",
|
|
1300
1314
|
"clear"
|
|
1301
1315
|
]);
|
|
1302
|
-
const createRule$
|
|
1316
|
+
const createRule$4 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1303
1317
|
function isSetInstanceofExpression(node) {
|
|
1304
1318
|
return node.type === AST_NODE_TYPES.BinaryExpression && node.operator === "instanceof" && node.right.type === AST_NODE_TYPES.Identifier && node.right.name === "Set";
|
|
1305
1319
|
}
|
|
@@ -1311,7 +1325,7 @@ function definitelyExits(statement) {
|
|
|
1311
1325
|
}
|
|
1312
1326
|
return false;
|
|
1313
1327
|
}
|
|
1314
|
-
const noSetInstanceofMutation = createRule$
|
|
1328
|
+
const noSetInstanceofMutation = createRule$4({
|
|
1315
1329
|
name: "no-set-instanceof-mutation",
|
|
1316
1330
|
meta: {
|
|
1317
1331
|
type: "problem",
|
|
@@ -1605,13 +1619,13 @@ const packageJsonKeyOrder = {
|
|
|
1605
1619
|
};
|
|
1606
1620
|
//#endregion
|
|
1607
1621
|
//#region src/rules/prefer-numeric-sort-compare.ts
|
|
1608
|
-
const createRule$
|
|
1622
|
+
const createRule$3 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1609
1623
|
const SORT_METHOD_NAMES = /* @__PURE__ */ new Set(["sort", "toSorted"]);
|
|
1610
1624
|
function isDefinitelyNumberType(type) {
|
|
1611
1625
|
if (type.isUnion()) return type.types.every((constituent) => isDefinitelyNumberType(constituent));
|
|
1612
1626
|
return (type.flags & ts.TypeFlags.NumberLike) !== 0;
|
|
1613
1627
|
}
|
|
1614
|
-
const preferNumericSortCompare = createRule$
|
|
1628
|
+
const preferNumericSortCompare = createRule$3({
|
|
1615
1629
|
name: "prefer-numeric-sort-compare",
|
|
1616
1630
|
meta: {
|
|
1617
1631
|
type: "suggestion",
|
|
@@ -1653,6 +1667,170 @@ const preferNumericSortCompare = createRule$2({
|
|
|
1653
1667
|
}
|
|
1654
1668
|
});
|
|
1655
1669
|
//#endregion
|
|
1670
|
+
//#region src/rules/prefer-options-object-param.ts
|
|
1671
|
+
const createRule$2 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1672
|
+
const FUNCTION_LIKE_SELECTOR$2 = [
|
|
1673
|
+
"ArrowFunctionExpression",
|
|
1674
|
+
"FunctionDeclaration",
|
|
1675
|
+
"FunctionExpression",
|
|
1676
|
+
"TSCallSignatureDeclaration",
|
|
1677
|
+
"TSConstructSignatureDeclaration",
|
|
1678
|
+
"TSDeclareFunction",
|
|
1679
|
+
"TSEmptyBodyFunctionExpression",
|
|
1680
|
+
"TSFunctionType",
|
|
1681
|
+
"TSMethodSignature"
|
|
1682
|
+
].join(", ");
|
|
1683
|
+
function isOptionalParam(param) {
|
|
1684
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) return isOptionalParam(param.parameter);
|
|
1685
|
+
if (param.type === AST_NODE_TYPES.AssignmentPattern) return true;
|
|
1686
|
+
if (param.type === AST_NODE_TYPES.Identifier) return param.optional;
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
function getTrailingOptionalRun(params) {
|
|
1690
|
+
const hasTrailingRest = params.length > 0 && params[params.length - 1]?.type === AST_NODE_TYPES.RestElement;
|
|
1691
|
+
const run = [];
|
|
1692
|
+
for (let i = params.length - (hasTrailingRest ? 2 : 1); i >= 0; i--) {
|
|
1693
|
+
const param = params[i];
|
|
1694
|
+
if (param === void 0 || !isOptionalParam(param)) break;
|
|
1695
|
+
run.unshift(param);
|
|
1696
|
+
}
|
|
1697
|
+
return run;
|
|
1698
|
+
}
|
|
1699
|
+
function resolveFixableParam(param) {
|
|
1700
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) return void 0;
|
|
1701
|
+
const identifierNode = param.type === AST_NODE_TYPES.AssignmentPattern ? param.left.type === AST_NODE_TYPES.Identifier ? param.left : void 0 : param.type === AST_NODE_TYPES.Identifier ? param : void 0;
|
|
1702
|
+
if (identifierNode === void 0) return void 0;
|
|
1703
|
+
const typeNode = identifierNode.typeAnnotation?.typeAnnotation;
|
|
1704
|
+
if (typeNode === void 0) return void 0;
|
|
1705
|
+
if (identifierNode.decorators.length > 0) return void 0;
|
|
1706
|
+
return {
|
|
1707
|
+
identifierNode,
|
|
1708
|
+
typeNode,
|
|
1709
|
+
defaultExpression: param.type === AST_NODE_TYPES.AssignmentPattern ? param.right : void 0
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function isDefined(value) {
|
|
1713
|
+
return value !== void 0;
|
|
1714
|
+
}
|
|
1715
|
+
function firstAndLastOrThrow(items) {
|
|
1716
|
+
const first = items[0];
|
|
1717
|
+
const last = items.at(-1);
|
|
1718
|
+
if (first === void 0 || last === void 0) throw new Error("Unreachable: expected at least one element (the caller already confirmed a minimum run length).");
|
|
1719
|
+
return [first, last];
|
|
1720
|
+
}
|
|
1721
|
+
function describeFunctionKind(node) {
|
|
1722
|
+
if (node.type === AST_NODE_TYPES.TSConstructSignatureDeclaration) return "constructor";
|
|
1723
|
+
if (node.type === AST_NODE_TYPES.TSMethodSignature) return "method";
|
|
1724
|
+
const { parent } = node;
|
|
1725
|
+
if (parent.type === AST_NODE_TYPES.MethodDefinition || parent.type === AST_NODE_TYPES.TSAbstractMethodDefinition) return parent.kind === "constructor" ? "constructor" : "method";
|
|
1726
|
+
if (parent.type === AST_NODE_TYPES.Property && parent.method) return "method";
|
|
1727
|
+
return "function";
|
|
1728
|
+
}
|
|
1729
|
+
function getBlockBody(node) {
|
|
1730
|
+
return "body" in node && node.body?.type === AST_NODE_TYPES.BlockStatement ? node.body : void 0;
|
|
1731
|
+
}
|
|
1732
|
+
const LIFTABLE_JSDOC_PARENTS = /* @__PURE__ */ new Set([
|
|
1733
|
+
AST_NODE_TYPES.VariableDeclarator,
|
|
1734
|
+
AST_NODE_TYPES.VariableDeclaration,
|
|
1735
|
+
AST_NODE_TYPES.MethodDefinition,
|
|
1736
|
+
AST_NODE_TYPES.TSAbstractMethodDefinition,
|
|
1737
|
+
AST_NODE_TYPES.PropertyDefinition,
|
|
1738
|
+
AST_NODE_TYPES.ExportNamedDeclaration,
|
|
1739
|
+
AST_NODE_TYPES.ExportDefaultDeclaration
|
|
1740
|
+
]);
|
|
1741
|
+
function getLeadingJSDocComment(sourceCode, node) {
|
|
1742
|
+
let current = node;
|
|
1743
|
+
for (;;) {
|
|
1744
|
+
const jsdocComment = sourceCode.getCommentsBefore(current).findLast((comment) => comment.type === AST_TOKEN_TYPES.Block && comment.value.startsWith("*"));
|
|
1745
|
+
if (jsdocComment) return jsdocComment;
|
|
1746
|
+
const { parent } = current;
|
|
1747
|
+
if (parent === void 0 || !LIFTABLE_JSDOC_PARENTS.has(parent.type)) return void 0;
|
|
1748
|
+
current = parent;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function jsDocMentionsParam(commentValue, name) {
|
|
1752
|
+
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1753
|
+
return new RegExp(`@param\\s+(?:\\{[^}]*\\}\\s+)?\\[?${escapedName}\\b`).test(commentValue);
|
|
1754
|
+
}
|
|
1755
|
+
function hasOptionsNameCollision(scope, exemptIdentifiers) {
|
|
1756
|
+
return scope.variables.some((variable) => variable.name === "options" && !variable.identifiers.every((identifier) => exemptIdentifiers.has(identifier)));
|
|
1757
|
+
}
|
|
1758
|
+
const preferOptionsObjectParam = createRule$2({
|
|
1759
|
+
name: "prefer-options-object-param",
|
|
1760
|
+
meta: {
|
|
1761
|
+
type: "suggestion",
|
|
1762
|
+
hasSuggestions: true,
|
|
1763
|
+
docs: { description: "Suggest bundling a run of 2+ trailing optional parameters into a single destructured 'options' parameter — without this, a caller needing only the last optional parameter must still pass 'undefined' for every optional parameter before it." },
|
|
1764
|
+
schema: [{
|
|
1765
|
+
type: "object",
|
|
1766
|
+
properties: { minTrailingOptional: {
|
|
1767
|
+
type: "integer",
|
|
1768
|
+
minimum: 2
|
|
1769
|
+
} },
|
|
1770
|
+
additionalProperties: false
|
|
1771
|
+
}],
|
|
1772
|
+
messages: {
|
|
1773
|
+
tooManyTrailingOptional: "This {{ kind }} has {{ count }} trailing optional parameters ({{ names }}) — a caller needing only the last one must still pass 'undefined' for every parameter before it. Bundle the trailing optional run into a single destructured 'options' parameter instead.",
|
|
1774
|
+
wrapInOptionsObject: "Bundle the trailing optional parameters into a single 'options' parameter."
|
|
1775
|
+
},
|
|
1776
|
+
defaultOptions: [{ minTrailingOptional: 2 }]
|
|
1777
|
+
},
|
|
1778
|
+
create(context, [{ minTrailingOptional }]) {
|
|
1779
|
+
const { sourceCode } = context;
|
|
1780
|
+
function checkParams(node) {
|
|
1781
|
+
const run = getTrailingOptionalRun(node.params);
|
|
1782
|
+
if (run.length < minTrailingOptional) return;
|
|
1783
|
+
const resolved = run.map(resolveFixableParam);
|
|
1784
|
+
const names = run.map((param, index) => resolved[index]?.identifierNode.name ?? sourceCode.getText(param)).join(", ");
|
|
1785
|
+
const data = {
|
|
1786
|
+
kind: describeFunctionKind(node),
|
|
1787
|
+
count: run.length,
|
|
1788
|
+
names
|
|
1789
|
+
};
|
|
1790
|
+
const resolvedParams = resolved.filter(isDefined);
|
|
1791
|
+
const allResolvable = resolvedParams.length === run.length;
|
|
1792
|
+
const hasRestParam = node.params.some((param) => param.type === AST_NODE_TYPES.RestElement);
|
|
1793
|
+
const body = getBlockBody(node);
|
|
1794
|
+
let isFixable = allResolvable && !hasRestParam && body !== void 0;
|
|
1795
|
+
if (isFixable) {
|
|
1796
|
+
const jsdocComment = getLeadingJSDocComment(sourceCode, node);
|
|
1797
|
+
const jsDocBail = jsdocComment !== void 0 && resolvedParams.some((info) => jsDocMentionsParam(jsdocComment.value, info.identifierNode.name));
|
|
1798
|
+
const exemptIdentifiers = new Set(resolvedParams.map((info) => info.identifierNode));
|
|
1799
|
+
const optionsCollision = hasOptionsNameCollision(sourceCode.getScope(node), exemptIdentifiers);
|
|
1800
|
+
isFixable = !jsDocBail && !optionsCollision;
|
|
1801
|
+
}
|
|
1802
|
+
if (!isFixable || body === void 0) {
|
|
1803
|
+
context.report({
|
|
1804
|
+
node,
|
|
1805
|
+
messageId: "tooManyTrailingOptional",
|
|
1806
|
+
data
|
|
1807
|
+
});
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
context.report({
|
|
1811
|
+
node,
|
|
1812
|
+
messageId: "tooManyTrailingOptional",
|
|
1813
|
+
data,
|
|
1814
|
+
suggest: [{
|
|
1815
|
+
messageId: "wrapInOptionsObject",
|
|
1816
|
+
fix(fixer) {
|
|
1817
|
+
const properties = resolvedParams.map((info) => `${info.identifierNode.name}?: ${sourceCode.getText(info.typeNode)}`);
|
|
1818
|
+
const destructureEntries = resolvedParams.map((info) => info.defaultExpression === void 0 ? info.identifierNode.name : `${info.identifierNode.name} = ${sourceCode.getText(info.defaultExpression)}`);
|
|
1819
|
+
const optionsParamText = `options?: { ${properties.join("; ")} }`;
|
|
1820
|
+
const destructureText = `const { ${destructureEntries.join(", ")} } = options ?? {};`;
|
|
1821
|
+
const [firstParam, lastParam] = firstAndLastOrThrow(run);
|
|
1822
|
+
const openBrace = firstTokenOrThrow(sourceCode, body);
|
|
1823
|
+
return [fixer.replaceTextRange([firstParam.range[0], lastParam.range[1]], optionsParamText), fixer.insertTextAfter(openBrace, `\n ${destructureText}`)];
|
|
1824
|
+
}
|
|
1825
|
+
}]
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
return { [FUNCTION_LIKE_SELECTOR$2](node) {
|
|
1829
|
+
checkParams(node);
|
|
1830
|
+
} };
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
//#endregion
|
|
1656
1834
|
//#region src/rules/prefer-readonly-array-param.ts
|
|
1657
1835
|
const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`);
|
|
1658
1836
|
function getFixableArrayOrTupleType(typeNode) {
|
|
@@ -1870,6 +2048,7 @@ const plugin = {
|
|
|
1870
2048
|
"no-side-effects-in-index": noSideEffectsInIndex,
|
|
1871
2049
|
"package-json-key-order": packageJsonKeyOrder,
|
|
1872
2050
|
"prefer-numeric-sort-compare": preferNumericSortCompare,
|
|
2051
|
+
"prefer-options-object-param": preferOptionsObjectParam,
|
|
1873
2052
|
"prefer-readonly-array-param": preferReadonlyArrayParam,
|
|
1874
2053
|
"prefer-readonly-object-param": preferReadonlyObjectParam,
|
|
1875
2054
|
"test-file-kind": {
|
|
@@ -1932,7 +2111,8 @@ const plugin = {
|
|
|
1932
2111
|
"exadev/no-object-assign": "error",
|
|
1933
2112
|
"exadev/no-pointless-reassignment": "error",
|
|
1934
2113
|
"exadev/prefer-readonly-array-param": "error",
|
|
1935
|
-
"exadev/test-file-kind": "error"
|
|
2114
|
+
"exadev/test-file-kind": "error",
|
|
2115
|
+
"max-params": ["error", { max: 4 }]
|
|
1936
2116
|
}
|
|
1937
2117
|
};
|
|
1938
2118
|
},
|
|
@@ -2044,6 +2224,7 @@ const recommendedTypeChecked = [
|
|
|
2044
2224
|
"exadev/no-pointless-reassignment": "error",
|
|
2045
2225
|
"exadev/no-set-instanceof-mutation": "error",
|
|
2046
2226
|
"exadev/prefer-numeric-sort-compare": "error",
|
|
2227
|
+
"exadev/prefer-options-object-param": "error",
|
|
2047
2228
|
"exadev/prefer-readonly-array-param": "error",
|
|
2048
2229
|
"exadev/prefer-readonly-object-param": "error",
|
|
2049
2230
|
"exadev/test-file-kind": "error",
|
|
@@ -2081,6 +2262,7 @@ const recommendedTypeChecked = [
|
|
|
2081
2262
|
skipBlankLines: true,
|
|
2082
2263
|
skipComments: true
|
|
2083
2264
|
}],
|
|
2265
|
+
"max-params": ["error", { max: 4 }],
|
|
2084
2266
|
"no-warning-comments": ["error", {
|
|
2085
2267
|
terms: ["stryker disable"],
|
|
2086
2268
|
location: "anywhere"
|
package/package.json
CHANGED