exadev-eslint-config 2.12.3 → 2.14.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 CHANGED
@@ -160,6 +160,14 @@ export default tseslint.config(
160
160
 
161
161
  Trailing arguments are arbitrary flat-config objects, appended in order after everything else -- `exadevConfig({}, { rules: { 'no-console': 'warn' } })` is equivalent to spreading the default export plus one more config object.
162
162
 
163
+ ## RFC 8785 canonical JSON formatting
164
+
165
+ Every JSON file (`**/*.json`, excluding `**/tsconfig*.json`, `**/turbo.json`, and `**/package.json`) is linted against [`eslint-plugin-json-canonical`](https://github.com/ExaDev/eslint-plugin-json-canonical)'s `configs.recommended` -- plain UTF-16 code-unit key ordering, canonical number formatting, and canonical string escaping, per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785). This is bundled unconditionally, the same way jsdoc/tsdoc support is: `eslint-plugin-json-canonical` is a plain dependency of this package, so every consumer already has it. There is no option to turn it off.
166
+
167
+ The plugin's own whitespace-collapsing rule (`no-insignificant-whitespace`, which reformats a document to a single compacted line) is deliberately not part of `configs.recommended` and is not enabled here either -- it stays available for a consumer to opt into directly for their own genuine canonicalization pass.
168
+
169
+ `**/tsconfig*.json` and `**/turbo.json` are excluded because they genuinely carry comments (TypeScript and turbo both accept them), which `@eslint/json`'s `json/json` language -- what this plugin's recommended config hard-codes -- has no concept of and fails to parse. `**/package.json` is excluded because its key order is a distinct, separately-optional concern (syncpack-style field-priority pinning, not RFC 8785's plain alphabetical order); a project wanting that instead reaches for `eslint-plugin-json-canonical` directly on its own terms, not through this package.
170
+
163
171
  ## Rules
164
172
 
165
173
  | Rule | Fixable | Description |
@@ -170,6 +178,7 @@ Trailing arguments are arbitrary flat-config objects, appended in order after ev
170
178
  | `no-non-barrel-reexport` | ✓ | Re-exports belong only in a barrel. Catches the split form across two statements (`import { x } from './y'; export { x };` or `export default x;`) which no AST selector alone can match. Autofix deletes the export and the now-pointless import when it was the import's only use. Self-scopes away from any index file. |
171
179
  | `no-side-effects-in-index` | | A barrel file may contain only re-export statements -- nothing that could execute at import time. Self-scopes to any index file. |
172
180
  | `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). |
181
+ | `no-control-flow` | | Bans `if`/`switch`/`for`/`for-in`/`for-of`/`while`/`do-while`/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. |
173
182
  | `no-pointless-reassignment` | ✓ | `const foo = bar` where both sides are plain identifiers and the alias adds no transformation. 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. |
174
183
  | `no-object-assign` | ✓/suggestion | `Object.assign` does not check a source object's properties against the target's declared types, unlike object spread. 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. |
175
184
  | `no-mutable-union-array-param` | ✓ | 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. |
package/dist/index.cjs CHANGED
@@ -28,6 +28,8 @@ let eslint_plugin_jsdoc = require("eslint-plugin-jsdoc");
28
28
  eslint_plugin_jsdoc = __toESM(eslint_plugin_jsdoc, 1);
29
29
  let eslint_plugin_tsdoc = require("eslint-plugin-tsdoc");
30
30
  eslint_plugin_tsdoc = __toESM(eslint_plugin_tsdoc, 1);
31
+ let eslint_plugin_json_canonical = require("eslint-plugin-json-canonical");
32
+ eslint_plugin_json_canonical = __toESM(eslint_plugin_json_canonical, 1);
31
33
  let node_module = require("node:module");
32
34
  let _eslint_js = require("@eslint/js");
33
35
  _eslint_js = __toESM(_eslint_js, 1);
@@ -74,6 +76,22 @@ const jsdocAndTsdoc = [{
74
76
  rules: { "tsdoc/syntax": "error" }
75
77
  }];
76
78
  //#endregion
79
+ //#region src/json-canonical.ts
80
+ function isSingleFlatConfig(value) {
81
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "language" in value;
82
+ }
83
+ const recommended = eslint_plugin_json_canonical.default.configs?.["recommended"];
84
+ if (!isSingleFlatConfig(recommended)) throw new Error("eslint-plugin-json-canonical: expected configs.recommended to be a single flat config object, got an array or undefined");
85
+ const jsonCanonicalConfig = [{
86
+ files: ["**/*.json"],
87
+ ignores: [
88
+ "**/tsconfig*.json",
89
+ "**/turbo.json",
90
+ "**/package.json"
91
+ ],
92
+ ...recommended
93
+ }];
94
+ //#endregion
77
95
  //#region src/optional-plugin.ts
78
96
  const nodeRequire = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
79
97
  function tryRequire(specifier, requireFn = nodeRequire) {
@@ -159,7 +177,7 @@ function buildReactConfig(options = {}) {
159
177
  }
160
178
  //#endregion
161
179
  //#region package.json
162
- var version = "2.12.3";
180
+ var version = "2.14.0";
163
181
  //#endregion
164
182
  //#region src/rules/barrel-helpers.ts
165
183
  const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
@@ -516,6 +534,76 @@ const noArrayIsArrayMutation = createRule$7({
516
534
  }
517
535
  });
518
536
  //#endregion
537
+ //#region src/rules/no-control-flow.ts
538
+ const noControlFlow = {
539
+ meta: {
540
+ type: "problem",
541
+ schema: [],
542
+ messages: {
543
+ ifStatement: "Use a lookup table, not an if/else branch.",
544
+ switchStatement: "Use a lookup table, not a switch branch.",
545
+ forStatement: "Use a lookup table or a declarative array method, not a for loop.",
546
+ forInStatement: "Use a lookup table or Object.entries, not a for-in loop.",
547
+ forOfStatement: "Use a declarative array method, not a for-of loop.",
548
+ whileStatement: "Use a lookup table or a declarative array method, not a while loop.",
549
+ doWhileStatement: "Use a lookup table or a declarative array method, not a do-while loop.",
550
+ conditionalExpression: "Use a lookup table, not a ternary."
551
+ }
552
+ },
553
+ create(context) {
554
+ return {
555
+ IfStatement(node) {
556
+ context.report({
557
+ node,
558
+ messageId: "ifStatement"
559
+ });
560
+ },
561
+ SwitchStatement(node) {
562
+ context.report({
563
+ node,
564
+ messageId: "switchStatement"
565
+ });
566
+ },
567
+ ForStatement(node) {
568
+ context.report({
569
+ node,
570
+ messageId: "forStatement"
571
+ });
572
+ },
573
+ ForInStatement(node) {
574
+ context.report({
575
+ node,
576
+ messageId: "forInStatement"
577
+ });
578
+ },
579
+ ForOfStatement(node) {
580
+ context.report({
581
+ node,
582
+ messageId: "forOfStatement"
583
+ });
584
+ },
585
+ WhileStatement(node) {
586
+ context.report({
587
+ node,
588
+ messageId: "whileStatement"
589
+ });
590
+ },
591
+ DoWhileStatement(node) {
592
+ context.report({
593
+ node,
594
+ messageId: "doWhileStatement"
595
+ });
596
+ },
597
+ ConditionalExpression(node) {
598
+ context.report({
599
+ node,
600
+ messageId: "conditionalExpression"
601
+ });
602
+ }
603
+ };
604
+ }
605
+ };
606
+ //#endregion
519
607
  //#region src/rules/no-enum-number-widening.ts
520
608
  const noEnumNumberWidening = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
521
609
  name: "no-enum-number-widening",
@@ -1356,6 +1444,7 @@ const plugin = {
1356
1444
  "barrel-direct-siblings-only": barrelDirectSiblingsOnly,
1357
1445
  "barrel-policy": barrelPolicy,
1358
1446
  "no-array-isarray-mutation": noArrayIsArrayMutation,
1447
+ "no-control-flow": noControlFlow,
1359
1448
  "no-enum-number-widening": noEnumNumberWidening,
1360
1449
  "no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
1361
1450
  "no-index-files": noIndexFiles,
@@ -1491,6 +1580,7 @@ function exadevConfig(options = {}, ...userConfigs) {
1491
1580
  return toPublicConfigArray([
1492
1581
  ...recommendedTypeChecked,
1493
1582
  ...jsdocAndTsdoc,
1583
+ ...jsonCanonicalConfig,
1494
1584
  ...buildReactConfig({ enabled: options.react }),
1495
1585
  ...buildNextjsConfig({ enabled: options.nextjs }),
1496
1586
  ...userConfigs
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import jsdoc from "eslint-plugin-jsdoc";
2
2
  import tsdoc from "eslint-plugin-tsdoc";
3
+ import jsonCanonical from "eslint-plugin-json-canonical";
3
4
  import { createRequire } from "node:module";
4
5
  import js from "@eslint/js";
5
6
  import tseslint from "typescript-eslint";
@@ -43,6 +44,22 @@ const jsdocAndTsdoc = [{
43
44
  rules: { "tsdoc/syntax": "error" }
44
45
  }];
45
46
  //#endregion
47
+ //#region src/json-canonical.ts
48
+ function isSingleFlatConfig(value) {
49
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "language" in value;
50
+ }
51
+ const recommended = jsonCanonical.configs?.["recommended"];
52
+ if (!isSingleFlatConfig(recommended)) throw new Error("eslint-plugin-json-canonical: expected configs.recommended to be a single flat config object, got an array or undefined");
53
+ const jsonCanonicalConfig = [{
54
+ files: ["**/*.json"],
55
+ ignores: [
56
+ "**/tsconfig*.json",
57
+ "**/turbo.json",
58
+ "**/package.json"
59
+ ],
60
+ ...recommended
61
+ }];
62
+ //#endregion
46
63
  //#region src/optional-plugin.ts
47
64
  const nodeRequire = createRequire(import.meta.url);
48
65
  function tryRequire(specifier, requireFn = nodeRequire) {
@@ -128,7 +145,7 @@ function buildReactConfig(options = {}) {
128
145
  }
129
146
  //#endregion
130
147
  //#region package.json
131
- var version = "2.12.3";
148
+ var version = "2.14.0";
132
149
  //#endregion
133
150
  //#region src/rules/barrel-helpers.ts
134
151
  const INDEX_BASENAME$1 = /^index\.[cm]?[tj]sx?$/;
@@ -485,6 +502,76 @@ const noArrayIsArrayMutation = createRule$7({
485
502
  }
486
503
  });
487
504
  //#endregion
505
+ //#region src/rules/no-control-flow.ts
506
+ const noControlFlow = {
507
+ meta: {
508
+ type: "problem",
509
+ schema: [],
510
+ messages: {
511
+ ifStatement: "Use a lookup table, not an if/else branch.",
512
+ switchStatement: "Use a lookup table, not a switch branch.",
513
+ forStatement: "Use a lookup table or a declarative array method, not a for loop.",
514
+ forInStatement: "Use a lookup table or Object.entries, not a for-in loop.",
515
+ forOfStatement: "Use a declarative array method, not a for-of loop.",
516
+ whileStatement: "Use a lookup table or a declarative array method, not a while loop.",
517
+ doWhileStatement: "Use a lookup table or a declarative array method, not a do-while loop.",
518
+ conditionalExpression: "Use a lookup table, not a ternary."
519
+ }
520
+ },
521
+ create(context) {
522
+ return {
523
+ IfStatement(node) {
524
+ context.report({
525
+ node,
526
+ messageId: "ifStatement"
527
+ });
528
+ },
529
+ SwitchStatement(node) {
530
+ context.report({
531
+ node,
532
+ messageId: "switchStatement"
533
+ });
534
+ },
535
+ ForStatement(node) {
536
+ context.report({
537
+ node,
538
+ messageId: "forStatement"
539
+ });
540
+ },
541
+ ForInStatement(node) {
542
+ context.report({
543
+ node,
544
+ messageId: "forInStatement"
545
+ });
546
+ },
547
+ ForOfStatement(node) {
548
+ context.report({
549
+ node,
550
+ messageId: "forOfStatement"
551
+ });
552
+ },
553
+ WhileStatement(node) {
554
+ context.report({
555
+ node,
556
+ messageId: "whileStatement"
557
+ });
558
+ },
559
+ DoWhileStatement(node) {
560
+ context.report({
561
+ node,
562
+ messageId: "doWhileStatement"
563
+ });
564
+ },
565
+ ConditionalExpression(node) {
566
+ context.report({
567
+ node,
568
+ messageId: "conditionalExpression"
569
+ });
570
+ }
571
+ };
572
+ }
573
+ };
574
+ //#endregion
488
575
  //#region src/rules/no-enum-number-widening.ts
489
576
  const noEnumNumberWidening = ESLintUtils.RuleCreator((name) => `https://github.com/ExaDev/eslint-config/blob/main/src/rules/${name}.ts`)({
490
577
  name: "no-enum-number-widening",
@@ -1325,6 +1412,7 @@ const plugin = {
1325
1412
  "barrel-direct-siblings-only": barrelDirectSiblingsOnly,
1326
1413
  "barrel-policy": barrelPolicy,
1327
1414
  "no-array-isarray-mutation": noArrayIsArrayMutation,
1415
+ "no-control-flow": noControlFlow,
1328
1416
  "no-enum-number-widening": noEnumNumberWidening,
1329
1417
  "no-enum-reverse-lookup-widening": noEnumReverseLookupWidening,
1330
1418
  "no-index-files": noIndexFiles,
@@ -1460,6 +1548,7 @@ function exadevConfig(options = {}, ...userConfigs) {
1460
1548
  return toPublicConfigArray([
1461
1549
  ...recommendedTypeChecked,
1462
1550
  ...jsdocAndTsdoc,
1551
+ ...jsonCanonicalConfig,
1463
1552
  ...buildReactConfig({ enabled: options.react }),
1464
1553
  ...buildNextjsConfig({ enabled: options.nextjs }),
1465
1554
  ...userConfigs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exadev-eslint-config",
3
- "version": "2.12.3",
3
+ "version": "2.14.0",
4
4
  "description": "Shared custom ESLint rules and plugin for ExaDev projects",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -88,6 +88,7 @@
88
88
  "@eslint/js": "^10.0.1",
89
89
  "@typescript-eslint/utils": "8.67.0",
90
90
  "eslint-plugin-jsdoc": "64.3.6",
91
+ "eslint-plugin-json-canonical": "1.0.0",
91
92
  "eslint-plugin-tsdoc": "^0.5.2",
92
93
  "ts-api-utils": "^2.5.0"
93
94
  },