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