exadev-eslint-config 2.20.1 → 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 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 (including its `export` keyword, when exported). 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. |
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. |
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.1";
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
- const declaration = node.parent;
1335
- if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
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
  });
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.1";
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
- const declaration = node.parent;
1301
- if (declaration.type !== "VariableDeclaration" || declaration.declarations.length !== 1) return null;
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
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exadev-eslint-config",
3
3
  "description": "Shared custom ESLint rules and plugin for ExaDev projects",
4
- "version": "2.20.1",
4
+ "version": "2.20.2",
5
5
  "dependencies": {
6
6
  "@eslint/js": "^10.0.1",
7
7
  "@typescript-eslint/utils": "8.67.0",