@sarj/eslint-plugin 2.8.0 → 2.9.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
@@ -12,10 +12,19 @@ import sarj from "@sarj/eslint-plugin";
12
12
  export default [...sarj.configs.recommended];
13
13
  ```
14
14
 
15
- Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
15
+ 41 rules. Each rule's source under `src/rules/` carries its own `@fileoverview` rationale plus `meta.docs.description` + `meta.messages` — read the file for the full reasoning, including the false positives it deliberately does not fire on.
16
16
 
17
17
  Presets: `recommended` (warn-first), `strict` (every rule at error), `style-guide` (formatting/naming subset).
18
18
 
19
+ ## New in 2.9.0
20
+
21
+ Both distilled from two years of PR-review comments across ~1,065 PRs.
22
+
23
+ | Rule | What it catches | Preset |
24
+ |---|---|---|
25
+ | `no-zod-native-enum` | `z.nativeEnum(...)` and `z.enum(SomeTsEnum)` — the schema-layer back door around `no-enum`. Autofixes an inline string-literal object to `z.enum([...])`. | warn / error |
26
+ | `prefer-module-level-constant` | A literal-only `const` collection (array, object, `Set`, `Map`, `Object.freeze`) or non-global regex declared inside a function body, never mutated and never escaping — hoist it to module scope. Options: `minElements` (default 3), `checkRegex`, `ignoreTestFiles`. | warn / error |
27
+
19
28
  ## Options
20
29
 
21
30
  ### Declare your logger (`loggerNames` / `logFunctions`)
package/dist/index.cjs CHANGED
@@ -5322,6 +5322,484 @@ var no_storage_in_stateless_modules_default = import_utils43.ESLintUtils.RuleCre
5322
5322
  }
5323
5323
  });
5324
5324
 
5325
+ // src/rules/no-zod-native-enum.ts
5326
+ var import_utils44 = require("@typescript-eslint/utils");
5327
+ var ts2 = __toESM(require("typescript"), 1);
5328
+ var IGNORE_PATTERNS2 = [
5329
+ /[\\/]generated[\\/]/,
5330
+ /\.gen\.tsx?$/,
5331
+ /\.generated\.tsx?$/,
5332
+ /\.d\.ts$/
5333
+ ];
5334
+ function isIgnoredFile2(filename, sourceText) {
5335
+ if (IGNORE_PATTERNS2.some((re) => re.test(filename))) {
5336
+ return true;
5337
+ }
5338
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5339
+ }
5340
+ function isZodModule(source) {
5341
+ return /(^|[/@-])zod([/-]|$)/.test(source);
5342
+ }
5343
+ function unwrap3(node) {
5344
+ if (node.type === import_utils44.AST_NODE_TYPES.TSAsExpression || node.type === import_utils44.AST_NODE_TYPES.TSSatisfiesExpression) {
5345
+ return unwrap3(node.expression);
5346
+ }
5347
+ return node;
5348
+ }
5349
+ function stringValueTexts(node, sourceCode) {
5350
+ const texts = [];
5351
+ for (const prop of node.properties) {
5352
+ if (prop.type !== import_utils44.AST_NODE_TYPES.Property) {
5353
+ return null;
5354
+ }
5355
+ if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
5356
+ return null;
5357
+ }
5358
+ const value = prop.value;
5359
+ if (value.type !== import_utils44.AST_NODE_TYPES.Literal || typeof value.value !== "string") {
5360
+ return null;
5361
+ }
5362
+ const text = sourceCode.getText(value);
5363
+ if (!texts.includes(text)) {
5364
+ texts.push(text);
5365
+ }
5366
+ }
5367
+ return texts.length > 0 ? texts : null;
5368
+ }
5369
+ function resolvesToLocalEnum(node, scope) {
5370
+ let current = scope;
5371
+ while (current !== null) {
5372
+ const variable = current.variables.find((v) => v.name === node.name);
5373
+ if (variable !== void 0) {
5374
+ return variable.defs.some(
5375
+ (def) => def.node.type === import_utils44.AST_NODE_TYPES.TSEnumDeclaration
5376
+ );
5377
+ }
5378
+ current = current.upper;
5379
+ }
5380
+ return false;
5381
+ }
5382
+ var ENUM_SYMBOL_FLAGS = ts2.SymbolFlags.RegularEnum | ts2.SymbolFlags.ConstEnum | ts2.SymbolFlags.Enum;
5383
+ function resolvesToImportedEnum(node, services) {
5384
+ const checker = services.program.getTypeChecker();
5385
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
5386
+ let symbol = checker.getSymbolAtLocation(tsNode);
5387
+ if (symbol === void 0) {
5388
+ return false;
5389
+ }
5390
+ if ((symbol.flags & ts2.SymbolFlags.Alias) !== 0) {
5391
+ symbol = checker.getAliasedSymbol(symbol);
5392
+ }
5393
+ return (symbol.flags & ENUM_SYMBOL_FLAGS) !== 0;
5394
+ }
5395
+ var no_zod_native_enum_default = import_utils44.ESLintUtils.RuleCreator(
5396
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5397
+ )({
5398
+ name: "no-zod-native-enum",
5399
+ meta: {
5400
+ type: "suggestion",
5401
+ fixable: "code",
5402
+ docs: {
5403
+ description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
5404
+ },
5405
+ schema: [],
5406
+ messages: {
5407
+ nativeEnum: '`z.nativeEnum()` exists to wrap a TypeScript `enum`, which `no-enum` bans. Use `z.enum(["a", "b"])` and derive the union with `z.infer<typeof Schema>`.',
5408
+ enumOfTsEnum: '`z.enum()` is being passed the TypeScript enum `{{name}}`, which `no-enum` bans. Pass a string-literal array instead: `z.enum(["a", "b"])`.'
5409
+ }
5410
+ },
5411
+ defaultOptions: [],
5412
+ create(context) {
5413
+ const sourceCode = context.sourceCode;
5414
+ if (isIgnoredFile2(context.filename, sourceCode.getText())) {
5415
+ return {};
5416
+ }
5417
+ let services;
5418
+ try {
5419
+ services = import_utils44.ESLintUtils.getParserServices(context);
5420
+ } catch {
5421
+ services = null;
5422
+ }
5423
+ const zodImportedNames = /* @__PURE__ */ new Map();
5424
+ function isZodMemberCall(node, api) {
5425
+ const callee = node.callee;
5426
+ if (callee.type === import_utils44.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils44.AST_NODE_TYPES.Identifier) {
5427
+ return callee.property.name === api;
5428
+ }
5429
+ if (callee.type === import_utils44.AST_NODE_TYPES.Identifier) {
5430
+ return zodImportedNames.get(callee.name) === api;
5431
+ }
5432
+ return false;
5433
+ }
5434
+ function buildFix(node) {
5435
+ const callee = node.callee;
5436
+ if (callee.type !== import_utils44.AST_NODE_TYPES.MemberExpression || callee.property.type !== import_utils44.AST_NODE_TYPES.Identifier) {
5437
+ return null;
5438
+ }
5439
+ const arg = node.arguments[0];
5440
+ if (arg === void 0 || node.arguments.length !== 1 || arg.type === import_utils44.AST_NODE_TYPES.SpreadElement) {
5441
+ return null;
5442
+ }
5443
+ const inner = unwrap3(arg);
5444
+ if (inner.type !== import_utils44.AST_NODE_TYPES.ObjectExpression) {
5445
+ return null;
5446
+ }
5447
+ const values = stringValueTexts(inner, sourceCode);
5448
+ if (values === null) {
5449
+ return null;
5450
+ }
5451
+ const property = callee.property;
5452
+ const replacementArg = `[${values.join(", ")}]`;
5453
+ return (fixer) => [
5454
+ fixer.replaceText(property, "enum"),
5455
+ fixer.replaceText(arg, replacementArg)
5456
+ ];
5457
+ }
5458
+ return {
5459
+ ImportDeclaration(node) {
5460
+ if (!isZodModule(node.source.value)) {
5461
+ return;
5462
+ }
5463
+ for (const spec of node.specifiers) {
5464
+ if (spec.type === import_utils44.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === import_utils44.AST_NODE_TYPES.Identifier) {
5465
+ zodImportedNames.set(spec.local.name, spec.imported.name);
5466
+ }
5467
+ }
5468
+ },
5469
+ CallExpression(node) {
5470
+ if (isZodMemberCall(node, "nativeEnum")) {
5471
+ const fix = buildFix(node);
5472
+ context.report({
5473
+ node,
5474
+ messageId: "nativeEnum",
5475
+ ...fix === null ? {} : { fix }
5476
+ });
5477
+ return;
5478
+ }
5479
+ if (!isZodMemberCall(node, "enum")) {
5480
+ return;
5481
+ }
5482
+ const arg = node.arguments[0];
5483
+ if (arg === void 0 || arg.type !== import_utils44.AST_NODE_TYPES.Identifier) {
5484
+ return;
5485
+ }
5486
+ const isEnum = resolvesToLocalEnum(arg, sourceCode.getScope(arg)) || services !== null && resolvesToImportedEnum(arg, services);
5487
+ if (isEnum) {
5488
+ context.report({
5489
+ node,
5490
+ messageId: "enumOfTsEnum",
5491
+ data: { name: arg.name }
5492
+ });
5493
+ }
5494
+ }
5495
+ };
5496
+ }
5497
+ });
5498
+
5499
+ // src/rules/prefer-module-level-constant.ts
5500
+ var import_utils45 = require("@typescript-eslint/utils");
5501
+ var DEFAULT_MIN_ELEMENTS = 3;
5502
+ var MAX_LITERAL_DEPTH = 4;
5503
+ var IGNORE_PATTERNS3 = [
5504
+ /[\\/]generated[\\/]/,
5505
+ /\.gen\.tsx?$/,
5506
+ /\.generated\.tsx?$/,
5507
+ /\.d\.ts$/
5508
+ ];
5509
+ var TEST_FILE_PATTERNS = [
5510
+ /\.(?:test|spec)\.[cm]?[jt]sx?$/,
5511
+ /[\\/]__tests__[\\/]/,
5512
+ /[\\/]__mocks__[\\/]/,
5513
+ /[\\/]tests?[\\/]/,
5514
+ /\.stories\.[cm]?[jt]sx?$/
5515
+ ];
5516
+ var MUTATING_METHODS = /* @__PURE__ */ new Set([
5517
+ // Array
5518
+ "push",
5519
+ "pop",
5520
+ "shift",
5521
+ "unshift",
5522
+ "splice",
5523
+ "sort",
5524
+ "reverse",
5525
+ "fill",
5526
+ "copyWithin",
5527
+ // Set / Map
5528
+ "add",
5529
+ "set",
5530
+ "delete",
5531
+ "clear",
5532
+ // Object-ish escape hatches
5533
+ "assign"
5534
+ ]);
5535
+ var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([
5536
+ import_utils45.AST_NODE_TYPES.FunctionDeclaration,
5537
+ import_utils45.AST_NODE_TYPES.FunctionExpression,
5538
+ import_utils45.AST_NODE_TYPES.ArrowFunctionExpression
5539
+ ]);
5540
+ var COLLECTION_CONSTRUCTORS = /* @__PURE__ */ new Set(["Set", "Map"]);
5541
+ function isIgnoredFile3(filename, sourceText) {
5542
+ if (IGNORE_PATTERNS3.some((re) => re.test(filename))) {
5543
+ return true;
5544
+ }
5545
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5546
+ }
5547
+ function isTestFile2(filename) {
5548
+ return TEST_FILE_PATTERNS.some((re) => re.test(filename));
5549
+ }
5550
+ function unwrap4(node) {
5551
+ if (node.type === import_utils45.AST_NODE_TYPES.TSAsExpression || node.type === import_utils45.AST_NODE_TYPES.TSSatisfiesExpression || node.type === import_utils45.AST_NODE_TYPES.TSNonNullExpression) {
5552
+ return unwrap4(node.expression);
5553
+ }
5554
+ return node;
5555
+ }
5556
+ function isRegexLiteral(node) {
5557
+ return node.type === import_utils45.AST_NODE_TYPES.Literal && "regex" in node && node.regex !== void 0;
5558
+ }
5559
+ function isLiteralOnly(node, depth) {
5560
+ if (depth > MAX_LITERAL_DEPTH) {
5561
+ return false;
5562
+ }
5563
+ const inner = unwrap4(node);
5564
+ switch (inner.type) {
5565
+ case import_utils45.AST_NODE_TYPES.Literal: {
5566
+ return true;
5567
+ }
5568
+ case import_utils45.AST_NODE_TYPES.TemplateLiteral: {
5569
+ return inner.expressions.length === 0;
5570
+ }
5571
+ case import_utils45.AST_NODE_TYPES.UnaryExpression: {
5572
+ return (inner.operator === "-" || inner.operator === "+") && inner.argument.type === import_utils45.AST_NODE_TYPES.Literal && typeof inner.argument.value === "number";
5573
+ }
5574
+ case import_utils45.AST_NODE_TYPES.ArrayExpression: {
5575
+ return inner.elements.every(
5576
+ (el) => el !== null && el.type !== import_utils45.AST_NODE_TYPES.SpreadElement && isLiteralOnly(el, depth + 1)
5577
+ );
5578
+ }
5579
+ case import_utils45.AST_NODE_TYPES.ObjectExpression: {
5580
+ return inner.properties.every((prop) => {
5581
+ if (prop.type !== import_utils45.AST_NODE_TYPES.Property) {
5582
+ return false;
5583
+ }
5584
+ if (prop.shorthand || prop.method || prop.kind !== "init") {
5585
+ return false;
5586
+ }
5587
+ if (prop.computed && prop.key.type !== import_utils45.AST_NODE_TYPES.Literal) {
5588
+ return false;
5589
+ }
5590
+ return isLiteralOnly(prop.value, depth + 1);
5591
+ });
5592
+ }
5593
+ default: {
5594
+ return false;
5595
+ }
5596
+ }
5597
+ }
5598
+ function unwrapObjectFreeze(node) {
5599
+ const inner = unwrap4(node);
5600
+ if (inner.type === import_utils45.AST_NODE_TYPES.CallExpression && inner.callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !inner.callee.computed && inner.callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && inner.callee.object.name === "Object" && inner.callee.property.type === import_utils45.AST_NODE_TYPES.Identifier && inner.callee.property.name === "freeze" && inner.arguments.length === 1 && inner.arguments[0] !== void 0 && inner.arguments[0].type !== import_utils45.AST_NODE_TYPES.SpreadElement) {
5601
+ return unwrap4(inner.arguments[0]);
5602
+ }
5603
+ return inner;
5604
+ }
5605
+ function classify(init, checkRegex) {
5606
+ const node = unwrapObjectFreeze(init);
5607
+ if (isRegexLiteral(node)) {
5608
+ if (!checkRegex) {
5609
+ return null;
5610
+ }
5611
+ if (/[gy]/.test(node.regex.flags)) {
5612
+ return null;
5613
+ }
5614
+ return { kind: "regex", size: 1 };
5615
+ }
5616
+ if (node.type === import_utils45.AST_NODE_TYPES.ArrayExpression) {
5617
+ return isLiteralOnly(node, 0) ? { kind: "array", size: node.elements.length } : null;
5618
+ }
5619
+ if (node.type === import_utils45.AST_NODE_TYPES.ObjectExpression) {
5620
+ return isLiteralOnly(node, 0) ? { kind: "object", size: node.properties.length } : null;
5621
+ }
5622
+ if (node.type === import_utils45.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils45.AST_NODE_TYPES.Identifier && COLLECTION_CONSTRUCTORS.has(node.callee.name)) {
5623
+ const arg = node.arguments[0];
5624
+ if (node.arguments.length !== 1 || arg === void 0 || arg.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
5625
+ return null;
5626
+ }
5627
+ const entries = unwrap4(arg);
5628
+ if (entries.type !== import_utils45.AST_NODE_TYPES.ArrayExpression) {
5629
+ return null;
5630
+ }
5631
+ return isLiteralOnly(entries, 0) ? { kind: node.callee.name === "Set" ? "Set" : "Map", size: entries.elements.length } : null;
5632
+ }
5633
+ return null;
5634
+ }
5635
+ function enclosingFunction2(node) {
5636
+ let current = node.parent;
5637
+ while (current !== void 0 && current !== null) {
5638
+ if (FUNCTION_TYPES3.has(current.type)) {
5639
+ return current;
5640
+ }
5641
+ current = current.parent;
5642
+ }
5643
+ return null;
5644
+ }
5645
+ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
5646
+ [
5647
+ [
5648
+ "Object",
5649
+ /* @__PURE__ */ new Set(["keys", "values", "entries", "freeze", "fromEntries", "assign"])
5650
+ ],
5651
+ ["Array", /* @__PURE__ */ new Set(["from", "isArray"])],
5652
+ ["JSON", /* @__PURE__ */ new Set(["stringify"])]
5653
+ ]
5654
+ );
5655
+ function isNonRetainingBuiltinCall(node, argument) {
5656
+ const callee = node.callee;
5657
+ if (callee.type === import_utils45.AST_NODE_TYPES.Identifier && callee.name === "structuredClone") {
5658
+ return true;
5659
+ }
5660
+ if (callee.type !== import_utils45.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== import_utils45.AST_NODE_TYPES.Identifier || callee.property.type !== import_utils45.AST_NODE_TYPES.Identifier) {
5661
+ return false;
5662
+ }
5663
+ const members = NON_RETAINING_BUILTINS.get(callee.object.name);
5664
+ if (members === void 0 || !members.has(callee.property.name)) {
5665
+ return false;
5666
+ }
5667
+ if (callee.object.name === "Object" && callee.property.name === "assign") {
5668
+ return node.arguments[0] !== argument;
5669
+ }
5670
+ return true;
5671
+ }
5672
+ function isSafeRead(identifier) {
5673
+ const parent = identifier.parent;
5674
+ if (parent.type === import_utils45.AST_NODE_TYPES.MemberExpression) {
5675
+ if (parent.object !== identifier) {
5676
+ return true;
5677
+ }
5678
+ const grandparent = parent.parent;
5679
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
5680
+ return false;
5681
+ }
5682
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.UpdateExpression) {
5683
+ return false;
5684
+ }
5685
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
5686
+ return false;
5687
+ }
5688
+ if (!parent.computed && parent.property.type === import_utils45.AST_NODE_TYPES.Identifier && MUTATING_METHODS.has(parent.property.name) && grandparent.type === import_utils45.AST_NODE_TYPES.CallExpression && grandparent.callee === parent) {
5689
+ return false;
5690
+ }
5691
+ return true;
5692
+ }
5693
+ if (parent.type === import_utils45.AST_NODE_TYPES.ForOfStatement && parent.right === identifier) {
5694
+ return true;
5695
+ }
5696
+ if (parent.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
5697
+ return true;
5698
+ }
5699
+ if (parent.type === import_utils45.AST_NODE_TYPES.BinaryExpression) {
5700
+ return true;
5701
+ }
5702
+ if (parent.type === import_utils45.AST_NODE_TYPES.CallExpression && parent.arguments.includes(identifier) && isNonRetainingBuiltinCall(parent, identifier)) {
5703
+ return true;
5704
+ }
5705
+ if (parent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && parent.operator !== "delete") {
5706
+ return true;
5707
+ }
5708
+ return false;
5709
+ }
5710
+ var prefer_module_level_constant_default = import_utils45.ESLintUtils.RuleCreator(
5711
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5712
+ )({
5713
+ name: "prefer-module-level-constant",
5714
+ meta: {
5715
+ type: "suggestion",
5716
+ docs: {
5717
+ description: "Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once."
5718
+ },
5719
+ schema: [
5720
+ {
5721
+ type: "object",
5722
+ additionalProperties: false,
5723
+ properties: {
5724
+ minElements: { type: "number", minimum: 1 },
5725
+ checkRegex: { type: "boolean" },
5726
+ ignoreTestFiles: { type: "boolean" }
5727
+ }
5728
+ }
5729
+ ],
5730
+ messages: {
5731
+ hoistCollection: "`{{name}}` is a literal-only {{kind}} rebuilt on every call. Hoist it to module scope so it is allocated once and can be reused, exported, and tested.",
5732
+ hoistRegex: "`{{name}}` is a constant regex recompiled on every call. Hoist it to module scope."
5733
+ }
5734
+ },
5735
+ defaultOptions: [{}],
5736
+ create(context, [optionsArg]) {
5737
+ const options = optionsArg ?? {};
5738
+ const minElements = options.minElements ?? DEFAULT_MIN_ELEMENTS;
5739
+ const checkRegex = options.checkRegex ?? true;
5740
+ const ignoreTestFiles = options.ignoreTestFiles ?? true;
5741
+ const sourceCode = context.sourceCode;
5742
+ const filename = context.filename;
5743
+ if (isIgnoredFile3(filename, sourceCode.getText())) {
5744
+ return {};
5745
+ }
5746
+ if (ignoreTestFiles && isTestFile2(filename)) {
5747
+ return {};
5748
+ }
5749
+ function allReferencesAreSafeReads(declarator) {
5750
+ const variables = sourceCode.getDeclaredVariables(declarator);
5751
+ const variable = variables[0];
5752
+ if (variable === void 0) {
5753
+ return false;
5754
+ }
5755
+ for (const reference of variable.references) {
5756
+ if (reference.init === true) {
5757
+ continue;
5758
+ }
5759
+ if (reference.isWrite()) {
5760
+ return false;
5761
+ }
5762
+ if (reference.identifier.type !== import_utils45.AST_NODE_TYPES.Identifier) {
5763
+ return false;
5764
+ }
5765
+ if (!isSafeRead(reference.identifier)) {
5766
+ return false;
5767
+ }
5768
+ }
5769
+ return true;
5770
+ }
5771
+ return {
5772
+ VariableDeclarator(node) {
5773
+ const declaration = node.parent;
5774
+ if (declaration.type !== import_utils45.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const" || declaration.declare === true) {
5775
+ return;
5776
+ }
5777
+ if (node.id.type !== import_utils45.AST_NODE_TYPES.Identifier || node.init === null) {
5778
+ return;
5779
+ }
5780
+ if (enclosingFunction2(node) === null) {
5781
+ return;
5782
+ }
5783
+ const candidate = classify(node.init, checkRegex);
5784
+ if (candidate === null) {
5785
+ return;
5786
+ }
5787
+ if (candidate.kind !== "regex" && candidate.size < minElements) {
5788
+ return;
5789
+ }
5790
+ if (!allReferencesAreSafeReads(node)) {
5791
+ return;
5792
+ }
5793
+ context.report({
5794
+ node: node.id,
5795
+ messageId: candidate.kind === "regex" ? "hoistRegex" : "hoistCollection",
5796
+ data: { name: node.id.name, kind: candidate.kind }
5797
+ });
5798
+ }
5799
+ };
5800
+ }
5801
+ });
5802
+
5325
5803
  // src/index.ts
5326
5804
  var rules = {
5327
5805
  "enforce-file-structure": enforce_file_structure_default,
@@ -5362,12 +5840,14 @@ var rules = {
5362
5840
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
5363
5841
  "no-dynamic-sql": no_dynamic_sql_default,
5364
5842
  "no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
5365
- "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
5843
+ "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default,
5844
+ "no-zod-native-enum": no_zod_native_enum_default,
5845
+ "prefer-module-level-constant": prefer_module_level_constant_default
5366
5846
  };
5367
5847
  var plugin = {
5368
5848
  meta: {
5369
5849
  name: "@sarj/eslint-plugin",
5370
- version: "2.8.0"
5850
+ version: "2.9.0"
5371
5851
  },
5372
5852
  rules,
5373
5853
  configs: {
@@ -5416,7 +5896,14 @@ var plugin = {
5416
5896
  "@sarj/no-repeated-string-literal": "warn",
5417
5897
  "@sarj/no-positional-tuple-return": "warn",
5418
5898
  // Injection guard — low FP, applies to any repo touching SQL.
5419
- "@sarj/no-dynamic-sql": "warn"
5899
+ "@sarj/no-dynamic-sql": "warn",
5900
+ // Mined from two years of PR review (SARJ-928). Schema-layer sibling of
5901
+ // `no-enum`; autofixable for inline string-literal objects.
5902
+ "@sarj/no-zod-native-enum": "warn",
5903
+ // Mined from two years of PR review — the single most frequent uncovered
5904
+ // theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
5905
+ // positives, so it is safe to run everywhere.
5906
+ "@sarj/prefer-module-level-constant": "warn"
5420
5907
  }
5421
5908
  },
5422
5909
  strict: {
@@ -5476,7 +5963,10 @@ var plugin = {
5476
5963
  // and takes an `allow` list for repos that lay their client layer out
5477
5964
  // differently.
5478
5965
  "@sarj/no-raw-fetch-outside-clients": "error",
5479
- "@sarj/no-storage-in-stateless-modules": "error"
5966
+ "@sarj/no-storage-in-stateless-modules": "error",
5967
+ // Mined from two years of PR review (SARJ-928).
5968
+ "@sarj/no-zod-native-enum": "error",
5969
+ "@sarj/prefer-module-level-constant": "error"
5480
5970
  }
5481
5971
  }
5482
5972
  }