@sarj/eslint-plugin 2.8.0 → 2.10.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.cjs CHANGED
@@ -287,12 +287,115 @@ function isProse(text) {
287
287
  }
288
288
  return false;
289
289
  }
290
- function isRedundantNarration(body) {
290
+ var NARRATION_MAX_WORDS = 6;
291
+ var NARRATION_MIN_CONTENT = 1;
292
+ var TOKEN_PLURAL_MIN = 4;
293
+ var RESTATABLE_STATEMENTS = /* @__PURE__ */ new Set([
294
+ import_utils3.AST_NODE_TYPES.ExpressionStatement,
295
+ import_utils3.AST_NODE_TYPES.ReturnStatement,
296
+ import_utils3.AST_NODE_TYPES.ThrowStatement,
297
+ import_utils3.AST_NODE_TYPES.VariableDeclaration
298
+ ]);
299
+ var NARRATION_VERB_RE = /^(?:add|append|assign|build|calculate|call|check|clear|close|compute|convert|copy|count|create|declare|decrement|define|delete|extract|fetch|filter|find|format|generate|get|handle|increment|init|initialise|initialize|insert|iterate|join|load|log|loop|make|map|merge|open|parse|print|process|push|read|remove|render|reset|return|save|send|set|setup|sort|split|start|stop|store|update|validate|wrap|write)(?:s|es|d|ed|ing)?$/i;
300
+ var NARRATION_STOPWORDS = /* @__PURE__ */ new Set([
301
+ "a",
302
+ "all",
303
+ "an",
304
+ "and",
305
+ "any",
306
+ "are",
307
+ "as",
308
+ "at",
309
+ "back",
310
+ "be",
311
+ "both",
312
+ "by",
313
+ "each",
314
+ "for",
315
+ "from",
316
+ "here",
317
+ "if",
318
+ "in",
319
+ "into",
320
+ "is",
321
+ "it",
322
+ "its",
323
+ "just",
324
+ "new",
325
+ "of",
326
+ "on",
327
+ "one",
328
+ "onto",
329
+ "or",
330
+ "our",
331
+ "out",
332
+ "over",
333
+ "so",
334
+ "that",
335
+ "the",
336
+ "then",
337
+ "this",
338
+ "to",
339
+ "up",
340
+ "us",
341
+ "we",
342
+ "when",
343
+ "with"
344
+ ]);
345
+ function normalizeToken(word) {
346
+ const lower = word.toLowerCase();
347
+ return lower.length > TOKEN_PLURAL_MIN && lower.endsWith("s") && !lower.endsWith("ss") ? lower.slice(0, -1) : lower;
348
+ }
349
+ function codeTokens(source) {
350
+ const tokens = /* @__PURE__ */ new Set();
351
+ for (const identifier of source.match(/[A-Za-z_$][\w$]*/g) ?? []) {
352
+ tokens.add(normalizeToken(identifier));
353
+ for (const part of identifier.split(/[_$]+|(?<=[a-z0-9])(?=[A-Z])/)) {
354
+ if (part.length > 0) tokens.add(normalizeToken(part));
355
+ }
356
+ }
357
+ return tokens;
358
+ }
359
+ function isTrivialInitializer(node) {
360
+ return node.declarations.every((declarator) => {
361
+ const init = declarator.init;
362
+ if (init == null || init.type === import_utils3.AST_NODE_TYPES.Literal) return true;
363
+ return init.type === import_utils3.AST_NODE_TYPES.ArrayExpression && init.elements.length === 0 || init.type === import_utils3.AST_NODE_TYPES.ObjectExpression && init.properties.length === 0;
364
+ });
365
+ }
366
+ function restatableStatementBelow(comment, sourceCode) {
367
+ const token = sourceCode.getTokenAfter(comment, { includeComments: false });
368
+ if (token === null || token.loc.start.line !== comment.loc.end.line + 1) return null;
369
+ for (let node = sourceCode.getNodeByRangeIndex(token.range[0]); node != null && node.type !== import_utils3.AST_NODE_TYPES.Program; node = node.parent) {
370
+ if (!RESTATABLE_STATEMENTS.has(node.type)) continue;
371
+ if (node.loc.start.line !== token.loc.start.line || node.loc.end.line !== node.loc.start.line) {
372
+ return null;
373
+ }
374
+ if (node.type === import_utils3.AST_NODE_TYPES.VariableDeclaration && isTrivialInitializer(node)) {
375
+ return null;
376
+ }
377
+ return sourceCode.getText(node);
378
+ }
379
+ return null;
380
+ }
381
+ function restatesNextLine(body, statement) {
382
+ if (statement === null) return false;
383
+ const words = body.match(/[A-Za-z][\w$]*/g) ?? [];
384
+ const opener = words[0];
385
+ if (opener === void 0 || words.length > NARRATION_MAX_WORDS) return false;
386
+ if (!NARRATION_VERB_RE.test(opener)) return false;
387
+ const content = words.slice(1).map(normalizeToken).filter((word) => !NARRATION_STOPWORDS.has(word));
388
+ if (content.length < NARRATION_MIN_CONTENT) return false;
389
+ const head = statement.split("(")[0] ?? statement;
390
+ const code = codeTokens(head);
391
+ return content.every((word) => code.has(word));
392
+ }
393
+ function isRedundantNarration(body, statementBelow) {
291
394
  const t = body.trim();
292
395
  if (!t || looksLikeCode(t) || hasPseudocode(t)) return false;
293
396
  if (STEP_NARRATION_RE.test(t)) return true;
294
397
  if (META_COMMENTARY_RE.test(t)) return true;
295
- return false;
398
+ return restatesNextLine(t, statementBelow);
296
399
  }
297
400
  function hasCommentedOutCode(texts, precedingProse) {
298
401
  for (let i = 0; i < texts.length; i++) {
@@ -377,7 +480,8 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
377
480
  }
378
481
  if (comment.type === "Line" && texts.length === 1) {
379
482
  const body = texts[0];
380
- if (body !== void 0 && isRedundantNarration(body)) {
483
+ const statement = restatableStatementBelow(comment, sourceCode);
484
+ if (body !== void 0 && isRedundantNarration(body, statement)) {
381
485
  context.report({ node: comment, messageId: "redundantNarration" });
382
486
  }
383
487
  }
@@ -3638,6 +3742,10 @@ function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
3638
3742
  if (last !== void 0 && innocuous.has(last)) {
3639
3743
  return false;
3640
3744
  }
3745
+ const first = leadingWord(identifier);
3746
+ if (first !== void 0 && FLAG_PREFIXES.has(first)) {
3747
+ return false;
3748
+ }
3641
3749
  if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
3642
3750
  return true;
3643
3751
  }
@@ -3648,10 +3756,6 @@ function isAuthSecretName(identifier) {
3648
3756
  return false;
3649
3757
  }
3650
3758
  const tokens = tokenize(identifier);
3651
- const first = leadingWord(identifier);
3652
- if (first !== void 0 && FLAG_PREFIXES.has(first)) {
3653
- return false;
3654
- }
3655
3759
  const last = tokens.at(-1);
3656
3760
  if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
3657
3761
  return false;
@@ -3728,6 +3832,49 @@ function isRawSecretValue(prop) {
3728
3832
  }
3729
3833
  return prop.value.type === "Identifier" || prop.value.type === "MemberExpression";
3730
3834
  }
3835
+ var RAW_BLOB_WORDS = /* @__PURE__ */ new Set([
3836
+ "body",
3837
+ "bodies",
3838
+ "payload",
3839
+ "payloads",
3840
+ "params"
3841
+ ]);
3842
+ var RAW_BLOB_IDENTIFIERS = /* @__PURE__ */ new Set(["formdata"]);
3843
+ var BLOB_REDACTION_RE = /redact|sanit|scrub|mask|truncat|anonym|filtered|preview|summar/i;
3844
+ var BLOB_REDACTION_TOKENS = /* @__PURE__ */ new Set([
3845
+ "safe",
3846
+ "clean",
3847
+ "shape",
3848
+ "keys",
3849
+ "public"
3850
+ ]);
3851
+ function isRawBlobName(name) {
3852
+ if (REDACTION_RE.test(name) || BLOB_REDACTION_RE.test(name)) {
3853
+ return false;
3854
+ }
3855
+ const tokens = tokenize(name);
3856
+ if (tokens.some((tok) => BLOB_REDACTION_TOKENS.has(tok))) {
3857
+ return false;
3858
+ }
3859
+ const first = leadingWord(name);
3860
+ if (first !== void 0 && FLAG_PREFIXES.has(first)) {
3861
+ return false;
3862
+ }
3863
+ if (RAW_BLOB_IDENTIFIERS.has(name.toLowerCase())) {
3864
+ return true;
3865
+ }
3866
+ const last = tokens.at(-1);
3867
+ return last !== void 0 && RAW_BLOB_WORDS.has(last);
3868
+ }
3869
+ function rawBlobValueName(value) {
3870
+ if (value.type === "Identifier") {
3871
+ return isRawBlobName(value.name) ? value.name : null;
3872
+ }
3873
+ if (value.type === "MemberExpression" && !value.computed && value.property.type === "Identifier") {
3874
+ return isRawBlobName(value.property.name) ? value.property.name : null;
3875
+ }
3876
+ return null;
3877
+ }
3731
3878
  function propertyKeyName2(prop) {
3732
3879
  if (prop.computed) {
3733
3880
  return null;
@@ -3747,7 +3894,7 @@ var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
3747
3894
  meta: {
3748
3895
  type: "problem",
3749
3896
  docs: {
3750
- description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
3897
+ description: "Disallow passing a secret-named value or a raw request/response blob to a logging call; both leak to log sinks. Redact or omit."
3751
3898
  },
3752
3899
  schema: [
3753
3900
  {
@@ -3757,52 +3904,58 @@ var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
3757
3904
  }
3758
3905
  ],
3759
3906
  messages: {
3760
- noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
3907
+ noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it.",
3908
+ noRawBodyInLog: "Raw `{{name}}` passed to a logging call. Request/response blobs carry PII and often echo credentials back, and log sinks have no retention policy. Log a derived value instead (a status, `{{name}}.id`, a length, a truncated issue list) or pass it through a redactor (`redact({{name}})`)."
3761
3909
  }
3762
3910
  },
3763
3911
  defaultOptions: [{}],
3764
3912
  create(context, [loggingOptions]) {
3765
3913
  const matcher = createLogMatcher(loggingOptions);
3914
+ const blobArmApplies = !isTestFile(context.filename);
3915
+ function reportSecretArgument(arg) {
3916
+ const name = arg.type === "Identifier" ? arg.name : arg.type === "MemberExpression" && !arg.computed && arg.property.type === "Identifier" ? arg.property.name : null;
3917
+ if (name === null || !isSecretKeyword(name)) {
3918
+ return false;
3919
+ }
3920
+ context.report({ node: arg, messageId: "noSecretInLog", data: { name } });
3921
+ return true;
3922
+ }
3923
+ function reportSecretProperty(prop) {
3924
+ const keyName2 = propertyKeyName2(prop);
3925
+ if (keyName2 === null || !isSecretKeyword(keyName2) || !isRawSecretValue(prop)) {
3926
+ return false;
3927
+ }
3928
+ context.report({ node: prop, messageId: "noSecretInLog", data: { name: keyName2 } });
3929
+ return true;
3930
+ }
3931
+ function reportRawBlob(node, value) {
3932
+ if (!blobArmApplies) {
3933
+ return;
3934
+ }
3935
+ const name = rawBlobValueName(value);
3936
+ if (name !== null) {
3937
+ context.report({ node, messageId: "noRawBodyInLog", data: { name } });
3938
+ }
3939
+ }
3766
3940
  return {
3767
3941
  CallExpression(node) {
3768
3942
  if (!matcher.isLoggingCall(node)) {
3769
3943
  return;
3770
3944
  }
3771
3945
  for (const arg of node.arguments) {
3772
- if (arg.type === "Identifier") {
3773
- if (isSecretKeyword(arg.name)) {
3774
- context.report({
3775
- node: arg,
3776
- messageId: "noSecretInLog",
3777
- data: { name: arg.name }
3778
- });
3779
- }
3780
- continue;
3781
- }
3782
- if (arg.type === "MemberExpression") {
3783
- if (!arg.computed && arg.property.type === "Identifier" && isSecretKeyword(arg.property.name)) {
3784
- context.report({
3785
- node: arg,
3786
- messageId: "noSecretInLog",
3787
- data: { name: arg.property.name }
3788
- });
3789
- }
3790
- continue;
3791
- }
3792
3946
  if (arg.type === "ObjectExpression") {
3793
3947
  for (const prop of arg.properties) {
3794
3948
  if (prop.type !== "Property") {
3795
3949
  continue;
3796
3950
  }
3797
- const keyName2 = propertyKeyName2(prop);
3798
- if (keyName2 !== null && isSecretKeyword(keyName2) && isRawSecretValue(prop)) {
3799
- context.report({
3800
- node: prop,
3801
- messageId: "noSecretInLog",
3802
- data: { name: keyName2 }
3803
- });
3951
+ if (!reportSecretProperty(prop)) {
3952
+ reportRawBlob(prop, prop.value);
3804
3953
  }
3805
3954
  }
3955
+ continue;
3956
+ }
3957
+ if (!reportSecretArgument(arg)) {
3958
+ reportRawBlob(arg, arg);
3806
3959
  }
3807
3960
  }
3808
3961
  }
@@ -4577,7 +4730,7 @@ function functionName(node) {
4577
4730
  }
4578
4731
  return null;
4579
4732
  }
4580
- function isExported(node) {
4733
+ function isInlineExported(node) {
4581
4734
  for (let current = node; current != null; current = current.parent) {
4582
4735
  const parent = current.parent;
4583
4736
  if (parent?.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent?.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration) {
@@ -4586,6 +4739,58 @@ function isExported(node) {
4586
4739
  }
4587
4740
  return false;
4588
4741
  }
4742
+ function moduleScopeBindingName(node) {
4743
+ let current = node;
4744
+ let child = node;
4745
+ while (current.parent != null && current.parent.type !== import_utils35.AST_NODE_TYPES.Program) {
4746
+ child = current;
4747
+ current = current.parent;
4748
+ }
4749
+ if (current.parent?.type !== import_utils35.AST_NODE_TYPES.Program) {
4750
+ return null;
4751
+ }
4752
+ if (current.type === import_utils35.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils35.AST_NODE_TYPES.ClassDeclaration) {
4753
+ return current.id?.name ?? null;
4754
+ }
4755
+ if (current.type === import_utils35.AST_NODE_TYPES.VariableDeclaration) {
4756
+ if (child.type !== import_utils35.AST_NODE_TYPES.VariableDeclarator || child.id.type !== import_utils35.AST_NODE_TYPES.Identifier) {
4757
+ return null;
4758
+ }
4759
+ return child.id.name;
4760
+ }
4761
+ return null;
4762
+ }
4763
+ function specifierExportedNames(program) {
4764
+ const names = /* @__PURE__ */ new Set();
4765
+ for (const statement of program.body) {
4766
+ if (statement.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration && statement.declaration == null && statement.source == null && statement.exportKind !== "type") {
4767
+ for (const specifier of statement.specifiers) {
4768
+ if (specifier.exportKind !== "type" && specifier.local.type === import_utils35.AST_NODE_TYPES.Identifier) {
4769
+ names.add(specifier.local.name);
4770
+ }
4771
+ }
4772
+ continue;
4773
+ }
4774
+ if (statement.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration && statement.declaration.type === import_utils35.AST_NODE_TYPES.Identifier) {
4775
+ names.add(statement.declaration.name);
4776
+ continue;
4777
+ }
4778
+ if (statement.type === import_utils35.AST_NODE_TYPES.TSExportAssignment && statement.expression.type === import_utils35.AST_NODE_TYPES.Identifier) {
4779
+ names.add(statement.expression.name);
4780
+ }
4781
+ }
4782
+ return names;
4783
+ }
4784
+ function isExported(node, specifierExports) {
4785
+ if (isInlineExported(node)) {
4786
+ return true;
4787
+ }
4788
+ if (specifierExports.size === 0) {
4789
+ return false;
4790
+ }
4791
+ const binding = moduleScopeBindingName(node);
4792
+ return binding !== null && specifierExports.has(binding);
4793
+ }
4589
4794
  var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
4590
4795
  (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
4591
4796
  )({
@@ -4602,6 +4807,7 @@ var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
4602
4807
  },
4603
4808
  defaultOptions: [],
4604
4809
  create(context) {
4810
+ const specifierExports = specifierExportedNames(context.sourceCode.ast);
4605
4811
  const check = (node) => {
4606
4812
  const annotation = node.returnType?.typeAnnotation;
4607
4813
  if (annotation === void 0) {
@@ -4615,7 +4821,7 @@ var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
4615
4821
  if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
4616
4822
  return;
4617
4823
  }
4618
- if (!isExported(node)) {
4824
+ if (!isExported(node, specifierExports)) {
4619
4825
  return;
4620
4826
  }
4621
4827
  context.report({
@@ -5322,6 +5528,484 @@ var no_storage_in_stateless_modules_default = import_utils43.ESLintUtils.RuleCre
5322
5528
  }
5323
5529
  });
5324
5530
 
5531
+ // src/rules/no-zod-native-enum.ts
5532
+ var import_utils44 = require("@typescript-eslint/utils");
5533
+ var ts2 = __toESM(require("typescript"), 1);
5534
+ var IGNORE_PATTERNS2 = [
5535
+ /[\\/]generated[\\/]/,
5536
+ /\.gen\.tsx?$/,
5537
+ /\.generated\.tsx?$/,
5538
+ /\.d\.ts$/
5539
+ ];
5540
+ function isIgnoredFile2(filename, sourceText) {
5541
+ if (IGNORE_PATTERNS2.some((re) => re.test(filename))) {
5542
+ return true;
5543
+ }
5544
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5545
+ }
5546
+ function isZodModule(source) {
5547
+ return /(^|[/@-])zod([/-]|$)/.test(source);
5548
+ }
5549
+ function unwrap3(node) {
5550
+ if (node.type === import_utils44.AST_NODE_TYPES.TSAsExpression || node.type === import_utils44.AST_NODE_TYPES.TSSatisfiesExpression) {
5551
+ return unwrap3(node.expression);
5552
+ }
5553
+ return node;
5554
+ }
5555
+ function stringValueTexts(node, sourceCode) {
5556
+ const texts = [];
5557
+ for (const prop of node.properties) {
5558
+ if (prop.type !== import_utils44.AST_NODE_TYPES.Property) {
5559
+ return null;
5560
+ }
5561
+ if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
5562
+ return null;
5563
+ }
5564
+ const value = prop.value;
5565
+ if (value.type !== import_utils44.AST_NODE_TYPES.Literal || typeof value.value !== "string") {
5566
+ return null;
5567
+ }
5568
+ const text = sourceCode.getText(value);
5569
+ if (!texts.includes(text)) {
5570
+ texts.push(text);
5571
+ }
5572
+ }
5573
+ return texts.length > 0 ? texts : null;
5574
+ }
5575
+ function resolvesToLocalEnum(node, scope) {
5576
+ let current = scope;
5577
+ while (current !== null) {
5578
+ const variable = current.variables.find((v) => v.name === node.name);
5579
+ if (variable !== void 0) {
5580
+ return variable.defs.some(
5581
+ (def) => def.node.type === import_utils44.AST_NODE_TYPES.TSEnumDeclaration
5582
+ );
5583
+ }
5584
+ current = current.upper;
5585
+ }
5586
+ return false;
5587
+ }
5588
+ var ENUM_SYMBOL_FLAGS = ts2.SymbolFlags.RegularEnum | ts2.SymbolFlags.ConstEnum | ts2.SymbolFlags.Enum;
5589
+ function resolvesToImportedEnum(node, services) {
5590
+ const checker = services.program.getTypeChecker();
5591
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
5592
+ let symbol = checker.getSymbolAtLocation(tsNode);
5593
+ if (symbol === void 0) {
5594
+ return false;
5595
+ }
5596
+ if ((symbol.flags & ts2.SymbolFlags.Alias) !== 0) {
5597
+ symbol = checker.getAliasedSymbol(symbol);
5598
+ }
5599
+ return (symbol.flags & ENUM_SYMBOL_FLAGS) !== 0;
5600
+ }
5601
+ var no_zod_native_enum_default = import_utils44.ESLintUtils.RuleCreator(
5602
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5603
+ )({
5604
+ name: "no-zod-native-enum",
5605
+ meta: {
5606
+ type: "suggestion",
5607
+ fixable: "code",
5608
+ docs: {
5609
+ description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
5610
+ },
5611
+ schema: [],
5612
+ messages: {
5613
+ 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>`.',
5614
+ enumOfTsEnum: '`z.enum()` is being passed the TypeScript enum `{{name}}`, which `no-enum` bans. Pass a string-literal array instead: `z.enum(["a", "b"])`.'
5615
+ }
5616
+ },
5617
+ defaultOptions: [],
5618
+ create(context) {
5619
+ const sourceCode = context.sourceCode;
5620
+ if (isIgnoredFile2(context.filename, sourceCode.getText())) {
5621
+ return {};
5622
+ }
5623
+ let services;
5624
+ try {
5625
+ services = import_utils44.ESLintUtils.getParserServices(context);
5626
+ } catch {
5627
+ services = null;
5628
+ }
5629
+ const zodImportedNames = /* @__PURE__ */ new Map();
5630
+ function isZodMemberCall(node, api) {
5631
+ const callee = node.callee;
5632
+ if (callee.type === import_utils44.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils44.AST_NODE_TYPES.Identifier) {
5633
+ return callee.property.name === api;
5634
+ }
5635
+ if (callee.type === import_utils44.AST_NODE_TYPES.Identifier) {
5636
+ return zodImportedNames.get(callee.name) === api;
5637
+ }
5638
+ return false;
5639
+ }
5640
+ function buildFix(node) {
5641
+ const callee = node.callee;
5642
+ if (callee.type !== import_utils44.AST_NODE_TYPES.MemberExpression || callee.property.type !== import_utils44.AST_NODE_TYPES.Identifier) {
5643
+ return null;
5644
+ }
5645
+ const arg = node.arguments[0];
5646
+ if (arg === void 0 || node.arguments.length !== 1 || arg.type === import_utils44.AST_NODE_TYPES.SpreadElement) {
5647
+ return null;
5648
+ }
5649
+ const inner = unwrap3(arg);
5650
+ if (inner.type !== import_utils44.AST_NODE_TYPES.ObjectExpression) {
5651
+ return null;
5652
+ }
5653
+ const values = stringValueTexts(inner, sourceCode);
5654
+ if (values === null) {
5655
+ return null;
5656
+ }
5657
+ const property = callee.property;
5658
+ const replacementArg = `[${values.join(", ")}]`;
5659
+ return (fixer) => [
5660
+ fixer.replaceText(property, "enum"),
5661
+ fixer.replaceText(arg, replacementArg)
5662
+ ];
5663
+ }
5664
+ return {
5665
+ ImportDeclaration(node) {
5666
+ if (!isZodModule(node.source.value)) {
5667
+ return;
5668
+ }
5669
+ for (const spec of node.specifiers) {
5670
+ if (spec.type === import_utils44.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === import_utils44.AST_NODE_TYPES.Identifier) {
5671
+ zodImportedNames.set(spec.local.name, spec.imported.name);
5672
+ }
5673
+ }
5674
+ },
5675
+ CallExpression(node) {
5676
+ if (isZodMemberCall(node, "nativeEnum")) {
5677
+ const fix = buildFix(node);
5678
+ context.report({
5679
+ node,
5680
+ messageId: "nativeEnum",
5681
+ ...fix === null ? {} : { fix }
5682
+ });
5683
+ return;
5684
+ }
5685
+ if (!isZodMemberCall(node, "enum")) {
5686
+ return;
5687
+ }
5688
+ const arg = node.arguments[0];
5689
+ if (arg === void 0 || arg.type !== import_utils44.AST_NODE_TYPES.Identifier) {
5690
+ return;
5691
+ }
5692
+ const isEnum = resolvesToLocalEnum(arg, sourceCode.getScope(arg)) || services !== null && resolvesToImportedEnum(arg, services);
5693
+ if (isEnum) {
5694
+ context.report({
5695
+ node,
5696
+ messageId: "enumOfTsEnum",
5697
+ data: { name: arg.name }
5698
+ });
5699
+ }
5700
+ }
5701
+ };
5702
+ }
5703
+ });
5704
+
5705
+ // src/rules/prefer-module-level-constant.ts
5706
+ var import_utils45 = require("@typescript-eslint/utils");
5707
+ var DEFAULT_MIN_ELEMENTS = 3;
5708
+ var MAX_LITERAL_DEPTH = 4;
5709
+ var IGNORE_PATTERNS3 = [
5710
+ /[\\/]generated[\\/]/,
5711
+ /\.gen\.tsx?$/,
5712
+ /\.generated\.tsx?$/,
5713
+ /\.d\.ts$/
5714
+ ];
5715
+ var TEST_FILE_PATTERNS = [
5716
+ /\.(?:test|spec)\.[cm]?[jt]sx?$/,
5717
+ /[\\/]__tests__[\\/]/,
5718
+ /[\\/]__mocks__[\\/]/,
5719
+ /[\\/]tests?[\\/]/,
5720
+ /\.stories\.[cm]?[jt]sx?$/
5721
+ ];
5722
+ var MUTATING_METHODS = /* @__PURE__ */ new Set([
5723
+ // Array
5724
+ "push",
5725
+ "pop",
5726
+ "shift",
5727
+ "unshift",
5728
+ "splice",
5729
+ "sort",
5730
+ "reverse",
5731
+ "fill",
5732
+ "copyWithin",
5733
+ // Set / Map
5734
+ "add",
5735
+ "set",
5736
+ "delete",
5737
+ "clear",
5738
+ // Object-ish escape hatches
5739
+ "assign"
5740
+ ]);
5741
+ var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([
5742
+ import_utils45.AST_NODE_TYPES.FunctionDeclaration,
5743
+ import_utils45.AST_NODE_TYPES.FunctionExpression,
5744
+ import_utils45.AST_NODE_TYPES.ArrowFunctionExpression
5745
+ ]);
5746
+ var COLLECTION_CONSTRUCTORS = /* @__PURE__ */ new Set(["Set", "Map"]);
5747
+ function isIgnoredFile3(filename, sourceText) {
5748
+ if (IGNORE_PATTERNS3.some((re) => re.test(filename))) {
5749
+ return true;
5750
+ }
5751
+ return /@generated\b/.test(sourceText.slice(0, 1024));
5752
+ }
5753
+ function isTestFile2(filename) {
5754
+ return TEST_FILE_PATTERNS.some((re) => re.test(filename));
5755
+ }
5756
+ function unwrap4(node) {
5757
+ 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) {
5758
+ return unwrap4(node.expression);
5759
+ }
5760
+ return node;
5761
+ }
5762
+ function isRegexLiteral(node) {
5763
+ return node.type === import_utils45.AST_NODE_TYPES.Literal && "regex" in node && node.regex !== void 0;
5764
+ }
5765
+ function isLiteralOnly(node, depth) {
5766
+ if (depth > MAX_LITERAL_DEPTH) {
5767
+ return false;
5768
+ }
5769
+ const inner = unwrap4(node);
5770
+ switch (inner.type) {
5771
+ case import_utils45.AST_NODE_TYPES.Literal: {
5772
+ return true;
5773
+ }
5774
+ case import_utils45.AST_NODE_TYPES.TemplateLiteral: {
5775
+ return inner.expressions.length === 0;
5776
+ }
5777
+ case import_utils45.AST_NODE_TYPES.UnaryExpression: {
5778
+ return (inner.operator === "-" || inner.operator === "+") && inner.argument.type === import_utils45.AST_NODE_TYPES.Literal && typeof inner.argument.value === "number";
5779
+ }
5780
+ case import_utils45.AST_NODE_TYPES.ArrayExpression: {
5781
+ return inner.elements.every(
5782
+ (el) => el !== null && el.type !== import_utils45.AST_NODE_TYPES.SpreadElement && isLiteralOnly(el, depth + 1)
5783
+ );
5784
+ }
5785
+ case import_utils45.AST_NODE_TYPES.ObjectExpression: {
5786
+ return inner.properties.every((prop) => {
5787
+ if (prop.type !== import_utils45.AST_NODE_TYPES.Property) {
5788
+ return false;
5789
+ }
5790
+ if (prop.shorthand || prop.method || prop.kind !== "init") {
5791
+ return false;
5792
+ }
5793
+ if (prop.computed && prop.key.type !== import_utils45.AST_NODE_TYPES.Literal) {
5794
+ return false;
5795
+ }
5796
+ return isLiteralOnly(prop.value, depth + 1);
5797
+ });
5798
+ }
5799
+ default: {
5800
+ return false;
5801
+ }
5802
+ }
5803
+ }
5804
+ function unwrapObjectFreeze(node) {
5805
+ const inner = unwrap4(node);
5806
+ 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) {
5807
+ return unwrap4(inner.arguments[0]);
5808
+ }
5809
+ return inner;
5810
+ }
5811
+ function classify(init, checkRegex) {
5812
+ const node = unwrapObjectFreeze(init);
5813
+ if (isRegexLiteral(node)) {
5814
+ if (!checkRegex) {
5815
+ return null;
5816
+ }
5817
+ if (/[gy]/.test(node.regex.flags)) {
5818
+ return null;
5819
+ }
5820
+ return { kind: "regex", size: 1 };
5821
+ }
5822
+ if (node.type === import_utils45.AST_NODE_TYPES.ArrayExpression) {
5823
+ return isLiteralOnly(node, 0) ? { kind: "array", size: node.elements.length } : null;
5824
+ }
5825
+ if (node.type === import_utils45.AST_NODE_TYPES.ObjectExpression) {
5826
+ return isLiteralOnly(node, 0) ? { kind: "object", size: node.properties.length } : null;
5827
+ }
5828
+ if (node.type === import_utils45.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils45.AST_NODE_TYPES.Identifier && COLLECTION_CONSTRUCTORS.has(node.callee.name)) {
5829
+ const arg = node.arguments[0];
5830
+ if (node.arguments.length !== 1 || arg === void 0 || arg.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
5831
+ return null;
5832
+ }
5833
+ const entries = unwrap4(arg);
5834
+ if (entries.type !== import_utils45.AST_NODE_TYPES.ArrayExpression) {
5835
+ return null;
5836
+ }
5837
+ return isLiteralOnly(entries, 0) ? { kind: node.callee.name === "Set" ? "Set" : "Map", size: entries.elements.length } : null;
5838
+ }
5839
+ return null;
5840
+ }
5841
+ function enclosingFunction2(node) {
5842
+ let current = node.parent;
5843
+ while (current !== void 0 && current !== null) {
5844
+ if (FUNCTION_TYPES3.has(current.type)) {
5845
+ return current;
5846
+ }
5847
+ current = current.parent;
5848
+ }
5849
+ return null;
5850
+ }
5851
+ var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
5852
+ [
5853
+ [
5854
+ "Object",
5855
+ /* @__PURE__ */ new Set(["keys", "values", "entries", "freeze", "fromEntries", "assign"])
5856
+ ],
5857
+ ["Array", /* @__PURE__ */ new Set(["from", "isArray"])],
5858
+ ["JSON", /* @__PURE__ */ new Set(["stringify"])]
5859
+ ]
5860
+ );
5861
+ function isNonRetainingBuiltinCall(node, argument) {
5862
+ const callee = node.callee;
5863
+ if (callee.type === import_utils45.AST_NODE_TYPES.Identifier && callee.name === "structuredClone") {
5864
+ return true;
5865
+ }
5866
+ 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) {
5867
+ return false;
5868
+ }
5869
+ const members = NON_RETAINING_BUILTINS.get(callee.object.name);
5870
+ if (members === void 0 || !members.has(callee.property.name)) {
5871
+ return false;
5872
+ }
5873
+ if (callee.object.name === "Object" && callee.property.name === "assign") {
5874
+ return node.arguments[0] !== argument;
5875
+ }
5876
+ return true;
5877
+ }
5878
+ function isSafeRead(identifier) {
5879
+ const parent = identifier.parent;
5880
+ if (parent.type === import_utils45.AST_NODE_TYPES.MemberExpression) {
5881
+ if (parent.object !== identifier) {
5882
+ return true;
5883
+ }
5884
+ const grandparent = parent.parent;
5885
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
5886
+ return false;
5887
+ }
5888
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.UpdateExpression) {
5889
+ return false;
5890
+ }
5891
+ if (grandparent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
5892
+ return false;
5893
+ }
5894
+ 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) {
5895
+ return false;
5896
+ }
5897
+ return true;
5898
+ }
5899
+ if (parent.type === import_utils45.AST_NODE_TYPES.ForOfStatement && parent.right === identifier) {
5900
+ return true;
5901
+ }
5902
+ if (parent.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
5903
+ return true;
5904
+ }
5905
+ if (parent.type === import_utils45.AST_NODE_TYPES.BinaryExpression) {
5906
+ return true;
5907
+ }
5908
+ if (parent.type === import_utils45.AST_NODE_TYPES.CallExpression && parent.arguments.includes(identifier) && isNonRetainingBuiltinCall(parent, identifier)) {
5909
+ return true;
5910
+ }
5911
+ if (parent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && parent.operator !== "delete") {
5912
+ return true;
5913
+ }
5914
+ return false;
5915
+ }
5916
+ var prefer_module_level_constant_default = import_utils45.ESLintUtils.RuleCreator(
5917
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
5918
+ )({
5919
+ name: "prefer-module-level-constant",
5920
+ meta: {
5921
+ type: "suggestion",
5922
+ docs: {
5923
+ description: "Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once."
5924
+ },
5925
+ schema: [
5926
+ {
5927
+ type: "object",
5928
+ additionalProperties: false,
5929
+ properties: {
5930
+ minElements: { type: "number", minimum: 1 },
5931
+ checkRegex: { type: "boolean" },
5932
+ ignoreTestFiles: { type: "boolean" }
5933
+ }
5934
+ }
5935
+ ],
5936
+ messages: {
5937
+ 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.",
5938
+ hoistRegex: "`{{name}}` is a constant regex recompiled on every call. Hoist it to module scope."
5939
+ }
5940
+ },
5941
+ defaultOptions: [{}],
5942
+ create(context, [optionsArg]) {
5943
+ const options = optionsArg ?? {};
5944
+ const minElements = options.minElements ?? DEFAULT_MIN_ELEMENTS;
5945
+ const checkRegex = options.checkRegex ?? true;
5946
+ const ignoreTestFiles = options.ignoreTestFiles ?? true;
5947
+ const sourceCode = context.sourceCode;
5948
+ const filename = context.filename;
5949
+ if (isIgnoredFile3(filename, sourceCode.getText())) {
5950
+ return {};
5951
+ }
5952
+ if (ignoreTestFiles && isTestFile2(filename)) {
5953
+ return {};
5954
+ }
5955
+ function allReferencesAreSafeReads(declarator) {
5956
+ const variables = sourceCode.getDeclaredVariables(declarator);
5957
+ const variable = variables[0];
5958
+ if (variable === void 0) {
5959
+ return false;
5960
+ }
5961
+ for (const reference of variable.references) {
5962
+ if (reference.init === true) {
5963
+ continue;
5964
+ }
5965
+ if (reference.isWrite()) {
5966
+ return false;
5967
+ }
5968
+ if (reference.identifier.type !== import_utils45.AST_NODE_TYPES.Identifier) {
5969
+ return false;
5970
+ }
5971
+ if (!isSafeRead(reference.identifier)) {
5972
+ return false;
5973
+ }
5974
+ }
5975
+ return true;
5976
+ }
5977
+ return {
5978
+ VariableDeclarator(node) {
5979
+ const declaration = node.parent;
5980
+ if (declaration.type !== import_utils45.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const" || declaration.declare === true) {
5981
+ return;
5982
+ }
5983
+ if (node.id.type !== import_utils45.AST_NODE_TYPES.Identifier || node.init === null) {
5984
+ return;
5985
+ }
5986
+ if (enclosingFunction2(node) === null) {
5987
+ return;
5988
+ }
5989
+ const candidate = classify(node.init, checkRegex);
5990
+ if (candidate === null) {
5991
+ return;
5992
+ }
5993
+ if (candidate.kind !== "regex" && candidate.size < minElements) {
5994
+ return;
5995
+ }
5996
+ if (!allReferencesAreSafeReads(node)) {
5997
+ return;
5998
+ }
5999
+ context.report({
6000
+ node: node.id,
6001
+ messageId: candidate.kind === "regex" ? "hoistRegex" : "hoistCollection",
6002
+ data: { name: node.id.name, kind: candidate.kind }
6003
+ });
6004
+ }
6005
+ };
6006
+ }
6007
+ });
6008
+
5325
6009
  // src/index.ts
5326
6010
  var rules = {
5327
6011
  "enforce-file-structure": enforce_file_structure_default,
@@ -5362,12 +6046,14 @@ var rules = {
5362
6046
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
5363
6047
  "no-dynamic-sql": no_dynamic_sql_default,
5364
6048
  "no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
5365
- "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
6049
+ "no-storage-in-stateless-modules": no_storage_in_stateless_modules_default,
6050
+ "no-zod-native-enum": no_zod_native_enum_default,
6051
+ "prefer-module-level-constant": prefer_module_level_constant_default
5366
6052
  };
5367
6053
  var plugin = {
5368
6054
  meta: {
5369
6055
  name: "@sarj/eslint-plugin",
5370
- version: "2.8.0"
6056
+ version: "2.10.0"
5371
6057
  },
5372
6058
  rules,
5373
6059
  configs: {
@@ -5416,7 +6102,14 @@ var plugin = {
5416
6102
  "@sarj/no-repeated-string-literal": "warn",
5417
6103
  "@sarj/no-positional-tuple-return": "warn",
5418
6104
  // Injection guard — low FP, applies to any repo touching SQL.
5419
- "@sarj/no-dynamic-sql": "warn"
6105
+ "@sarj/no-dynamic-sql": "warn",
6106
+ // Mined from two years of PR review (SARJ-928). Schema-layer sibling of
6107
+ // `no-enum`; autofixable for inline string-literal objects.
6108
+ "@sarj/no-zod-native-enum": "warn",
6109
+ // Mined from two years of PR review — the single most frequent uncovered
6110
+ // theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
6111
+ // positives, so it is safe to run everywhere.
6112
+ "@sarj/prefer-module-level-constant": "warn"
5420
6113
  }
5421
6114
  },
5422
6115
  strict: {
@@ -5476,7 +6169,10 @@ var plugin = {
5476
6169
  // and takes an `allow` list for repos that lay their client layer out
5477
6170
  // differently.
5478
6171
  "@sarj/no-raw-fetch-outside-clients": "error",
5479
- "@sarj/no-storage-in-stateless-modules": "error"
6172
+ "@sarj/no-storage-in-stateless-modules": "error",
6173
+ // Mined from two years of PR review (SARJ-928).
6174
+ "@sarj/no-zod-native-enum": "error",
6175
+ "@sarj/prefer-module-level-constant": "error"
5480
6176
  }
5481
6177
  }
5482
6178
  }