@noctcore/eslint-plugin-contracts 0.2.0 → 0.4.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
@@ -45,13 +45,16 @@ var recommended = {
45
45
  "noctcore-contracts/money-must-be-decimal": "error",
46
46
  "noctcore-contracts/require-error-cause": "error",
47
47
  "noctcore-contracts/restrict-throw-to-taxonomy": "error",
48
- // Config-required / heuristic rules ship inert. `require-registered-keys` and
49
- // `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
50
- // set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
48
+ "noctcore-contracts/schema-enum-field-consistency": "error",
49
+ "noctcore-contracts/fetch-must-check-ok": "error",
50
+ // Config-required / heuristic rules ship inert. `require-registered-keys`,
51
+ // `env-var-schema-parity` and `translation-key-exists` do nothing until their
52
+ // `sinks` / `schema` / `catalogs` options are set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
51
53
  // type-aware concern. Enable them explicitly once configured for your project.
52
54
  "noctcore-contracts/require-registered-keys": "off",
53
55
  "noctcore-contracts/env-var-schema-parity": "off",
54
- "noctcore-contracts/require-schema-parse-at-boundary": "off"
56
+ "noctcore-contracts/require-schema-parse-at-boundary": "off",
57
+ "noctcore-contracts/translation-key-exists": "off"
55
58
  };
56
59
 
57
60
  // src/rules/env-var-schema-parity.ts
@@ -151,9 +154,476 @@ var envVarSchemaParityRule = createRule({
151
154
  }
152
155
  });
153
156
 
154
- // src/rules/money-must-be-decimal.ts
157
+ // src/rules/fetch-must-check-ok.ts
155
158
  var import_utils2 = require("@typescript-eslint/utils");
156
- var RULE_NAME2 = "money-must-be-decimal";
159
+ var RULE_NAME2 = "fetch-must-check-ok";
160
+ var optionSchema2 = {
161
+ type: "object",
162
+ additionalProperties: false,
163
+ properties: {
164
+ fetchFunctions: {
165
+ type: "array",
166
+ items: { type: "string", minLength: 1 },
167
+ uniqueItems: true
168
+ }
169
+ }
170
+ };
171
+ function isNode(value) {
172
+ return typeof value === "object" && value !== null && "type" in value;
173
+ }
174
+ function walkSome(root, keys, predicate) {
175
+ const stack = [root];
176
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
177
+ if (predicate(node)) {
178
+ return true;
179
+ }
180
+ for (const key of keys[node.type] ?? []) {
181
+ const value = Reflect.get(node, key);
182
+ if (Array.isArray(value)) {
183
+ for (const child of value) {
184
+ if (isNode(child)) {
185
+ stack.push(child);
186
+ }
187
+ }
188
+ } else if (isNode(value)) {
189
+ stack.push(value);
190
+ }
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+ function calleePath(node) {
196
+ if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
197
+ return node.name;
198
+ }
199
+ if (node.type === import_utils2.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
200
+ const object = calleePath(node.object);
201
+ return object === null ? null : `${object}.${node.property.name}`;
202
+ }
203
+ return null;
204
+ }
205
+ var OK_PROP = "ok";
206
+ var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
207
+ var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
208
+ var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
209
+ var FIRST_ERROR_STATUS = 400;
210
+ function literalValue(node) {
211
+ if (node.type !== import_utils2.AST_NODE_TYPES.Literal) {
212
+ return void 0;
213
+ }
214
+ return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
215
+ }
216
+ function mirror(operator) {
217
+ switch (operator) {
218
+ case "<":
219
+ return ">";
220
+ case "<=":
221
+ return ">=";
222
+ case ">":
223
+ return "<";
224
+ case ">=":
225
+ return "<=";
226
+ default:
227
+ return operator;
228
+ }
229
+ }
230
+ function booleanPolarity(operator, value) {
231
+ if (operator === "===" || operator === "==") {
232
+ return value ? "positive" : "negative";
233
+ }
234
+ if (operator === "!==" || operator === "!=") {
235
+ return value ? "negative" : "positive";
236
+ }
237
+ return "opaque";
238
+ }
239
+ function statusPolarity(operator, value) {
240
+ const isSuccessCode = value >= 200 && value < 300;
241
+ switch (operator) {
242
+ case "===":
243
+ case "==":
244
+ return isSuccessCode ? "positive" : "opaque";
245
+ case "!==":
246
+ case "!=":
247
+ return isSuccessCode ? "negative" : "opaque";
248
+ case "<":
249
+ return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
250
+ case "<=":
251
+ return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
252
+ // A failure test is only useful for what it says about the OTHER side, so
253
+ // what matters is that everything below the threshold is a success:
254
+ // `>= 300` and `>= 400` both leave only good responses behind, while
255
+ // `>= 500` leaves every 4xx there.
256
+ case ">=":
257
+ return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
258
+ case ">":
259
+ return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
260
+ default:
261
+ return "opaque";
262
+ }
263
+ }
264
+ function comparisonPolarity(node, readIsLeft) {
265
+ const value = literalValue(readIsLeft ? node.right : node.left);
266
+ const operator = readIsLeft ? node.operator : mirror(node.operator);
267
+ if (typeof value === "boolean") {
268
+ return booleanPolarity(operator, value);
269
+ }
270
+ return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
271
+ }
272
+ function propReadOn(node, objectName, props) {
273
+ if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed) {
274
+ return false;
275
+ }
276
+ if (node.object.type !== import_utils2.AST_NODE_TYPES.Identifier || node.object.name !== objectName || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier) {
277
+ return false;
278
+ }
279
+ const name = node.property.name;
280
+ return typeof props === "string" ? name === props : props.has(name);
281
+ }
282
+ function findJsonReads(root, keys, name) {
283
+ const reads = [];
284
+ walkSome(root, keys, (node) => {
285
+ if (propReadOn(node, name, "json")) {
286
+ reads.push(node);
287
+ }
288
+ return false;
289
+ });
290
+ return reads;
291
+ }
292
+ function isAssertionName(node) {
293
+ return node.type === import_utils2.AST_NODE_TYPES.Identifier && ASSERTION_NAMES.test(node.name);
294
+ }
295
+ function isAssertionCallee(callee) {
296
+ if (callee.type === import_utils2.AST_NODE_TYPES.Identifier) {
297
+ return isAssertionName(callee);
298
+ }
299
+ return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
300
+ }
301
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
302
+ import_utils2.AST_NODE_TYPES.IfStatement,
303
+ import_utils2.AST_NODE_TYPES.WhileStatement,
304
+ import_utils2.AST_NODE_TYPES.DoWhileStatement,
305
+ import_utils2.AST_NODE_TYPES.ConditionalExpression,
306
+ import_utils2.AST_NODE_TYPES.SwitchStatement,
307
+ import_utils2.AST_NODE_TYPES.CallExpression
308
+ ]);
309
+ var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
310
+ ["equal", "==="],
311
+ ["equals", "==="],
312
+ ["strictEqual", "==="],
313
+ ["deepEqual", "==="],
314
+ ["deepStrictEqual", "==="],
315
+ ["toBe", "==="],
316
+ ["toEqual", "==="],
317
+ ["notEqual", "!=="],
318
+ ["notStrictEqual", "!=="],
319
+ ["notDeepEqual", "!=="]
320
+ ]);
321
+ function assertionOperator(callee) {
322
+ return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils2.AST_NODE_TYPES.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
323
+ }
324
+ function assertionCheck(parent, child, check) {
325
+ if (check.compared) {
326
+ return check;
327
+ }
328
+ const operator = assertionOperator(parent.callee);
329
+ if (operator === void 0) {
330
+ return check;
331
+ }
332
+ const other = parent.arguments.find((arg) => arg !== child);
333
+ const value = other === void 0 ? void 0 : literalValue(other);
334
+ if (typeof value === "boolean") {
335
+ return {
336
+ ...check,
337
+ compared: true,
338
+ polarity: booleanPolarity(operator, value)
339
+ };
340
+ }
341
+ return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
342
+ }
343
+ function terminalCheck(parent, child, state) {
344
+ const check = { owner: parent, ...state };
345
+ switch (parent.type) {
346
+ case import_utils2.AST_NODE_TYPES.IfStatement:
347
+ case import_utils2.AST_NODE_TYPES.WhileStatement:
348
+ case import_utils2.AST_NODE_TYPES.DoWhileStatement:
349
+ case import_utils2.AST_NODE_TYPES.ConditionalExpression:
350
+ return parent.test === child ? check : null;
351
+ case import_utils2.AST_NODE_TYPES.SwitchStatement:
352
+ return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
353
+ // An assertion settles a status only when it COMPARES it.
354
+ // `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
355
+ // number is truthy, which every response that arrived satisfies. Arity
356
+ // cannot tell them apart (`assert.ok(res.status, 'message')` also has two
357
+ // arguments), so the comparison is read from the assertion's own name.
358
+ case import_utils2.AST_NODE_TYPES.CallExpression:
359
+ return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
360
+ default:
361
+ return null;
362
+ }
363
+ }
364
+ function combinatorStep(parent, child, state, parse) {
365
+ switch (parent.type) {
366
+ case import_utils2.AST_NODE_TYPES.UnaryExpression:
367
+ if (parent.operator === "typeof") {
368
+ return "stop";
369
+ }
370
+ if (parent.operator === "!") {
371
+ state.polarity = state.polarity === "positive" ? "negative" : "positive";
372
+ }
373
+ return "continue";
374
+ case import_utils2.AST_NODE_TYPES.BinaryExpression: {
375
+ if (!COMPARISONS.has(parent.operator)) {
376
+ return "stop";
377
+ }
378
+ const polarity = comparisonPolarity(parent, parent.left === child);
379
+ state.compared = true;
380
+ state.polarity = polarity;
381
+ return polarity === "opaque" ? "stop" : "continue";
382
+ }
383
+ case import_utils2.AST_NODE_TYPES.LogicalExpression:
384
+ if (parent.operator === "&&") {
385
+ state.underAnd = true;
386
+ }
387
+ if (parent.operator === "||") {
388
+ state.underOr = true;
389
+ }
390
+ return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
391
+ case import_utils2.AST_NODE_TYPES.ChainExpression:
392
+ case import_utils2.AST_NODE_TYPES.TSNonNullExpression:
393
+ return "continue";
394
+ default:
395
+ return "stop";
396
+ }
397
+ }
398
+ function climb(read, parse) {
399
+ const state = {
400
+ polarity: "positive",
401
+ compared: false,
402
+ underAnd: false,
403
+ underOr: false
404
+ };
405
+ let child = read;
406
+ let parent = read.parent;
407
+ while (parent !== void 0) {
408
+ if (TERMINAL_TYPES.has(parent.type)) {
409
+ return terminalCheck(parent, child, state);
410
+ }
411
+ const step = combinatorStep(parent, child, state, parse);
412
+ if (step === "stop") {
413
+ return null;
414
+ }
415
+ if (step !== "continue") {
416
+ return step;
417
+ }
418
+ child = parent;
419
+ parent = parent.parent;
420
+ }
421
+ return null;
422
+ }
423
+ function contains(outer, inner) {
424
+ return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
425
+ }
426
+ function alwaysExits(node) {
427
+ if (node.type === import_utils2.AST_NODE_TYPES.ReturnStatement || node.type === import_utils2.AST_NODE_TYPES.ThrowStatement) {
428
+ return true;
429
+ }
430
+ return node.type === import_utils2.AST_NODE_TYPES.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
431
+ }
432
+ function scopeOfCheck(node) {
433
+ let current = node;
434
+ while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
435
+ current = current.parent;
436
+ }
437
+ return current.parent ?? current;
438
+ }
439
+ function isSuccessCase(arm) {
440
+ if (arm.test === null) {
441
+ return false;
442
+ }
443
+ const value = literalValue(arm.test);
444
+ return typeof value === "number" && value >= 200 && value < 300;
445
+ }
446
+ function switchArmProtects(owner, parse) {
447
+ const index = owner.cases.findIndex((arm) => contains(arm, parse));
448
+ const own = owner.cases[index];
449
+ if (own === void 0 || !isSuccessCase(own)) {
450
+ return false;
451
+ }
452
+ for (let i = index - 1; i >= 0; i -= 1) {
453
+ const arm = owner.cases[i];
454
+ if (arm === void 0 || arm.consequent.length > 0) {
455
+ break;
456
+ }
457
+ if (!isSuccessCase(arm)) {
458
+ return false;
459
+ }
460
+ }
461
+ return true;
462
+ }
463
+ function branchProtects(check, parse) {
464
+ const { owner } = check;
465
+ if (owner.type === import_utils2.AST_NODE_TYPES.SwitchStatement) {
466
+ return switchArmProtects(owner, parse);
467
+ }
468
+ if (owner.type === import_utils2.AST_NODE_TYPES.LogicalExpression) {
469
+ if (!contains(owner.right, parse)) {
470
+ return false;
471
+ }
472
+ if (owner.operator === "&&") {
473
+ return check.polarity === "positive";
474
+ }
475
+ return owner.operator === "||" && check.polarity === "negative";
476
+ }
477
+ if (owner.type === import_utils2.AST_NODE_TYPES.WhileStatement || owner.type === import_utils2.AST_NODE_TYPES.DoWhileStatement) {
478
+ return contains(owner.body, parse) && entersOnSuccess(check);
479
+ }
480
+ if (owner.type !== import_utils2.AST_NODE_TYPES.ConditionalExpression && owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
481
+ return false;
482
+ }
483
+ if (contains(owner.consequent, parse)) {
484
+ return entersOnSuccess(check);
485
+ }
486
+ return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
487
+ }
488
+ function entersOnSuccess(check) {
489
+ return check.polarity === "positive" && !check.underOr;
490
+ }
491
+ function skipsOnSuccess(check) {
492
+ return check.polarity === "negative" && !check.underAnd;
493
+ }
494
+ function guardProtects(check, parse) {
495
+ const { owner } = check;
496
+ if (owner.type === import_utils2.AST_NODE_TYPES.CallExpression) {
497
+ return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
498
+ }
499
+ if (owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
500
+ return false;
501
+ }
502
+ if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
503
+ return false;
504
+ }
505
+ if (alwaysExits(owner.consequent)) {
506
+ return skipsOnSuccess(check);
507
+ }
508
+ return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
509
+ }
510
+ function protects(check, parse) {
511
+ if (!check.compared) {
512
+ return false;
513
+ }
514
+ return branchProtects(check, parse) || guardProtects(check, parse);
515
+ }
516
+ function statusAliases(root, keys, name) {
517
+ const aliases = /* @__PURE__ */ new Map();
518
+ walkSome(root, keys, (node) => {
519
+ if (node.type === import_utils2.AST_NODE_TYPES.VariableDeclarator && node.id.type === import_utils2.AST_NODE_TYPES.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
520
+ aliases.set(node.id.name, node.init.property.name);
521
+ }
522
+ return false;
523
+ });
524
+ return aliases;
525
+ }
526
+ function checkedProp(node, name, aliases) {
527
+ if (propReadOn(node, name, OK_PROPS)) {
528
+ return node.property.type === import_utils2.AST_NODE_TYPES.Identifier ? node.property.name : void 0;
529
+ }
530
+ return node.type === import_utils2.AST_NODE_TYPES.Identifier ? aliases.get(node.name) : void 0;
531
+ }
532
+ function isProtected(root, keys, name, aliases, parse) {
533
+ return walkSome(root, keys, (node) => {
534
+ const prop = checkedProp(node, name, aliases);
535
+ if (prop === void 0) {
536
+ return false;
537
+ }
538
+ const check = climb(node, parse);
539
+ if (check === null) {
540
+ return false;
541
+ }
542
+ return protects(
543
+ prop === OK_PROP ? { ...check, compared: true } : check,
544
+ parse
545
+ );
546
+ });
547
+ }
548
+ function scopeOf(node) {
549
+ let current = node.parent;
550
+ while (current !== void 0) {
551
+ if (current.type === import_utils2.AST_NODE_TYPES.BlockStatement || current.type === import_utils2.AST_NODE_TYPES.Program) {
552
+ return current;
553
+ }
554
+ current = current.parent;
555
+ }
556
+ return node;
557
+ }
558
+ function skipAwait(node) {
559
+ return node?.type === import_utils2.AST_NODE_TYPES.AwaitExpression ? node.parent : node;
560
+ }
561
+ function thenCallbackParam(node) {
562
+ if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier || node.property.name !== "then" || node.parent.type !== import_utils2.AST_NODE_TYPES.CallExpression) {
563
+ return null;
564
+ }
565
+ const callback = node.parent.arguments[0];
566
+ if (callback === void 0 || callback.type !== import_utils2.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils2.AST_NODE_TYPES.FunctionExpression) {
567
+ return null;
568
+ }
569
+ const param = callback.params[0];
570
+ return param?.type === import_utils2.AST_NODE_TYPES.Identifier ? { name: param.name, body: callback.body } : null;
571
+ }
572
+ var fetchMustCheckOkRule = createRule({
573
+ name: RULE_NAME2,
574
+ meta: {
575
+ type: "problem",
576
+ docs: {
577
+ description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
578
+ },
579
+ schema: [optionSchema2],
580
+ messages: {
581
+ missingOkCheck: "`fetch` resolves on 4xx/5xx too, so `.json()` here can parse an error body as data. Check `response.ok` (or compare the status) and leave early before reading the body."
582
+ }
583
+ },
584
+ defaultOptions: [{ fetchFunctions: ["fetch"] }],
585
+ create(context, [options]) {
586
+ const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
587
+ const keys = context.sourceCode.visitorKeys;
588
+ function reportUnprotected(root, name) {
589
+ const aliases = statusAliases(root, keys, name);
590
+ for (const read of findJsonReads(root, keys, name)) {
591
+ if (!isProtected(root, keys, name, aliases, read)) {
592
+ context.report({ node: read, messageId: "missingOkCheck" });
593
+ }
594
+ }
595
+ }
596
+ return {
597
+ CallExpression(node) {
598
+ const path3 = calleePath(node.callee);
599
+ if (path3 === null || !fetchFunctions.has(path3)) {
600
+ return;
601
+ }
602
+ const parent = skipAwait(node.parent);
603
+ if (parent === void 0) {
604
+ return;
605
+ }
606
+ if (parent.type === import_utils2.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property.type === import_utils2.AST_NODE_TYPES.Identifier && parent.property.name === "json") {
607
+ context.report({ node: parent, messageId: "missingOkCheck" });
608
+ return;
609
+ }
610
+ const callback = thenCallbackParam(parent);
611
+ if (callback !== null) {
612
+ reportUnprotected(callback.body, callback.name);
613
+ return;
614
+ }
615
+ if (parent.type !== import_utils2.AST_NODE_TYPES.VariableDeclarator || parent.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
616
+ return;
617
+ }
618
+ reportUnprotected(scopeOf(parent), parent.id.name);
619
+ }
620
+ };
621
+ }
622
+ });
623
+
624
+ // src/rules/money-must-be-decimal.ts
625
+ var import_utils3 = require("@typescript-eslint/utils");
626
+ var RULE_NAME3 = "money-must-be-decimal";
157
627
  var DEFAULT_DECIMAL_TYPE = "Decimal";
158
628
  var DEFAULT_FIELD_PATTERNS = [
159
629
  "amount",
@@ -163,7 +633,7 @@ var DEFAULT_FIELD_PATTERNS = [
163
633
  "balance"
164
634
  ];
165
635
  var DEFAULT_ALLOWED_FILES = [];
166
- var optionSchema2 = {
636
+ var optionSchema3 = {
167
637
  type: "object",
168
638
  additionalProperties: false,
169
639
  properties: {
@@ -192,22 +662,22 @@ function isAllowedFile(filename, patterns) {
192
662
  return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
193
663
  }
194
664
  function staticName(node) {
195
- if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
665
+ if (node.type === import_utils3.AST_NODE_TYPES.Identifier) {
196
666
  return node.name;
197
667
  }
198
668
  return void 0;
199
669
  }
200
670
  function isNumberAnnotation(annotation) {
201
- return annotation?.typeAnnotation.type === import_utils2.AST_NODE_TYPES.TSNumberKeyword;
671
+ return annotation?.typeAnnotation.type === import_utils3.AST_NODE_TYPES.TSNumberKeyword;
202
672
  }
203
673
  var moneyMustBeDecimalRule = createRule({
204
- name: RULE_NAME2,
674
+ name: RULE_NAME3,
205
675
  meta: {
206
676
  type: "problem",
207
677
  docs: {
208
678
  description: "Disallow monetary values typed as the JS primitive `number`. Money-named fields explicitly typed `: number` lose precision to float rounding; use a Decimal money type instead."
209
679
  },
210
- schema: [optionSchema2],
680
+ schema: [optionSchema3],
211
681
  messages: {
212
682
  moneyMustBeDecimal: "Monetary values must use {{decimalType}}, never the JS `number` primitive, to avoid float rounding errors. Rename or retype this field to a {{decimalType}} money type."
213
683
  }
@@ -243,7 +713,7 @@ var moneyMustBeDecimalRule = createRule({
243
713
  },
244
714
  // `const total: number = ...`: annotated variable declarator.
245
715
  VariableDeclarator(node) {
246
- if (node.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
716
+ if (node.id.type !== import_utils3.AST_NODE_TYPES.Identifier) {
247
717
  return;
248
718
  }
249
719
  const name = node.id.name;
@@ -256,15 +726,15 @@ var moneyMustBeDecimalRule = createRule({
256
726
  });
257
727
 
258
728
  // src/rules/no-direct-process-env.ts
259
- var import_utils3 = require("@typescript-eslint/utils");
260
- var RULE_NAME3 = "no-direct-process-env";
729
+ var import_utils4 = require("@typescript-eslint/utils");
730
+ var RULE_NAME4 = "no-direct-process-env";
261
731
  var DEFAULT_CONFIG_MODULE = "@/config";
262
732
  var DEFAULT_ALLOWED_FILES2 = [
263
733
  "**/*.config.{ts,js,mjs,cjs}",
264
734
  "**/*.{spec,test}.{ts,tsx}",
265
735
  "**/scripts/**"
266
736
  ];
267
- var optionSchema3 = {
737
+ var optionSchema4 = {
268
738
  type: "object",
269
739
  additionalProperties: false,
270
740
  properties: {
@@ -319,22 +789,22 @@ function isAllowedFile2(filename, patterns) {
319
789
  return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
320
790
  }
321
791
  function isProcessEnv2(node) {
322
- if (node.type !== import_utils3.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils3.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
792
+ if (node.type !== import_utils4.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils4.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
323
793
  return false;
324
794
  }
325
795
  if (node.computed) {
326
- return node.property.type === import_utils3.AST_NODE_TYPES.Literal && node.property.value === "env";
796
+ return node.property.type === import_utils4.AST_NODE_TYPES.Literal && node.property.value === "env";
327
797
  }
328
- return node.property.type === import_utils3.AST_NODE_TYPES.Identifier && node.property.name === "env";
798
+ return node.property.type === import_utils4.AST_NODE_TYPES.Identifier && node.property.name === "env";
329
799
  }
330
800
  var noDirectProcessEnvRule = createRule({
331
- name: RULE_NAME3,
801
+ name: RULE_NAME4,
332
802
  meta: {
333
803
  type: "problem",
334
804
  docs: {
335
805
  description: "Disallow direct `process.env` access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use."
336
806
  },
337
- schema: [optionSchema3],
807
+ schema: [optionSchema4],
338
808
  messages: {
339
809
  directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
340
810
  }
@@ -369,10 +839,10 @@ var noDirectProcessEnvRule = createRule({
369
839
  });
370
840
 
371
841
  // src/rules/no-error-stringify.ts
372
- var import_utils4 = require("@typescript-eslint/utils");
373
- var RULE_NAME4 = "no-error-stringify";
842
+ var import_utils5 = require("@typescript-eslint/utils");
843
+ var RULE_NAME5 = "no-error-stringify";
374
844
  var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
375
- var optionSchema4 = {
845
+ var optionSchema5 = {
376
846
  type: "object",
377
847
  additionalProperties: false,
378
848
  properties: {
@@ -385,19 +855,19 @@ var optionSchema4 = {
385
855
  }
386
856
  };
387
857
  function isEmptyStringLiteral(node) {
388
- return node.type === import_utils4.AST_NODE_TYPES.Literal && node.value === "";
858
+ return node.type === import_utils5.AST_NODE_TYPES.Literal && node.value === "";
389
859
  }
390
860
  function isErrorIdentifier(node, names) {
391
- return node.type === import_utils4.AST_NODE_TYPES.Identifier && names.has(node.name);
861
+ return node.type === import_utils5.AST_NODE_TYPES.Identifier && names.has(node.name);
392
862
  }
393
863
  var noErrorStringifyRule = createRule({
394
- name: RULE_NAME4,
864
+ name: RULE_NAME5,
395
865
  meta: {
396
866
  type: "problem",
397
867
  docs: {
398
868
  description: 'Disallow stringifying an error with bare `${error}` interpolation, `error.toString()`, or `error + ""`. These drop the cause chain. Use `error instanceof Error ? error.message : String(error)` instead.'
399
869
  },
400
- schema: [optionSchema4],
870
+ schema: [optionSchema5],
401
871
  messages: {
402
872
  noErrorStringify: "Stringifying an error this way drops its cause chain. Use `{{name}} instanceof Error ? {{name}}.message : String({{name}})` (or pass the Error object straight to the logger)."
403
873
  }
@@ -412,7 +882,7 @@ var noErrorStringifyRule = createRule({
412
882
  // `error.toString()`
413
883
  'CallExpression[callee.type="MemberExpression"]'(node) {
414
884
  const callee = node.callee;
415
- if (!callee.computed && callee.property.type === import_utils4.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
885
+ if (!callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
416
886
  report(node, callee.object.name);
417
887
  }
418
888
  },
@@ -445,14 +915,14 @@ var noErrorStringifyRule = createRule({
445
915
  });
446
916
 
447
917
  // src/rules/require-error-cause.ts
448
- var import_utils5 = require("@typescript-eslint/utils");
449
- var RULE_NAME5 = "require-error-cause";
918
+ var import_utils6 = require("@typescript-eslint/utils");
919
+ var RULE_NAME6 = "require-error-cause";
450
920
  function constructorSimpleName(node) {
451
921
  const callee = node.callee;
452
- if (callee.type === import_utils5.AST_NODE_TYPES.Identifier) {
922
+ if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
453
923
  return callee.name;
454
924
  }
455
- if (callee.type === import_utils5.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier) {
925
+ if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils6.AST_NODE_TYPES.Identifier) {
456
926
  return callee.property.name;
457
927
  }
458
928
  return null;
@@ -462,16 +932,16 @@ function isErrorLikeName(name) {
462
932
  }
463
933
  function alreadyHasCause(node) {
464
934
  for (const arg of node.arguments) {
465
- if (arg.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
935
+ if (arg.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
466
936
  return true;
467
937
  }
468
- if (arg.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
938
+ if (arg.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
469
939
  for (const prop of arg.properties) {
470
- if (prop.type === import_utils5.AST_NODE_TYPES.SpreadElement) {
940
+ if (prop.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
471
941
  return true;
472
942
  }
473
943
  const key = prop.key;
474
- const isCause = key.type === import_utils5.AST_NODE_TYPES.Identifier && key.name === "cause" || key.type === import_utils5.AST_NODE_TYPES.Literal && key.value === "cause";
944
+ const isCause = key.type === import_utils6.AST_NODE_TYPES.Identifier && key.name === "cause" || key.type === import_utils6.AST_NODE_TYPES.Literal && key.value === "cause";
475
945
  if (isCause) {
476
946
  return true;
477
947
  }
@@ -489,7 +959,7 @@ function buildFix(node, binding) {
489
959
  if (last === void 0) {
490
960
  return null;
491
961
  }
492
- if (last.type === import_utils5.AST_NODE_TYPES.ObjectExpression) {
962
+ if (last.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
493
963
  const props = last.properties;
494
964
  if (props.length === 0) {
495
965
  return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
@@ -503,7 +973,7 @@ function buildFix(node, binding) {
503
973
  return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
504
974
  }
505
975
  var requireErrorCauseRule = createRule({
506
- name: RULE_NAME5,
976
+ name: RULE_NAME6,
507
977
  meta: {
508
978
  type: "problem",
509
979
  docs: {
@@ -522,7 +992,7 @@ var requireErrorCauseRule = createRule({
522
992
  CatchClause(node) {
523
993
  const param = node.param;
524
994
  catchBindings.push(
525
- param && param.type === import_utils5.AST_NODE_TYPES.Identifier ? param.name : null
995
+ param && param.type === import_utils6.AST_NODE_TYPES.Identifier ? param.name : null
526
996
  );
527
997
  },
528
998
  "CatchClause:exit"() {
@@ -534,7 +1004,7 @@ var requireErrorCauseRule = createRule({
534
1004
  return;
535
1005
  }
536
1006
  const arg = node.argument;
537
- if (arg.type !== import_utils5.AST_NODE_TYPES.NewExpression) {
1007
+ if (arg.type !== import_utils6.AST_NODE_TYPES.NewExpression) {
538
1008
  return;
539
1009
  }
540
1010
  const ctor = constructorSimpleName(arg);
@@ -557,9 +1027,9 @@ var requireErrorCauseRule = createRule({
557
1027
  });
558
1028
 
559
1029
  // src/rules/require-registered-keys.ts
560
- var import_utils6 = require("@typescript-eslint/utils");
561
- var RULE_NAME6 = "require-registered-keys";
562
- var optionSchema5 = {
1030
+ var import_utils7 = require("@typescript-eslint/utils");
1031
+ var RULE_NAME7 = "require-registered-keys";
1032
+ var optionSchema6 = {
563
1033
  type: "object",
564
1034
  additionalProperties: false,
565
1035
  properties: {
@@ -578,30 +1048,30 @@ var optionSchema5 = {
578
1048
  registry: { type: "string", minLength: 1 }
579
1049
  }
580
1050
  };
581
- function calleePath(callee) {
582
- if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
1051
+ function calleePath2(callee) {
1052
+ if (callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
583
1053
  return callee.name;
584
1054
  }
585
- if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed) {
586
- if (callee.property.type !== import_utils6.AST_NODE_TYPES.Identifier) {
1055
+ if (callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !callee.computed) {
1056
+ if (callee.property.type !== import_utils7.AST_NODE_TYPES.Identifier) {
587
1057
  return null;
588
1058
  }
589
- const objectPath = calleePath(callee.object);
1059
+ const objectPath = calleePath2(callee.object);
590
1060
  return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
591
1061
  }
592
1062
  return null;
593
1063
  }
594
1064
  function isStringLiteral(node) {
595
- return node.type === import_utils6.AST_NODE_TYPES.Literal && typeof node.value === "string";
1065
+ return node.type === import_utils7.AST_NODE_TYPES.Literal && typeof node.value === "string";
596
1066
  }
597
1067
  var requireRegisteredKeysRule = createRule({
598
- name: RULE_NAME6,
1068
+ name: RULE_NAME7,
599
1069
  meta: {
600
1070
  type: "suggestion",
601
1071
  docs: {
602
1072
  description: "Require the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal."
603
1073
  },
604
- schema: [optionSchema5],
1074
+ schema: [optionSchema6],
605
1075
  messages: {
606
1076
  unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
607
1077
  }
@@ -625,11 +1095,11 @@ var requireRegisteredKeysRule = createRule({
625
1095
  const registryHint = registry ? ` (import it from '${registry}')` : "";
626
1096
  return {
627
1097
  CallExpression(node) {
628
- const path2 = calleePath(node.callee);
629
- if (path2 === null) {
1098
+ const path3 = calleePath2(node.callee);
1099
+ if (path3 === null) {
630
1100
  return;
631
1101
  }
632
- const indexes = sinkMap.get(path2);
1102
+ const indexes = sinkMap.get(path3);
633
1103
  if (indexes === void 0) {
634
1104
  return;
635
1105
  }
@@ -639,7 +1109,7 @@ var requireRegisteredKeysRule = createRule({
639
1109
  context.report({
640
1110
  node: arg,
641
1111
  messageId: "unregisteredKey",
642
- data: { callee: path2, value: `'${arg.value}'`, registryHint }
1112
+ data: { callee: path3, value: `'${arg.value}'`, registryHint }
643
1113
  });
644
1114
  }
645
1115
  }
@@ -649,29 +1119,29 @@ var requireRegisteredKeysRule = createRule({
649
1119
  });
650
1120
 
651
1121
  // src/rules/require-schema-parse-at-boundary.ts
652
- var import_utils7 = require("@typescript-eslint/utils");
653
- var RULE_NAME7 = "require-schema-parse-at-boundary";
1122
+ var import_utils8 = require("@typescript-eslint/utils");
1123
+ var RULE_NAME8 = "require-schema-parse-at-boundary";
654
1124
  function isJsonParseCall(node) {
655
- return node.type === import_utils7.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils7.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils7.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
1125
+ return node.type === import_utils8.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
656
1126
  }
657
1127
  function isAwaitJsonCall(node) {
658
- if (node.type !== import_utils7.AST_NODE_TYPES.AwaitExpression) {
1128
+ if (node.type !== import_utils8.AST_NODE_TYPES.AwaitExpression) {
659
1129
  return false;
660
1130
  }
661
1131
  const call = node.argument;
662
- return call.type === import_utils7.AST_NODE_TYPES.CallExpression && call.arguments.length === 0 && call.callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.property.type === import_utils7.AST_NODE_TYPES.Identifier && call.callee.property.name === "json";
1132
+ return call.type === import_utils8.AST_NODE_TYPES.CallExpression && call.arguments.length === 0 && call.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && call.callee.property.name === "json";
663
1133
  }
664
1134
  function isShapeClaim(annotation) {
665
- if (annotation.type === import_utils7.AST_NODE_TYPES.TSArrayType) {
1135
+ if (annotation.type === import_utils8.AST_NODE_TYPES.TSArrayType) {
666
1136
  return true;
667
1137
  }
668
- if (annotation.type === import_utils7.AST_NODE_TYPES.TSTypeReference) {
669
- return !(annotation.typeName.type === import_utils7.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
1138
+ if (annotation.type === import_utils8.AST_NODE_TYPES.TSTypeReference) {
1139
+ return !(annotation.typeName.type === import_utils8.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
670
1140
  }
671
1141
  return false;
672
1142
  }
673
1143
  var requireSchemaParseAtBoundaryRule = createRule({
674
- name: RULE_NAME7,
1144
+ name: RULE_NAME8,
675
1145
  meta: {
676
1146
  type: "problem",
677
1147
  docs: {
@@ -699,10 +1169,10 @@ var requireSchemaParseAtBoundaryRule = createRule({
699
1169
  });
700
1170
 
701
1171
  // src/rules/restrict-throw-to-taxonomy.ts
702
- var import_utils8 = require("@typescript-eslint/utils");
703
- var RULE_NAME8 = "restrict-throw-to-taxonomy";
1172
+ var import_utils9 = require("@typescript-eslint/utils");
1173
+ var RULE_NAME9 = "restrict-throw-to-taxonomy";
704
1174
  var DEFAULT_ALLOW = ["Error"];
705
- var optionSchema6 = {
1175
+ var optionSchema7 = {
706
1176
  type: "object",
707
1177
  additionalProperties: false,
708
1178
  properties: {
@@ -715,25 +1185,25 @@ var optionSchema6 = {
715
1185
  };
716
1186
  function constructorSimpleName2(node) {
717
1187
  const callee = node.callee;
718
- if (callee.type === import_utils8.AST_NODE_TYPES.Identifier) {
1188
+ if (callee.type === import_utils9.AST_NODE_TYPES.Identifier) {
719
1189
  return callee.name;
720
1190
  }
721
- if (callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
1191
+ if (callee.type === import_utils9.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils9.AST_NODE_TYPES.Identifier) {
722
1192
  return callee.property.name;
723
1193
  }
724
1194
  return null;
725
1195
  }
726
1196
  function isNonErrorValue(node) {
727
- return node.type === import_utils8.AST_NODE_TYPES.Literal || node.type === import_utils8.AST_NODE_TYPES.TemplateLiteral || node.type === import_utils8.AST_NODE_TYPES.ObjectExpression || node.type === import_utils8.AST_NODE_TYPES.ArrayExpression;
1197
+ return node.type === import_utils9.AST_NODE_TYPES.Literal || node.type === import_utils9.AST_NODE_TYPES.TemplateLiteral || node.type === import_utils9.AST_NODE_TYPES.ObjectExpression || node.type === import_utils9.AST_NODE_TYPES.ArrayExpression;
728
1198
  }
729
1199
  var restrictThrowToTaxonomyRule = createRule({
730
- name: RULE_NAME8,
1200
+ name: RULE_NAME9,
731
1201
  meta: {
732
1202
  type: "problem",
733
1203
  docs: {
734
1204
  description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
735
1205
  },
736
- schema: [optionSchema6],
1206
+ schema: [optionSchema7],
737
1207
  messages: {
738
1208
  disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
739
1209
  nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
@@ -746,7 +1216,7 @@ var restrictThrowToTaxonomyRule = createRule({
746
1216
  return {
747
1217
  ThrowStatement(node) {
748
1218
  const arg = node.argument;
749
- if (arg.type === import_utils8.AST_NODE_TYPES.NewExpression) {
1219
+ if (arg.type === import_utils9.AST_NODE_TYPES.NewExpression) {
750
1220
  const name = constructorSimpleName2(arg);
751
1221
  if (name !== null && !allow.has(name)) {
752
1222
  context.report({
@@ -758,7 +1228,7 @@ var restrictThrowToTaxonomyRule = createRule({
758
1228
  return;
759
1229
  }
760
1230
  if (isNonErrorValue(arg)) {
761
- const kind = arg.type === import_utils8.AST_NODE_TYPES.ObjectExpression ? "object" : arg.type === import_utils8.AST_NODE_TYPES.ArrayExpression ? "array" : "literal";
1231
+ const kind = arg.type === import_utils9.AST_NODE_TYPES.ObjectExpression ? "object" : arg.type === import_utils9.AST_NODE_TYPES.ArrayExpression ? "array" : "literal";
762
1232
  context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
763
1233
  }
764
1234
  }
@@ -766,10 +1236,863 @@ var restrictThrowToTaxonomyRule = createRule({
766
1236
  }
767
1237
  });
768
1238
 
1239
+ // src/rules/schema-enum-field-consistency.ts
1240
+ var import_utils10 = require("@typescript-eslint/utils");
1241
+ var RULE_NAME10 = "schema-enum-field-consistency";
1242
+ var MODIFIERS = /* @__PURE__ */ new Set([
1243
+ "optional",
1244
+ "nullable",
1245
+ "nullish",
1246
+ "default",
1247
+ "prefault",
1248
+ "catch",
1249
+ "describe",
1250
+ "meta",
1251
+ "readonly"
1252
+ ]);
1253
+ var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
1254
+ var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
1255
+ var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
1256
+ var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
1257
+ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1258
+ var UNION = /* @__PURE__ */ new Set(["union"]);
1259
+ var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1260
+ var STRING = /* @__PURE__ */ new Set(["string"]);
1261
+ var optionSchema8 = {
1262
+ type: "object",
1263
+ additionalProperties: false,
1264
+ properties: {
1265
+ zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
1266
+ ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
1267
+ enumIdentifierPattern: { type: "string" }
1268
+ }
1269
+ };
1270
+ var OTHER = { kind: "other" };
1271
+ function isEnumOccurrence(occurrence) {
1272
+ return occurrence.kind.kind === "enum";
1273
+ }
1274
+ function methodCall(node) {
1275
+ if (node.type !== import_utils10.AST_NODE_TYPES.CallExpression) return null;
1276
+ const callee = node.callee;
1277
+ if (callee.type !== import_utils10.AST_NODE_TYPES.MemberExpression || callee.computed) return null;
1278
+ if (callee.property.type !== import_utils10.AST_NODE_TYPES.Identifier) return null;
1279
+ return { receiver: callee.object, method: callee.property.name, call: node };
1280
+ }
1281
+ function propertyName(property) {
1282
+ if (property.computed) return null;
1283
+ if (property.key.type === import_utils10.AST_NODE_TYPES.Identifier) return property.key.name;
1284
+ if (property.key.type === import_utils10.AST_NODE_TYPES.Literal && typeof property.key.value === "string") {
1285
+ return property.key.value;
1286
+ }
1287
+ return null;
1288
+ }
1289
+ var schemaEnumFieldConsistencyRule = createRule({
1290
+ name: RULE_NAME10,
1291
+ meta: {
1292
+ type: "problem",
1293
+ docs: {
1294
+ description: "Disallow a zod field that is an enum in one object schema of a module from being `z.string()` in another, which widens the wire type every consumer then narrows by hand."
1295
+ },
1296
+ schema: [optionSchema8],
1297
+ messages: {
1298
+ widenedEnumField: "`{{field}}` is `z.string()` here but an enum on line {{line}} of this file. The widened type leaks `string` to every consumer, which then has to narrow or cast it. Use {{suggestion}} instead (and, if the stored data is free text, migrate it first)."
1299
+ }
1300
+ },
1301
+ defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
1302
+ create(context, [options]) {
1303
+ const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
1304
+ const ignoreFields = new Set(options.ignoreFields ?? []);
1305
+ const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
1306
+ const sourceCode = context.sourceCode;
1307
+ const fields = /* @__PURE__ */ new Map();
1308
+ function isZodCall(node, names) {
1309
+ const call = methodCall(node);
1310
+ return call !== null && call.receiver.type === import_utils10.AST_NODE_TYPES.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
1311
+ }
1312
+ function resolveVariable(identifier) {
1313
+ let scope = sourceCode.getScope(identifier);
1314
+ while (scope !== null) {
1315
+ const variable = scope.set.get(identifier.name);
1316
+ if (variable !== void 0) return variable;
1317
+ scope = scope.upper;
1318
+ }
1319
+ return null;
1320
+ }
1321
+ function identifierIsEnum(identifier, seen) {
1322
+ const definition = resolveVariable(identifier)?.defs[0];
1323
+ if (definition === void 0) return false;
1324
+ if (definition.type === "ImportBinding") {
1325
+ return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
1326
+ }
1327
+ if (definition.type !== "Variable") return false;
1328
+ const init = definition.node.init;
1329
+ if (init === null || seen.has(init)) return false;
1330
+ return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
1331
+ }
1332
+ function isLiteralUnion(node) {
1333
+ if (!isZodCall(node, UNION)) return false;
1334
+ const members = node.arguments[0];
1335
+ if (members?.type !== import_utils10.AST_NODE_TYPES.ArrayExpression || members.elements.length === 0) {
1336
+ return false;
1337
+ }
1338
+ return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
1339
+ }
1340
+ function isMultiLiteral(node) {
1341
+ if (!isZodCall(node, LITERAL)) return false;
1342
+ const value = node.arguments[0];
1343
+ return value?.type === import_utils10.AST_NODE_TYPES.ArrayExpression && value.elements.length > 1;
1344
+ }
1345
+ function classify(node, seen) {
1346
+ let current = node;
1347
+ for (; ; ) {
1348
+ const call = methodCall(current);
1349
+ if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
1350
+ break;
1351
+ }
1352
+ current = call.receiver;
1353
+ }
1354
+ if (current.type === import_utils10.AST_NODE_TYPES.Identifier) {
1355
+ return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
1356
+ }
1357
+ if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
1358
+ return { kind: "enum", identifier: null };
1359
+ }
1360
+ current = node;
1361
+ for (; ; ) {
1362
+ if (isZodCall(current, STRING)) return { kind: "string" };
1363
+ const call = methodCall(current);
1364
+ if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
1365
+ current = call.receiver;
1366
+ }
1367
+ }
1368
+ function collectShape(shape) {
1369
+ if (shape?.type !== import_utils10.AST_NODE_TYPES.ObjectExpression) return;
1370
+ for (const property of shape.properties) {
1371
+ if (property.type !== import_utils10.AST_NODE_TYPES.Property) continue;
1372
+ const name = propertyName(property);
1373
+ if (name === null || ignoreFields.has(name)) continue;
1374
+ const kind = classify(property.value, /* @__PURE__ */ new Set());
1375
+ if (kind.kind === "other") continue;
1376
+ const occurrences = fields.get(name) ?? [];
1377
+ occurrences.push({ property, kind });
1378
+ fields.set(name, occurrences);
1379
+ }
1380
+ }
1381
+ return {
1382
+ CallExpression(node) {
1383
+ if (isZodCall(node, OBJECT_FACTORIES)) {
1384
+ collectShape(node.arguments[0]);
1385
+ return;
1386
+ }
1387
+ const call = methodCall(node);
1388
+ if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
1389
+ },
1390
+ "Program:exit"() {
1391
+ for (const [field, occurrences] of fields) {
1392
+ const enumOccurrence = occurrences.find(isEnumOccurrence);
1393
+ if (enumOccurrence === void 0) continue;
1394
+ const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
1395
+ for (const occurrence of occurrences) {
1396
+ if (occurrence.kind.kind !== "string") continue;
1397
+ context.report({
1398
+ node: occurrence.property,
1399
+ messageId: "widenedEnumField",
1400
+ data: {
1401
+ field,
1402
+ line: String(enumOccurrence.property.loc.start.line),
1403
+ suggestion
1404
+ }
1405
+ });
1406
+ }
1407
+ }
1408
+ }
1409
+ };
1410
+ }
1411
+ });
1412
+
1413
+ // src/i18n/catalogs.ts
1414
+ var import_node_fs2 = require("fs");
1415
+ var import_node_path2 = __toESM(require("path"), 1);
1416
+ var NS_PLACEHOLDER = "{ns}";
1417
+ var SAFE_NAMESPACE = /^(?!\.{1,2}$)[^/\\\0]+$/u;
1418
+ var fileCache = /* @__PURE__ */ new Map();
1419
+ function readCatalogFile(absolute) {
1420
+ let mtimeMs;
1421
+ try {
1422
+ const stats = (0, import_node_fs2.statSync)(absolute);
1423
+ if (!stats.isFile()) return { kind: "missing" };
1424
+ mtimeMs = stats.mtimeMs;
1425
+ } catch {
1426
+ return { kind: "missing" };
1427
+ }
1428
+ const cached = fileCache.get(absolute);
1429
+ if (cached !== void 0 && cached.mtimeMs === mtimeMs) {
1430
+ return cached.value.ok ? { kind: "ok", entry: cached } : { kind: "invalid", reason: cached.value.reason };
1431
+ }
1432
+ let value;
1433
+ try {
1434
+ value = { ok: true, json: JSON.parse((0, import_node_fs2.readFileSync)(absolute, "utf8")) };
1435
+ } catch (error) {
1436
+ value = { ok: false, reason: error instanceof Error ? error.message : String(error) };
1437
+ }
1438
+ const entry = { mtimeMs, value, flattened: /* @__PURE__ */ new Map() };
1439
+ fileCache.set(absolute, entry);
1440
+ return value.ok ? { kind: "ok", entry } : { kind: "invalid", reason: value.reason };
1441
+ }
1442
+ function isRecord(value) {
1443
+ return value !== null && typeof value === "object";
1444
+ }
1445
+ function descend(json, keyPath) {
1446
+ if (keyPath === void 0 || keyPath === "") return json;
1447
+ let current = json;
1448
+ for (const segment of keyPath.split(".")) {
1449
+ if (!isRecord(current) || !Object.hasOwn(current, segment)) return void 0;
1450
+ current = current[segment];
1451
+ }
1452
+ return current;
1453
+ }
1454
+ function flatten(root, keySeparator, label) {
1455
+ const leaves = /* @__PURE__ */ new Set();
1456
+ const branches = /* @__PURE__ */ new Set();
1457
+ const visit = (value, prefix) => {
1458
+ if (!isRecord(value)) {
1459
+ leaves.add(prefix);
1460
+ return;
1461
+ }
1462
+ branches.add(prefix);
1463
+ if (keySeparator === false) return;
1464
+ for (const [key, child] of Object.entries(value)) {
1465
+ visit(child, `${prefix}${keySeparator}${key}`);
1466
+ }
1467
+ };
1468
+ for (const [key, child] of Object.entries(root)) {
1469
+ if (keySeparator === false) {
1470
+ (isRecord(child) ? branches : leaves).add(key);
1471
+ } else {
1472
+ visit(child, key);
1473
+ }
1474
+ }
1475
+ return { label, leaves, branches };
1476
+ }
1477
+ function loadSource(cwd, file, keyPath, keySeparator) {
1478
+ const absolute = import_node_path2.default.isAbsolute(file) ? file : import_node_path2.default.resolve(cwd, file);
1479
+ const read = readCatalogFile(absolute);
1480
+ if (read.kind === "missing") return { kind: "absent" };
1481
+ if (read.kind === "invalid") return { kind: "error", reason: `${file}: ${read.reason}` };
1482
+ const cacheKey = `${keyPath ?? ""}\0${keySeparator === false ? "" : keySeparator}`;
1483
+ const cached = read.entry.flattened.get(cacheKey);
1484
+ if (cached !== void 0) {
1485
+ return cached === null ? { kind: "absent" } : { kind: "ok", catalog: cached };
1486
+ }
1487
+ const subtree = read.entry.value.ok ? descend(read.entry.value.json, keyPath) : void 0;
1488
+ const label = keyPath ? `${file}#${keyPath}` : file;
1489
+ const catalog = isRecord(subtree) ? flatten(subtree, keySeparator, label) : null;
1490
+ read.entry.flattened.set(cacheKey, catalog);
1491
+ return catalog === null ? { kind: "absent" } : { kind: "ok", catalog };
1492
+ }
1493
+ function catalogsForNamespace(namespace, sources, settings) {
1494
+ const catalogs = [];
1495
+ const errors = [];
1496
+ for (const source of sources) {
1497
+ const templated = source.file.includes(NS_PLACEHOLDER) || (source.keyPath?.includes(NS_PLACEHOLDER) ?? false);
1498
+ if (templated) {
1499
+ if (!SAFE_NAMESPACE.test(namespace)) continue;
1500
+ const file = source.file.replaceAll(NS_PLACEHOLDER, namespace);
1501
+ const keyPath = source.keyPath?.replaceAll(NS_PLACEHOLDER, namespace);
1502
+ const load2 = loadSource(settings.cwd, file, keyPath, settings.keySeparator);
1503
+ if (load2.kind === "ok") catalogs.push(load2.catalog);
1504
+ else if (load2.kind === "error") errors.push(load2.reason);
1505
+ continue;
1506
+ }
1507
+ if ((source.namespace ?? settings.defaultNamespace) !== namespace) continue;
1508
+ const load = loadSource(settings.cwd, source.file, source.keyPath, settings.keySeparator);
1509
+ if (load.kind === "ok") catalogs.push(load.catalog);
1510
+ else if (load.kind === "error") errors.push(load.reason);
1511
+ else {
1512
+ const where = source.keyPath ? `${source.file}#${source.keyPath}` : source.file;
1513
+ errors.push(`${where}: not found or not a JSON object`);
1514
+ }
1515
+ }
1516
+ return { catalogs, errors };
1517
+ }
1518
+ var PLURAL_CATEGORIES = ["zero", "one", "two", "few", "many", "other"];
1519
+ function catalogHasKey(catalog, key, lookup) {
1520
+ if (catalog.leaves.has(key)) return true;
1521
+ if (lookup.returnObjects && catalog.branches.has(key)) return true;
1522
+ if (lookup.plural) {
1523
+ const sep = lookup.pluralSeparator;
1524
+ for (const category of PLURAL_CATEGORIES) {
1525
+ if (catalog.leaves.has(`${key}${sep}${category}`)) return true;
1526
+ if (catalog.leaves.has(`${key}${sep}ordinal${sep}${category}`)) return true;
1527
+ }
1528
+ }
1529
+ if (lookup.context) {
1530
+ const variant = `${key}${lookup.contextSeparator}`;
1531
+ for (const leaf of catalog.leaves) {
1532
+ if (leaf.startsWith(variant)) return true;
1533
+ }
1534
+ }
1535
+ return false;
1536
+ }
1537
+ function catalogHasPrefix(catalog, prefix) {
1538
+ for (const leaf of catalog.leaves) {
1539
+ if (leaf.startsWith(prefix)) return true;
1540
+ }
1541
+ for (const branch of catalog.branches) {
1542
+ if (branch.startsWith(prefix)) return true;
1543
+ }
1544
+ return false;
1545
+ }
1546
+
1547
+ // src/i18n/translationUsage.ts
1548
+ var import_utils11 = require("@typescript-eslint/utils");
1549
+ var UNRESOLVED = "unresolved";
1550
+ var MAX_DEPTH = 8;
1551
+ function unwrap(node) {
1552
+ let current = node;
1553
+ while (current.type === import_utils11.AST_NODE_TYPES.TSAsExpression || current.type === import_utils11.AST_NODE_TYPES.TSSatisfiesExpression || current.type === import_utils11.AST_NODE_TYPES.TSNonNullExpression) {
1554
+ current = current.expression;
1555
+ }
1556
+ return current;
1557
+ }
1558
+ function staticString(node) {
1559
+ const inner = unwrap(node);
1560
+ if (inner.type === import_utils11.AST_NODE_TYPES.Literal && typeof inner.value === "string") return inner.value;
1561
+ if (inner.type === import_utils11.AST_NODE_TYPES.TemplateLiteral && inner.expressions.length === 0) {
1562
+ return inner.quasis[0]?.value.cooked ?? null;
1563
+ }
1564
+ return null;
1565
+ }
1566
+ function propertyName2(property) {
1567
+ if (property.computed) return staticString(property.key);
1568
+ if (property.key.type === import_utils11.AST_NODE_TYPES.Identifier) return property.key.name;
1569
+ return staticString(property.key);
1570
+ }
1571
+ function targetIdentifier(node) {
1572
+ if (node.type === import_utils11.AST_NODE_TYPES.Identifier) return node;
1573
+ if (node.type === import_utils11.AST_NODE_TYPES.AssignmentPattern && node.left.type === import_utils11.AST_NODE_TYPES.Identifier) {
1574
+ return node.left;
1575
+ }
1576
+ return null;
1577
+ }
1578
+ function createTranslationVisitor(context, settings, onUsage) {
1579
+ const sourceCode = context.sourceCode;
1580
+ const defaultBinding = { namespaces: [settings.defaultNamespace], keyPrefix: null };
1581
+ function resolveVariable(identifier) {
1582
+ let scope = sourceCode.getScope(identifier);
1583
+ while (scope !== null) {
1584
+ const variable = scope.set.get(identifier.name);
1585
+ if (variable !== void 0) return variable;
1586
+ scope = scope.upper;
1587
+ }
1588
+ return null;
1589
+ }
1590
+ function typedStringLiteral(node) {
1591
+ const services = sourceCode.parserServices;
1592
+ const program = services?.program;
1593
+ const map = services?.esTreeNodeToTSNodeMap;
1594
+ if (!program || !map) return null;
1595
+ const type = program.getTypeChecker().getTypeAtLocation(map.get(node));
1596
+ return type.isStringLiteral() ? type.value : null;
1597
+ }
1598
+ function resolveNamespaces(node, depth = 0) {
1599
+ if (node === void 0) return defaultBinding.namespaces;
1600
+ const inner = unwrap(node);
1601
+ const literal = staticString(inner);
1602
+ if (literal !== null) return [literal];
1603
+ if (inner.type === import_utils11.AST_NODE_TYPES.Literal && inner.value === null) return defaultBinding.namespaces;
1604
+ if (inner.type === import_utils11.AST_NODE_TYPES.ArrayExpression) {
1605
+ const namespaces = [];
1606
+ for (const element of inner.elements) {
1607
+ if (element === null || element.type === import_utils11.AST_NODE_TYPES.SpreadElement) return UNRESOLVED;
1608
+ const value2 = staticString(element) ?? resolveIdentifierString(element, depth);
1609
+ if (value2 === null) return UNRESOLVED;
1610
+ namespaces.push(value2);
1611
+ }
1612
+ return namespaces.length > 0 ? namespaces : defaultBinding.namespaces;
1613
+ }
1614
+ if (inner.type === import_utils11.AST_NODE_TYPES.Identifier && inner.name === "undefined") return defaultBinding.namespaces;
1615
+ const value = resolveIdentifierString(inner, depth);
1616
+ return value === null ? UNRESOLVED : [value];
1617
+ }
1618
+ function resolveIdentifierString(node, depth) {
1619
+ if (node.type !== import_utils11.AST_NODE_TYPES.Identifier || depth > MAX_DEPTH) return null;
1620
+ if (Object.hasOwn(settings.namespaceIdentifiers, node.name)) {
1621
+ return settings.namespaceIdentifiers[node.name] ?? null;
1622
+ }
1623
+ const definition = resolveVariable(node)?.defs[0];
1624
+ if (definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.id.type === import_utils11.AST_NODE_TYPES.Identifier && definition.node.init !== null) {
1625
+ const init = unwrap(definition.node.init);
1626
+ const literal = staticString(init);
1627
+ if (literal !== null) return literal;
1628
+ const chained = resolveIdentifierString(init, depth + 1);
1629
+ if (chained !== null) return chained;
1630
+ }
1631
+ return typedStringLiteral(node);
1632
+ }
1633
+ function isHookCall(node) {
1634
+ return node.type === import_utils11.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils11.AST_NODE_TYPES.Identifier && settings.hooks.has(node.callee.name);
1635
+ }
1636
+ function isInstance(node) {
1637
+ return node.type === import_utils11.AST_NODE_TYPES.Identifier && settings.instances.has(node.name);
1638
+ }
1639
+ function staticPrefix(node) {
1640
+ if (node === void 0) return null;
1641
+ const inner = unwrap(node);
1642
+ if (inner.type === import_utils11.AST_NODE_TYPES.Identifier && inner.name === "undefined") return null;
1643
+ if (inner.type === import_utils11.AST_NODE_TYPES.Literal && inner.value === null) return null;
1644
+ return staticString(inner) ?? UNRESOLVED;
1645
+ }
1646
+ function bindingFromHook(call) {
1647
+ const [nsArg, optionsArg] = call.arguments;
1648
+ const namespaces = resolveNamespaces(nsArg);
1649
+ if (namespaces === UNRESOLVED) return UNRESOLVED;
1650
+ let keyPrefix = null;
1651
+ if (optionsArg !== void 0) {
1652
+ const options = unwrap(optionsArg);
1653
+ if (options.type !== import_utils11.AST_NODE_TYPES.ObjectExpression) return UNRESOLVED;
1654
+ for (const property of options.properties) {
1655
+ if (property.type !== import_utils11.AST_NODE_TYPES.Property) return UNRESOLVED;
1656
+ if (propertyName2(property) !== "keyPrefix") continue;
1657
+ const prefix = staticPrefix(property.value);
1658
+ if (prefix === UNRESOLVED) return UNRESOLVED;
1659
+ keyPrefix = prefix;
1660
+ }
1661
+ }
1662
+ return { namespaces, keyPrefix };
1663
+ }
1664
+ function bindingFromGetFixedT(call) {
1665
+ const [, nsArg, prefixArg] = call.arguments;
1666
+ const namespaces = resolveNamespaces(nsArg);
1667
+ if (namespaces === UNRESOLVED) return UNRESOLVED;
1668
+ const keyPrefix = staticPrefix(prefixArg);
1669
+ if (keyPrefix === UNRESOLVED) return UNRESOLVED;
1670
+ return { namespaces, keyPrefix };
1671
+ }
1672
+ function isGetFixedT(node) {
1673
+ return node.type === import_utils11.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils11.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils11.AST_NODE_TYPES.Identifier && node.callee.property.name === "getFixedT" && isInstance(node.callee.object);
1674
+ }
1675
+ function hookCallOf(identifier) {
1676
+ const definition = resolveVariable(identifier)?.defs[0];
1677
+ if (definition?.type !== "Variable" || definition.node.id.type !== import_utils11.AST_NODE_TYPES.Identifier) return null;
1678
+ const init = definition.node.init === null ? null : unwrap(definition.node.init);
1679
+ return init !== null && isHookCall(init) ? init : null;
1680
+ }
1681
+ function bindingOfTSource(object) {
1682
+ const inner = unwrap(object);
1683
+ if (isHookCall(inner)) return bindingFromHook(inner);
1684
+ if (inner.type === import_utils11.AST_NODE_TYPES.Identifier) {
1685
+ const hook = hookCallOf(inner);
1686
+ if (hook !== null) return bindingFromHook(hook);
1687
+ if (isInstance(inner)) return defaultBinding;
1688
+ }
1689
+ return null;
1690
+ }
1691
+ function bindingFromType(annotation) {
1692
+ const type = annotation?.typeAnnotation;
1693
+ if (type?.type !== import_utils11.AST_NODE_TYPES.TSTypeReference) return null;
1694
+ const name = type.typeName.type === import_utils11.AST_NODE_TYPES.Identifier ? type.typeName.name : type.typeName.type === import_utils11.AST_NODE_TYPES.TSQualifiedName ? type.typeName.right.name : null;
1695
+ if (name === null || !settings.typeNames.has(name)) return null;
1696
+ const [nsType, prefixType] = type.typeArguments?.params ?? [];
1697
+ const literalOf = (node) => node.type === import_utils11.AST_NODE_TYPES.TSLiteralType ? staticString(node.literal) : null;
1698
+ let namespaces = defaultBinding.namespaces;
1699
+ if (nsType !== void 0) {
1700
+ if (nsType.type === import_utils11.AST_NODE_TYPES.TSTupleType) {
1701
+ const values = nsType.elementTypes.map(literalOf);
1702
+ if (values.length === 0 || values.some((value) => value === null)) return UNRESOLVED;
1703
+ namespaces = values;
1704
+ } else {
1705
+ const value = literalOf(nsType);
1706
+ if (value === null) return UNRESOLVED;
1707
+ namespaces = [value];
1708
+ }
1709
+ }
1710
+ let keyPrefix = null;
1711
+ if (prefixType !== void 0) {
1712
+ keyPrefix = literalOf(prefixType);
1713
+ if (keyPrefix === null) return UNRESOLVED;
1714
+ }
1715
+ return { namespaces, keyPrefix };
1716
+ }
1717
+ function bindingFromDeclarator(declarator, name, depth) {
1718
+ if (declarator.init === null) return null;
1719
+ const init = unwrap(declarator.init);
1720
+ const id = declarator.id;
1721
+ if (id.type === import_utils11.AST_NODE_TYPES.Identifier) {
1722
+ if (isGetFixedT(init)) return bindingFromGetFixedT(init);
1723
+ if (init.type === import_utils11.AST_NODE_TYPES.MemberExpression && !init.computed && init.property.type === import_utils11.AST_NODE_TYPES.Identifier && init.property.name === "t") {
1724
+ return bindingOfTSource(init.object);
1725
+ }
1726
+ if (init.type === import_utils11.AST_NODE_TYPES.Identifier) return bindingOfIdentifier(init, depth + 1);
1727
+ return null;
1728
+ }
1729
+ if (id.type === import_utils11.AST_NODE_TYPES.ObjectPattern) {
1730
+ for (const property of id.properties) {
1731
+ if (property.type !== import_utils11.AST_NODE_TYPES.Property || targetIdentifier(property.value) !== name) continue;
1732
+ return propertyName2(property) === "t" ? bindingOfTSource(init) : null;
1733
+ }
1734
+ return null;
1735
+ }
1736
+ if (id.type === import_utils11.AST_NODE_TYPES.ArrayPattern) {
1737
+ const first = id.elements[0];
1738
+ if (first && targetIdentifier(first) === name && isHookCall(init)) return bindingFromHook(init);
1739
+ }
1740
+ return null;
1741
+ }
1742
+ function bindingOfIdentifier(identifier, depth = 0) {
1743
+ if (depth > MAX_DEPTH) return null;
1744
+ const variable = resolveVariable(identifier);
1745
+ if (variable === null) {
1746
+ return settings.functions.has(identifier.name) ? defaultBinding : null;
1747
+ }
1748
+ const definition = variable.defs[0];
1749
+ if (definition === void 0) return null;
1750
+ switch (definition.type) {
1751
+ case "ImportBinding": {
1752
+ const specifier = definition.node;
1753
+ if (specifier.type !== import_utils11.AST_NODE_TYPES.ImportSpecifier) return null;
1754
+ const imported = specifier.imported.type === import_utils11.AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
1755
+ return settings.functions.has(imported) ? defaultBinding : null;
1756
+ }
1757
+ case "Parameter": {
1758
+ const name = definition.name;
1759
+ if (name.type !== import_utils11.AST_NODE_TYPES.Identifier) return null;
1760
+ const typed = bindingFromType(name.typeAnnotation);
1761
+ if (typed !== null) return typed;
1762
+ return settings.functions.has(name.name) ? UNRESOLVED : null;
1763
+ }
1764
+ case "Variable":
1765
+ return definition.name.type === import_utils11.AST_NODE_TYPES.Identifier ? bindingFromDeclarator(definition.node, definition.name, depth) : null;
1766
+ default:
1767
+ return null;
1768
+ }
1769
+ }
1770
+ function bindingOfCallee(callee) {
1771
+ if (callee.type === import_utils11.AST_NODE_TYPES.Identifier) return bindingOfIdentifier(callee);
1772
+ if (callee.type === import_utils11.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils11.AST_NODE_TYPES.Identifier && callee.property.name === "t") {
1773
+ return bindingOfTSource(callee.object);
1774
+ }
1775
+ return null;
1776
+ }
1777
+ function readCallOptions(node) {
1778
+ const none = { namespaces: null, plural: false, context: false, returnObjects: false };
1779
+ if (node === void 0) return none;
1780
+ const inner = unwrap(node);
1781
+ if (inner.type !== import_utils11.AST_NODE_TYPES.ObjectExpression) return UNRESOLVED;
1782
+ let namespaces = null;
1783
+ let plural = false;
1784
+ let context2 = false;
1785
+ let returnObjects = false;
1786
+ for (const property of inner.properties) {
1787
+ if (property.type !== import_utils11.AST_NODE_TYPES.Property) return UNRESOLVED;
1788
+ const name = propertyName2(property);
1789
+ if (name === null || name === "keyPrefix") return UNRESOLVED;
1790
+ if (name === "ns") {
1791
+ const resolved = resolveNamespaces(property.value);
1792
+ if (resolved === UNRESOLVED) return UNRESOLVED;
1793
+ namespaces = resolved;
1794
+ } else if (name === "count") {
1795
+ plural = true;
1796
+ } else if (name === "context") {
1797
+ context2 = true;
1798
+ } else if (name === "returnObjects") {
1799
+ const value = unwrap(property.value);
1800
+ returnObjects = !(value.type === import_utils11.AST_NODE_TYPES.Literal && value.value === false);
1801
+ }
1802
+ }
1803
+ return { namespaces, plural, context: context2, returnObjects };
1804
+ }
1805
+ function qualify(raw, binding, optionNamespaces) {
1806
+ const { nsSeparator, keySeparator } = settings;
1807
+ if (nsSeparator !== false && raw.includes(nsSeparator)) {
1808
+ if (binding.keyPrefix !== null) return null;
1809
+ const [head = "", ...rest] = raw.split(nsSeparator);
1810
+ if (head !== "" && rest.length > 0) {
1811
+ return { namespaces: [head], key: rest.join(keySeparator === false ? nsSeparator : keySeparator) };
1812
+ }
1813
+ }
1814
+ const namespaces = optionNamespaces ?? binding.namespaces;
1815
+ if (binding.keyPrefix === null || binding.keyPrefix === "") return { namespaces, key: raw };
1816
+ return { namespaces, key: `${binding.keyPrefix}${keySeparator === false ? "" : keySeparator}${raw}` };
1817
+ }
1818
+ function emit(node, keyNode, binding, options) {
1819
+ const inner = unwrap(keyNode);
1820
+ const raws = [];
1821
+ const single = staticString(inner);
1822
+ if (single !== null) {
1823
+ raws.push(single);
1824
+ } else if (inner.type === import_utils11.AST_NODE_TYPES.ArrayExpression && inner.elements.length > 0) {
1825
+ for (const element of inner.elements) {
1826
+ const value = element === null || element.type === import_utils11.AST_NODE_TYPES.SpreadElement ? null : staticString(element);
1827
+ if (value === null) {
1828
+ onUsage({ kind: "dynamic", node });
1829
+ return;
1830
+ }
1831
+ raws.push(value);
1832
+ }
1833
+ } else if (inner.type === import_utils11.AST_NODE_TYPES.TemplateLiteral) {
1834
+ const head = inner.quasis[0]?.value.cooked ?? "";
1835
+ const qualified = head === "" ? null : qualify(head, binding, options.namespaces);
1836
+ if (qualified === null) {
1837
+ onUsage({ kind: "dynamic", node });
1838
+ } else {
1839
+ onUsage({ kind: "prefix", node, namespaces: qualified.namespaces, prefix: qualified.key });
1840
+ }
1841
+ return;
1842
+ } else {
1843
+ onUsage({ kind: "dynamic", node });
1844
+ return;
1845
+ }
1846
+ let namespaces = null;
1847
+ const keys = [];
1848
+ for (const raw of raws) {
1849
+ const qualified = qualify(raw, binding, options.namespaces);
1850
+ if (qualified === null || raw === "") {
1851
+ onUsage({ kind: "unresolved", node });
1852
+ return;
1853
+ }
1854
+ if (namespaces !== null && namespaces.join("\0") !== qualified.namespaces.join("\0")) {
1855
+ onUsage({ kind: "unresolved", node });
1856
+ return;
1857
+ }
1858
+ namespaces = qualified.namespaces;
1859
+ keys.push(qualified.key);
1860
+ }
1861
+ onUsage({
1862
+ kind: "key",
1863
+ node,
1864
+ namespaces: namespaces ?? binding.namespaces,
1865
+ keys,
1866
+ plural: options.plural,
1867
+ context: options.context,
1868
+ returnObjects: options.returnObjects
1869
+ });
1870
+ }
1871
+ function jsxAttributeValue(attribute) {
1872
+ const value = attribute.value;
1873
+ if (value === null) return null;
1874
+ if (value.type === import_utils11.AST_NODE_TYPES.JSXExpressionContainer) {
1875
+ return value.expression.type === import_utils11.AST_NODE_TYPES.JSXEmptyExpression ? null : value.expression;
1876
+ }
1877
+ return value;
1878
+ }
1879
+ return {
1880
+ CallExpression(node) {
1881
+ const binding = bindingOfCallee(node.callee);
1882
+ if (binding === null) return;
1883
+ const [keyArg, secondArg, thirdArg] = node.arguments;
1884
+ if (keyArg === void 0) return;
1885
+ if (binding === UNRESOLVED) {
1886
+ onUsage({ kind: "unresolved", node: keyArg });
1887
+ return;
1888
+ }
1889
+ const optionsArg = secondArg !== void 0 && staticString(secondArg) !== null ? thirdArg : secondArg;
1890
+ const options = readCallOptions(optionsArg);
1891
+ if (options === UNRESOLVED) {
1892
+ onUsage({ kind: "unresolved", node: keyArg });
1893
+ return;
1894
+ }
1895
+ emit(keyArg, keyArg, binding, options);
1896
+ },
1897
+ JSXOpeningElement(node) {
1898
+ if (node.name.type !== import_utils11.AST_NODE_TYPES.JSXIdentifier || !settings.transComponents.has(node.name.name)) return;
1899
+ const attributes = /* @__PURE__ */ new Map();
1900
+ for (const attribute of node.attributes) {
1901
+ if (attribute.type === import_utils11.AST_NODE_TYPES.JSXSpreadAttribute) {
1902
+ onUsage({ kind: "unresolved", node });
1903
+ return;
1904
+ }
1905
+ if (attribute.name.type === import_utils11.AST_NODE_TYPES.JSXIdentifier) attributes.set(attribute.name.name, attribute);
1906
+ }
1907
+ const keyAttribute = attributes.get("i18nKey");
1908
+ const keyNode = keyAttribute === void 0 ? null : jsxAttributeValue(keyAttribute);
1909
+ if (keyNode === null) return;
1910
+ let binding = defaultBinding;
1911
+ const tAttribute = attributes.get("t");
1912
+ const tNode = tAttribute === void 0 ? null : jsxAttributeValue(tAttribute);
1913
+ if (tNode !== null) {
1914
+ binding = tNode.type === import_utils11.AST_NODE_TYPES.Identifier ? bindingOfIdentifier(tNode) : UNRESOLVED;
1915
+ }
1916
+ let namespaces = null;
1917
+ const nsAttribute = attributes.get("ns");
1918
+ const nsNode = nsAttribute === void 0 ? null : jsxAttributeValue(nsAttribute);
1919
+ if (nsNode !== null) {
1920
+ const resolved = resolveNamespaces(nsNode);
1921
+ if (resolved === UNRESOLVED) binding = UNRESOLVED;
1922
+ else namespaces = resolved;
1923
+ }
1924
+ if (binding === null || binding === UNRESOLVED) {
1925
+ onUsage({ kind: "unresolved", node: keyNode });
1926
+ return;
1927
+ }
1928
+ emit(keyNode, keyNode, binding, {
1929
+ namespaces,
1930
+ plural: attributes.has("count"),
1931
+ context: attributes.has("context"),
1932
+ returnObjects: false
1933
+ });
1934
+ }
1935
+ };
1936
+ }
1937
+
1938
+ // src/rules/translation-key-exists.ts
1939
+ var RULE_NAME11 = "translation-key-exists";
1940
+ var stringList = { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true };
1941
+ var separator = { oneOf: [{ type: "string", minLength: 1 }, { type: "boolean", enum: [false] }] };
1942
+ var optionSchema9 = {
1943
+ type: "object",
1944
+ additionalProperties: false,
1945
+ properties: {
1946
+ catalogs: {
1947
+ type: "array",
1948
+ items: {
1949
+ type: "object",
1950
+ additionalProperties: false,
1951
+ required: ["file"],
1952
+ properties: {
1953
+ file: { type: "string", minLength: 1 },
1954
+ namespace: { type: "string", minLength: 1 },
1955
+ keyPath: { type: "string", minLength: 1 }
1956
+ }
1957
+ }
1958
+ },
1959
+ defaultNamespace: { type: "string", minLength: 1 },
1960
+ fallbackNamespaces: stringList,
1961
+ hooks: stringList,
1962
+ instances: stringList,
1963
+ functions: stringList,
1964
+ typeNames: stringList,
1965
+ transComponents: stringList,
1966
+ namespaceIdentifiers: { type: "object", additionalProperties: { type: "string", minLength: 1 } },
1967
+ nsSeparator: separator,
1968
+ keySeparator: separator,
1969
+ pluralSeparator: { type: "string", minLength: 1 },
1970
+ contextSeparator: { type: "string", minLength: 1 },
1971
+ dynamicKeys: { type: "string", enum: ["ignore", "check-prefix"] }
1972
+ }
1973
+ };
1974
+ var TRANSLATION_DEFAULTS = {
1975
+ defaultNamespace: "translation",
1976
+ hooks: ["useTranslation"],
1977
+ instances: ["i18n", "i18next"],
1978
+ functions: ["t"],
1979
+ typeNames: ["TFunction"],
1980
+ transComponents: ["Trans"],
1981
+ nsSeparator: ":",
1982
+ keySeparator: ".",
1983
+ pluralSeparator: "_",
1984
+ contextSeparator: "_"
1985
+ };
1986
+ function translationSettingsOf(options) {
1987
+ return {
1988
+ hooks: new Set(options.hooks ?? TRANSLATION_DEFAULTS.hooks),
1989
+ instances: new Set(options.instances ?? TRANSLATION_DEFAULTS.instances),
1990
+ functions: new Set(options.functions ?? TRANSLATION_DEFAULTS.functions),
1991
+ typeNames: new Set(options.typeNames ?? TRANSLATION_DEFAULTS.typeNames),
1992
+ transComponents: new Set(options.transComponents ?? TRANSLATION_DEFAULTS.transComponents),
1993
+ namespaceIdentifiers: options.namespaceIdentifiers ?? {},
1994
+ defaultNamespace: options.defaultNamespace ?? TRANSLATION_DEFAULTS.defaultNamespace,
1995
+ nsSeparator: options.nsSeparator ?? TRANSLATION_DEFAULTS.nsSeparator,
1996
+ keySeparator: options.keySeparator ?? TRANSLATION_DEFAULTS.keySeparator
1997
+ };
1998
+ }
1999
+ var translationKeyExistsRule = createRule({
2000
+ name: RULE_NAME11,
2001
+ meta: {
2002
+ type: "problem",
2003
+ docs: {
2004
+ description: "Require every static i18next / react-i18next translation key (`t(...)`, `i18n.t(...)`, `<Trans i18nKey>`) to exist in the catalog of the namespace in scope."
2005
+ },
2006
+ schema: [optionSchema9],
2007
+ messages: {
2008
+ missingKey: "Translation key `{{key}}` does not exist in namespace `{{namespace}}` ({{catalogs}}). It renders as the raw key at runtime: fix the key or add it to the catalog.",
2009
+ missingKeyPrefix: "No key in namespace `{{namespace}}` ({{catalogs}}) starts with `{{prefix}}`, so this template key can never resolve.",
2010
+ unknownNamespace: "Namespace `{{namespace}}` has no catalog in the rule configuration. Fix the namespace name or add a `catalogs` entry for it.",
2011
+ catalogUnreadable: "Translation catalog could not be loaded: {{reason}}."
2012
+ }
2013
+ },
2014
+ defaultOptions: [{}],
2015
+ create(context, [options]) {
2016
+ const sources = options.catalogs ?? [];
2017
+ if (sources.length === 0) {
2018
+ return {};
2019
+ }
2020
+ const settings = translationSettingsOf(options);
2021
+ const fallbackNamespaces = options.fallbackNamespaces ?? [];
2022
+ const catalogSettings = {
2023
+ cwd: context.cwd,
2024
+ defaultNamespace: settings.defaultNamespace,
2025
+ keySeparator: settings.keySeparator
2026
+ };
2027
+ const lookupBase = {
2028
+ pluralSeparator: options.pluralSeparator ?? TRANSLATION_DEFAULTS.pluralSeparator,
2029
+ contextSeparator: options.contextSeparator ?? TRANSLATION_DEFAULTS.contextSeparator
2030
+ };
2031
+ const checkPrefix = options.dynamicKeys === "check-prefix";
2032
+ const resolved = /* @__PURE__ */ new Map();
2033
+ const reportedErrors = /* @__PURE__ */ new Set();
2034
+ function catalogsOf(namespace) {
2035
+ let entry = resolved.get(namespace);
2036
+ if (entry === void 0) {
2037
+ entry = catalogsForNamespace(namespace, sources, catalogSettings);
2038
+ resolved.set(namespace, entry);
2039
+ }
2040
+ return entry;
2041
+ }
2042
+ function searched(node, namespaces) {
2043
+ const catalogs = [];
2044
+ for (const namespace of [...namespaces, ...fallbackNamespaces]) {
2045
+ const entry = catalogsOf(namespace);
2046
+ for (const reason of entry.errors) {
2047
+ if (!reportedErrors.has(reason)) {
2048
+ reportedErrors.add(reason);
2049
+ context.report({ node, messageId: "catalogUnreadable", data: { reason } });
2050
+ }
2051
+ }
2052
+ if (entry.errors.length > 0) return null;
2053
+ catalogs.push(...entry.catalogs);
2054
+ }
2055
+ if (catalogs.length === 0) {
2056
+ context.report({ node, messageId: "unknownNamespace", data: { namespace: namespaces.join("`, `") } });
2057
+ return null;
2058
+ }
2059
+ return catalogs;
2060
+ }
2061
+ const labels = (catalogs) => catalogs.map((catalog) => catalog.label).join(", ");
2062
+ return createTranslationVisitor(context, settings, (usage) => {
2063
+ if (usage.kind === "key") {
2064
+ const catalogs = searched(usage.node, usage.namespaces);
2065
+ if (catalogs === null) return;
2066
+ const lookup = { ...lookupBase, plural: usage.plural, context: usage.context, returnObjects: usage.returnObjects };
2067
+ const found = usage.keys.some((key) => catalogs.some((catalog) => catalogHasKey(catalog, key, lookup)));
2068
+ if (!found) {
2069
+ context.report({
2070
+ node: usage.node,
2071
+ messageId: "missingKey",
2072
+ data: { key: usage.keys.join("` | `"), namespace: usage.namespaces.join("`, `"), catalogs: labels(catalogs) }
2073
+ });
2074
+ }
2075
+ return;
2076
+ }
2077
+ if (usage.kind === "prefix" && checkPrefix) {
2078
+ const catalogs = searched(usage.node, usage.namespaces);
2079
+ if (catalogs === null) return;
2080
+ if (!catalogs.some((catalog) => catalogHasPrefix(catalog, usage.prefix))) {
2081
+ context.report({
2082
+ node: usage.node,
2083
+ messageId: "missingKeyPrefix",
2084
+ data: { prefix: usage.prefix, namespace: usage.namespaces.join("`, `"), catalogs: labels(catalogs) }
2085
+ });
2086
+ }
2087
+ }
2088
+ });
2089
+ }
2090
+ });
2091
+
769
2092
  // src/rules/wire-message-naming.ts
770
- var RULE_NAME9 = "wire-message-naming";
2093
+ var RULE_NAME12 = "wire-message-naming";
771
2094
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
772
- var optionSchema7 = {
2095
+ var optionSchema10 = {
773
2096
  type: "object",
774
2097
  additionalProperties: false,
775
2098
  properties: {
@@ -810,14 +2133,14 @@ function typeLiteralNode(obj) {
810
2133
  return null;
811
2134
  }
812
2135
  var wireMessageNamingRule = createRule({
813
- name: RULE_NAME9,
2136
+ name: RULE_NAME12,
814
2137
  meta: {
815
2138
  type: "problem",
816
2139
  docs: {
817
2140
  description: "A message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares `type: z.literal(...)` must set that literal to kebab-case(const name minus its role suffix)."
818
2141
  },
819
2142
  fixable: "code",
820
- schema: [optionSchema7],
2143
+ schema: [optionSchema10],
821
2144
  messages: {
822
2145
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
823
2146
  }
@@ -853,11 +2176,11 @@ var wireMessageNamingRule = createRule({
853
2176
  });
854
2177
 
855
2178
  // src/rules/zod-schema-naming.ts
856
- var RULE_NAME10 = "zod-schema-naming";
2179
+ var RULE_NAME13 = "zod-schema-naming";
857
2180
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
858
2181
  var SUFFIX = "Schema";
859
2182
  var DEFAULT_ROLE_SUFFIXES2 = [];
860
- var optionSchema8 = {
2183
+ var optionSchema11 = {
861
2184
  type: "object",
862
2185
  additionalProperties: false,
863
2186
  properties: {
@@ -890,13 +2213,13 @@ function rootIdentifierName(node) {
890
2213
  return null;
891
2214
  }
892
2215
  var zodSchemaNamingRule = createRule({
893
- name: RULE_NAME10,
2216
+ name: RULE_NAME13,
894
2217
  meta: {
895
2218
  type: "problem",
896
2219
  docs: {
897
2220
  description: "Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type (`export type Foo = z.infer<typeof FooSchema>`)."
898
2221
  },
899
- schema: [optionSchema8],
2222
+ schema: [optionSchema11],
900
2223
  messages: {
901
2224
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
902
2225
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -957,12 +2280,15 @@ var rules = {
957
2280
  "restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
958
2281
  "require-registered-keys": requireRegisteredKeysRule,
959
2282
  "env-var-schema-parity": envVarSchemaParityRule,
960
- "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
2283
+ "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
2284
+ "schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
2285
+ "fetch-must-check-ok": fetchMustCheckOkRule,
2286
+ "translation-key-exists": translationKeyExistsRule
961
2287
  };
962
2288
 
963
2289
  // src/index.ts
964
2290
  var NAMESPACE = "noctcore-contracts";
965
- var VERSION = "0.2.0";
2291
+ var VERSION = "0.3.0";
966
2292
  var plugin = {
967
2293
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
968
2294
  rules,