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