exadev-eslint-config 2.20.0 → 2.20.2
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 +2 -2
- package/dist/index.cjs +23 -5
- package/dist/index.js +23 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -273,7 +273,7 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
273
273
|
| `no-side-effects-in-index` | | **A barrel may contain only re-export statements** — nothing that could execute at import time. Self-scopes to any index file. |
|
|
274
274
|
| `barrel-direct-siblings-only` | | **A barrel may re-export only from a direct sibling** (`./module`), never a nested path, parent, or bare package specifier (mode 3). |
|
|
275
275
|
| `no-control-flow` | | **Bans `if`/`switch`/loops/the ternary operator outright.** Not part of `recommended` or `barrel` — ordinary code legitimately needs control flow, so this is opt-in, wired via a consumer's own `files` glob for the specific packages that want it (a composition-root package selecting an adapter/strategy by a validated key, say): a lookup table replaces a branch, a declarative array method (`map`/`filter`/`some`/`every`/...) replaces a loop. Requires no type information. |
|
|
276
|
-
| `no-pointless-reassignment` | ✓ | **Flags a `const` alias that adds no transformation** (`const foo = bar` where both sides are plain identifiers). Autofix rewrites every read to the original name and deletes the declaration
|
|
276
|
+
| `no-pointless-reassignment` | ✓ | **Flags a `const` alias that adds no transformation** (`const foo = bar` where both sides are plain identifiers). Autofix rewrites every read to the original name and deletes the declaration. Still reported but deliberately not auto-fixable where collapsing the alias would change meaning: an explicit type annotation (`const exhaustive: never = item` — the annotation is the point), a read where the original name is shadowed, a read as a shorthand object property, more than one declarator in the statement, or a source that is written to anywhere. An alias that is itself part of the module's exported surface (`export const alias = original;`, a later `export { alias }`/`export { alias as other }`, or `export default alias;`) is neither reported nor fixed at all, since collapsing it would rename or delete a binding every importer of this module depends on. |
|
|
277
277
|
| `no-object-assign` | ✓/suggestion | **`Object.assign` skips the type-checking object spread gets** — it doesn't check a source object's properties against the target's declared types. A fresh object-literal target autofixes to `{ ...target, ...source }`; mutating an existing reassignable binding offers a suggestion only (changes the object's identity); a `const` binding or a non-statement call site gets a plain report with no fix. |
|
|
278
278
|
| `no-mutable-union-array-param` | ✓ | **A union-typed array parameter can be mutated with a value the caller's narrower array never declared.** A function parameter typed as an array of a union (`(string \| number)[]`) accepts a narrower caller array (`number[]`) by covariance; calling `push`/`unshift`/`splice`/`fill`/`copyWithin` on it can then insert a value the caller's own array was never declared to hold. Autofix marks the parameter `readonly`, turning the mutating call into a real compile error to resolve deliberately. Requires no type information. |
|
|
279
279
|
| `prefer-readonly-array-param` | ✓ | **Every non-readonly array/tuple parameter should be `readonly`.** A narrower, safely-autofixable sibling of [`@typescript-eslint/prefer-readonly-parameter-types`](https://typescript-eslint.io/rules/prefer-readonly-parameter-types/) scoped to array/tuple parameter shapes only: fires unconditionally on every non-readonly array or tuple parameter, regardless of whether the function body mutates it, in any parameter position (a plain identifier, a rest parameter, a default-valued parameter, or a constructor parameter property) and any function-like shape (a concrete function/arrow/method, or a declaration-only ambient function, interface method, function type alias, call/construct signature, or abstract/ambient class method). A union containing an array/tuple member is fixed on that member alone. Autofix prepends `readonly ` (or renames `Array<T>` to `ReadonlyArray<T>`), turning any resulting mutation into a real compile error to resolve deliberately. Requires no type information — registered in both `plugin.configs.recommended` and the default (type-checked) export. |
|
|
@@ -284,7 +284,7 @@ Bundled into `exadevConfig()`'s default output the same way React/Next.js auto-d
|
|
|
284
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`. |
|
|
285
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`. |
|
|
286
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
|
|
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 — registered in both `plugin.configs.recommended` and the default (type-checked) export, like `prefer-readonly-array-param` above; unlike its own `prefer-readonly-object-param`/`prefer-numeric-sort-compare` siblings, which genuinely need the checker and so cannot be offered outside the default export at all. |
|
|
288
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. |
|
|
289
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. |
|
|
290
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.20.
|
|
298
|
+
var version = "2.20.2";
|
|
299
299
|
//#endregion
|
|
300
300
|
//#region src/react.ts
|
|
301
301
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -1274,6 +1274,13 @@ function isConstDeclarator(declarator) {
|
|
|
1274
1274
|
if (declarator.parent.type !== "VariableDeclaration") throw new Error(`Unreachable: expected a VariableDeclarator's own parent to be a VariableDeclaration, got "${declarator.parent.type}" instead.`);
|
|
1275
1275
|
return declarator.parent.kind === "const";
|
|
1276
1276
|
}
|
|
1277
|
+
function asVariableDeclaration(node) {
|
|
1278
|
+
if (node.type !== "VariableDeclaration") throw new Error(`Unreachable: expected a VariableDeclarator's own parent to be a VariableDeclaration, got "${node.type}" instead.`);
|
|
1279
|
+
return node;
|
|
1280
|
+
}
|
|
1281
|
+
function findTopLevelScope(globalScope) {
|
|
1282
|
+
return globalScope.childScopes.find((child) => child.type === "module") ?? globalScope;
|
|
1283
|
+
}
|
|
1277
1284
|
function variableInScope(scope, name) {
|
|
1278
1285
|
const variable = scope.set.get(name);
|
|
1279
1286
|
if (variable === void 0) throw new Error(`Unreachable: expected scope.set to hold a variable named "${name}".`);
|
|
@@ -1297,6 +1304,14 @@ function isShorthandPropertyRead(sourceCode, identifier) {
|
|
|
1297
1304
|
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
1298
1305
|
return hasEnclosingShorthandBoundary(sourceCode.getTokensBefore(identifier));
|
|
1299
1306
|
}
|
|
1307
|
+
function isExportedAlias(declarationParentType, programBody, resolve, aliasVariable) {
|
|
1308
|
+
if (declarationParentType === "ExportNamedDeclaration") return true;
|
|
1309
|
+
return programBody.some((statement) => {
|
|
1310
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source == null) return (statement.specifiers ?? []).some((specifier) => specifier.local.type === "Identifier" && specifier.local.name !== void 0 && resolve(specifier.local.name) === aliasVariable);
|
|
1311
|
+
const target = statement.type === "ExportDefaultDeclaration" ? statement.declaration : void 0;
|
|
1312
|
+
return target?.type === "Identifier" && target.name !== void 0 && resolve(target.name) === aliasVariable;
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1300
1315
|
const noPointlessReassignment = {
|
|
1301
1316
|
meta: {
|
|
1302
1317
|
type: "problem",
|
|
@@ -1305,6 +1320,7 @@ const noPointlessReassignment = {
|
|
|
1305
1320
|
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
1306
1321
|
},
|
|
1307
1322
|
create(context) {
|
|
1323
|
+
const moduleScope = findTopLevelScope(context.sourceCode.getScope(context.sourceCode.ast));
|
|
1308
1324
|
return { VariableDeclarator(node) {
|
|
1309
1325
|
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
1310
1326
|
if (!isConstDeclarator(node)) return;
|
|
@@ -1314,6 +1330,9 @@ const noPointlessReassignment = {
|
|
|
1314
1330
|
const aliasName = node.id.name;
|
|
1315
1331
|
const originalName = node.init.name;
|
|
1316
1332
|
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
1333
|
+
const variable = variableInScope(scope, aliasName);
|
|
1334
|
+
const declaration = asVariableDeclaration(node.parent);
|
|
1335
|
+
if (isExportedAlias(declaration.parent.type, context.sourceCode.ast.body, (name) => resolveFrom(moduleScope, name), variable)) return;
|
|
1317
1336
|
context.report({
|
|
1318
1337
|
node,
|
|
1319
1338
|
messageId: "pointlessReassignment",
|
|
@@ -1322,7 +1341,6 @@ const noPointlessReassignment = {
|
|
|
1322
1341
|
value: originalName
|
|
1323
1342
|
},
|
|
1324
1343
|
fix(fixer) {
|
|
1325
|
-
const variable = variableInScope(scope, aliasName);
|
|
1326
1344
|
if (aliasIsAnnotated) return null;
|
|
1327
1345
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
1328
1346
|
const readReferences = variable.references.filter((reference) => reference.isRead());
|
|
@@ -1331,9 +1349,8 @@ const noPointlessReassignment = {
|
|
|
1331
1349
|
if (readRefs.some((reference) => isShorthandPropertyRead(context.sourceCode, reference.identifier))) return null;
|
|
1332
1350
|
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
1333
1351
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
1352
|
+
if (declaration.declarations.length !== 1) return null;
|
|
1353
|
+
fixes.push(fixer.remove(declaration));
|
|
1337
1354
|
return fixes;
|
|
1338
1355
|
}
|
|
1339
1356
|
});
|
|
@@ -2144,6 +2161,7 @@ const plugin = {
|
|
|
2144
2161
|
"exadev/no-mutable-union-array-param": "error",
|
|
2145
2162
|
"exadev/no-object-assign": "error",
|
|
2146
2163
|
"exadev/no-pointless-reassignment": "error",
|
|
2164
|
+
"exadev/prefer-options-object-param": "error",
|
|
2147
2165
|
"exadev/prefer-readonly-array-param": "error",
|
|
2148
2166
|
"exadev/test-file-kind": "error",
|
|
2149
2167
|
"max-params": ["error", { max: 4 }]
|
package/dist/index.js
CHANGED
|
@@ -261,7 +261,7 @@ function buildNextjsConfig(options = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
263
|
//#region package.json
|
|
264
|
-
var version = "2.20.
|
|
264
|
+
var version = "2.20.2";
|
|
265
265
|
//#endregion
|
|
266
266
|
//#region src/react.ts
|
|
267
267
|
const JSX_FILE_PATTERNS = ["**/*.jsx", "**/*.tsx"];
|
|
@@ -1240,6 +1240,13 @@ function isConstDeclarator(declarator) {
|
|
|
1240
1240
|
if (declarator.parent.type !== "VariableDeclaration") throw new Error(`Unreachable: expected a VariableDeclarator's own parent to be a VariableDeclaration, got "${declarator.parent.type}" instead.`);
|
|
1241
1241
|
return declarator.parent.kind === "const";
|
|
1242
1242
|
}
|
|
1243
|
+
function asVariableDeclaration(node) {
|
|
1244
|
+
if (node.type !== "VariableDeclaration") throw new Error(`Unreachable: expected a VariableDeclarator's own parent to be a VariableDeclaration, got "${node.type}" instead.`);
|
|
1245
|
+
return node;
|
|
1246
|
+
}
|
|
1247
|
+
function findTopLevelScope(globalScope) {
|
|
1248
|
+
return globalScope.childScopes.find((child) => child.type === "module") ?? globalScope;
|
|
1249
|
+
}
|
|
1243
1250
|
function variableInScope(scope, name) {
|
|
1244
1251
|
const variable = scope.set.get(name);
|
|
1245
1252
|
if (variable === void 0) throw new Error(`Unreachable: expected scope.set to hold a variable named "${name}".`);
|
|
@@ -1263,6 +1270,14 @@ function isShorthandPropertyRead(sourceCode, identifier) {
|
|
|
1263
1270
|
if (afterToken?.value !== "}" && afterToken?.value !== ",") return false;
|
|
1264
1271
|
return hasEnclosingShorthandBoundary(sourceCode.getTokensBefore(identifier));
|
|
1265
1272
|
}
|
|
1273
|
+
function isExportedAlias(declarationParentType, programBody, resolve, aliasVariable) {
|
|
1274
|
+
if (declarationParentType === "ExportNamedDeclaration") return true;
|
|
1275
|
+
return programBody.some((statement) => {
|
|
1276
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source == null) return (statement.specifiers ?? []).some((specifier) => specifier.local.type === "Identifier" && specifier.local.name !== void 0 && resolve(specifier.local.name) === aliasVariable);
|
|
1277
|
+
const target = statement.type === "ExportDefaultDeclaration" ? statement.declaration : void 0;
|
|
1278
|
+
return target?.type === "Identifier" && target.name !== void 0 && resolve(target.name) === aliasVariable;
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1266
1281
|
const noPointlessReassignment = {
|
|
1267
1282
|
meta: {
|
|
1268
1283
|
type: "problem",
|
|
@@ -1271,6 +1286,7 @@ const noPointlessReassignment = {
|
|
|
1271
1286
|
messages: { pointlessReassignment: "Pointless reassignment: '{{ name }}' is just an alias for '{{ value }}'. Use the original directly." }
|
|
1272
1287
|
},
|
|
1273
1288
|
create(context) {
|
|
1289
|
+
const moduleScope = findTopLevelScope(context.sourceCode.getScope(context.sourceCode.ast));
|
|
1274
1290
|
return { VariableDeclarator(node) {
|
|
1275
1291
|
if (node.id.type !== "Identifier" || node.init?.type !== "Identifier" || node.id.name.startsWith("_")) return;
|
|
1276
1292
|
if (!isConstDeclarator(node)) return;
|
|
@@ -1280,6 +1296,9 @@ const noPointlessReassignment = {
|
|
|
1280
1296
|
const aliasName = node.id.name;
|
|
1281
1297
|
const originalName = node.init.name;
|
|
1282
1298
|
const aliasIsAnnotated = hasTypeAnnotation(node.id);
|
|
1299
|
+
const variable = variableInScope(scope, aliasName);
|
|
1300
|
+
const declaration = asVariableDeclaration(node.parent);
|
|
1301
|
+
if (isExportedAlias(declaration.parent.type, context.sourceCode.ast.body, (name) => resolveFrom(moduleScope, name), variable)) return;
|
|
1283
1302
|
context.report({
|
|
1284
1303
|
node,
|
|
1285
1304
|
messageId: "pointlessReassignment",
|
|
@@ -1288,7 +1307,6 @@ const noPointlessReassignment = {
|
|
|
1288
1307
|
value: originalName
|
|
1289
1308
|
},
|
|
1290
1309
|
fix(fixer) {
|
|
1291
|
-
const variable = variableInScope(scope, aliasName);
|
|
1292
1310
|
if (aliasIsAnnotated) return null;
|
|
1293
1311
|
if (variable.references.filter((reference) => reference.isWrite() && reference.identifier !== node.id).length > 0) return null;
|
|
1294
1312
|
const readReferences = variable.references.filter((reference) => reference.isRead());
|
|
@@ -1297,9 +1315,8 @@ const noPointlessReassignment = {
|
|
|
1297
1315
|
if (readRefs.some((reference) => isShorthandPropertyRead(context.sourceCode, reference.identifier))) return null;
|
|
1298
1316
|
if (readRefs.some((reference) => resolveFrom(reference.from, originalName) !== sourceVariable)) return null;
|
|
1299
1317
|
const fixes = readRefs.map((reference) => fixer.replaceText(reference.identifier, originalName));
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
fixes.push(fixer.remove(declaration.parent.type === "ExportNamedDeclaration" ? declaration.parent : declaration));
|
|
1318
|
+
if (declaration.declarations.length !== 1) return null;
|
|
1319
|
+
fixes.push(fixer.remove(declaration));
|
|
1303
1320
|
return fixes;
|
|
1304
1321
|
}
|
|
1305
1322
|
});
|
|
@@ -2110,6 +2127,7 @@ const plugin = {
|
|
|
2110
2127
|
"exadev/no-mutable-union-array-param": "error",
|
|
2111
2128
|
"exadev/no-object-assign": "error",
|
|
2112
2129
|
"exadev/no-pointless-reassignment": "error",
|
|
2130
|
+
"exadev/prefer-options-object-param": "error",
|
|
2113
2131
|
"exadev/prefer-readonly-array-param": "error",
|
|
2114
2132
|
"exadev/test-file-kind": "error",
|
|
2115
2133
|
"max-params": ["error", { max: 4 }]
|
package/package.json
CHANGED