@noctcore/eslint-plugin-contracts 0.6.1 → 0.7.1

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
@@ -120,7 +120,8 @@ var envVarSchemaParityRule = createRule({
120
120
  meta: {
121
121
  type: "suggestion",
122
122
  docs: {
123
- description: "Require every `process.env.FOO` / `import.meta.env.FOO` key to be declared in a schema file (`.env.example` or a zod-env module), so config access and config declaration cannot drift apart."
123
+ description: "Require every `process.env.FOO` / `import.meta.env.FOO` key to be declared in a schema file (`.env.example` or a zod-env module), so config access and config declaration cannot drift apart.",
124
+ requiresOptions: true
124
125
  },
125
126
  schema: [optionSchema],
126
127
  messages: {
@@ -1087,7 +1088,8 @@ var requireRegisteredKeysRule = createRule({
1087
1088
  meta: {
1088
1089
  type: "suggestion",
1089
1090
  docs: {
1090
- 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."
1091
+ 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.",
1092
+ requiresOptions: true
1091
1093
  },
1092
1094
  schema: [optionSchema6],
1093
1095
  messages: {
@@ -1139,20 +1141,313 @@ var requireRegisteredKeysRule = createRule({
1139
1141
  // src/rules/require-schema-parse-at-boundary.ts
1140
1142
  var import_utils8 = require("@typescript-eslint/utils");
1141
1143
  var RULE_NAME8 = "require-schema-parse-at-boundary";
1144
+ var optionSchema7 = {
1145
+ type: "object",
1146
+ additionalProperties: false,
1147
+ properties: {
1148
+ boundaries: {
1149
+ type: "array",
1150
+ items: { type: "string", minLength: 1 },
1151
+ uniqueItems: true
1152
+ }
1153
+ }
1154
+ };
1155
+ function parentOf(node) {
1156
+ return node.parent ?? null;
1157
+ }
1158
+ function memberPath(node) {
1159
+ if (node.type === import_utils8.AST_NODE_TYPES.Identifier) {
1160
+ return node.name;
1161
+ }
1162
+ if (node.type === import_utils8.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
1163
+ const object = memberPath(node.object);
1164
+ return object === null ? null : `${object}.${node.property.name}`;
1165
+ }
1166
+ return null;
1167
+ }
1168
+ function methodCallReceiver(node, names) {
1169
+ if (node.type === import_utils8.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && names.includes(node.callee.property.name)) {
1170
+ return node.callee.object;
1171
+ }
1172
+ return null;
1173
+ }
1174
+ function unwrap(node) {
1175
+ let current = node;
1176
+ while (current.type === import_utils8.AST_NODE_TYPES.TSNonNullExpression || current.type === import_utils8.AST_NODE_TYPES.ChainExpression) {
1177
+ current = current.expression;
1178
+ }
1179
+ return current;
1180
+ }
1142
1181
  function isJsonParseCall(node) {
1143
- 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";
1182
+ return node.type === import_utils8.AST_NODE_TYPES.CallExpression && memberPath(node.callee) === "JSON.parse";
1144
1183
  }
1145
1184
  function isAwaitJsonCall(node) {
1146
1185
  if (node.type !== import_utils8.AST_NODE_TYPES.AwaitExpression) {
1147
1186
  return false;
1148
1187
  }
1149
1188
  const call = node.argument;
1150
- 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";
1189
+ return methodCallReceiver(call, ["json"]) !== null && call.type === import_utils8.AST_NODE_TYPES.CallExpression && call.arguments.length === 0;
1190
+ }
1191
+ function isStorageRead(node) {
1192
+ const receiver = methodCallReceiver(node, ["getItem"]);
1193
+ const path3 = receiver === null ? null : memberPath(receiver);
1194
+ if (path3 === null) {
1195
+ return false;
1196
+ }
1197
+ const last = path3.slice(path3.lastIndexOf(".") + 1);
1198
+ return last === "localStorage" || last === "sessionStorage";
1199
+ }
1200
+ var BoundaryMatcher = class {
1201
+ sourceCode;
1202
+ boundaries;
1203
+ constructor(sourceCode, boundaries) {
1204
+ this.sourceCode = sourceCode;
1205
+ this.boundaries = new Set(boundaries);
1206
+ }
1207
+ /** True when `expr`, the operand of the cast at `cast`, is boundary data. */
1208
+ isBoundary(expr, cast, seen) {
1209
+ const node = unwrap(expr);
1210
+ if (isJsonParseCall(node) || isAwaitJsonCall(node) || isStorageRead(node) || this.isSearchParamsRead(node) || this.isConfiguredBoundary(node) || this.isMessageEventData(node) || this.isToolUseInput(node)) {
1211
+ return true;
1212
+ }
1213
+ if (node.type !== import_utils8.AST_NODE_TYPES.Identifier) {
1214
+ return false;
1215
+ }
1216
+ const variable = this.resolve(node);
1217
+ if (variable === null || seen.has(variable)) {
1218
+ return false;
1219
+ }
1220
+ seen.add(variable);
1221
+ const init = this.constInit(variable);
1222
+ return init !== null && this.untouchedBefore(variable, node, cast) && this.isBoundary(init, init, seen);
1223
+ }
1224
+ resolve(identifier) {
1225
+ return import_utils8.ASTUtils.findVariable(this.sourceCode.getScope(identifier), identifier) ?? null;
1226
+ }
1227
+ /** The initializer of a single `const name = <init>` declaration, or null. */
1228
+ constInit(variable) {
1229
+ const [def] = variable.defs;
1230
+ if (variable.defs.length !== 1 || def === void 0) {
1231
+ return null;
1232
+ }
1233
+ const declarator = def.node;
1234
+ if (declarator.type !== import_utils8.AST_NODE_TYPES.VariableDeclarator || declarator.id.type !== import_utils8.AST_NODE_TYPES.Identifier || declarator.init === null) {
1235
+ return null;
1236
+ }
1237
+ const declaration = parentOf(declarator);
1238
+ if (declaration?.type !== import_utils8.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") {
1239
+ return null;
1240
+ }
1241
+ return declarator.init;
1242
+ }
1243
+ /**
1244
+ * True when the only reads of `variable` that could run before `cast` are the
1245
+ * `use` itself and other `as` casts, all in the declaring function. Any other
1246
+ * earlier read (a guard, a validator call, a mutation) might have checked the
1247
+ * value, so the cast is given the benefit of the doubt.
1248
+ */
1249
+ untouchedBefore(variable, use, cast) {
1250
+ const home = variable.scope.variableScope;
1251
+ return variable.references.every((reference) => {
1252
+ if (reference.init === true) {
1253
+ return true;
1254
+ }
1255
+ if (reference.from.variableScope !== home) {
1256
+ return false;
1257
+ }
1258
+ const id = reference.identifier;
1259
+ if (id === use || id.range[0] >= cast.range[0]) {
1260
+ return true;
1261
+ }
1262
+ return parentOf(id)?.type === import_utils8.AST_NODE_TYPES.TSAsExpression;
1263
+ });
1264
+ }
1265
+ /** A call whose callee is listed in the `boundaries` option. */
1266
+ isConfiguredBoundary(node) {
1267
+ if (this.boundaries.size === 0) {
1268
+ return false;
1269
+ }
1270
+ const call = node.type === import_utils8.AST_NODE_TYPES.AwaitExpression ? unwrap(node.argument) : node;
1271
+ if (call.type !== import_utils8.AST_NODE_TYPES.CallExpression) {
1272
+ return false;
1273
+ }
1274
+ const path3 = memberPath(call.callee);
1275
+ return path3 !== null && this.boundaries.has(path3);
1276
+ }
1277
+ /** `.get(...)` / `.getAll(...)` on a `URLSearchParams`. */
1278
+ isSearchParamsRead(node) {
1279
+ const receiver = methodCallReceiver(node, ["get", "getAll"]);
1280
+ return receiver !== null && this.isSearchParams(unwrap(receiver), /* @__PURE__ */ new Set());
1281
+ }
1282
+ isSearchParams(node, seen) {
1283
+ if (node.type === import_utils8.AST_NODE_TYPES.NewExpression) {
1284
+ return node.callee.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.name === "URLSearchParams";
1285
+ }
1286
+ if (node.type === import_utils8.AST_NODE_TYPES.MemberExpression) {
1287
+ return !node.computed && node.property.type === import_utils8.AST_NODE_TYPES.Identifier && node.property.name === "searchParams";
1288
+ }
1289
+ if (node.type !== import_utils8.AST_NODE_TYPES.Identifier) {
1290
+ return false;
1291
+ }
1292
+ if (node.name === "searchParams") {
1293
+ return true;
1294
+ }
1295
+ const variable = this.resolve(node);
1296
+ if (variable === null || seen.has(variable)) {
1297
+ return false;
1298
+ }
1299
+ seen.add(variable);
1300
+ const init = this.constInit(variable);
1301
+ return init !== null && this.isSearchParams(unwrap(init), seen);
1302
+ }
1303
+ /** `event.data` (or a `{ data }` destructured parameter) in a `message` listener. */
1304
+ isMessageEventData(node) {
1305
+ let identifier;
1306
+ if (node.type === import_utils8.AST_NODE_TYPES.MemberExpression) {
1307
+ if (node.computed || node.property.type !== import_utils8.AST_NODE_TYPES.Identifier || node.property.name !== "data" || node.object.type !== import_utils8.AST_NODE_TYPES.Identifier) {
1308
+ return false;
1309
+ }
1310
+ identifier = node.object;
1311
+ } else if (node.type === import_utils8.AST_NODE_TYPES.Identifier) {
1312
+ identifier = node;
1313
+ } else {
1314
+ return false;
1315
+ }
1316
+ const variable = this.resolve(identifier);
1317
+ const param = variable === null ? null : parameterOf(variable);
1318
+ if (param === null) {
1319
+ return false;
1320
+ }
1321
+ const { fn, name } = param;
1322
+ const [first] = fn.params;
1323
+ if (first === void 0) {
1324
+ return false;
1325
+ }
1326
+ const isParamItself = name === first;
1327
+ const isDestructuredData = first.type === import_utils8.AST_NODE_TYPES.ObjectPattern && first.properties.some(
1328
+ (property) => property.type === import_utils8.AST_NODE_TYPES.Property && !property.computed && property.key.type === import_utils8.AST_NODE_TYPES.Identifier && property.key.name === "data" && property.value === name
1329
+ );
1330
+ if (node.type === import_utils8.AST_NODE_TYPES.MemberExpression ? !isParamItself : !isDestructuredData) {
1331
+ return false;
1332
+ }
1333
+ return isMessageListener(fn);
1334
+ }
1335
+ /** `block.input` where `block` is narrowed to an Anthropic `tool_use` content block. */
1336
+ isToolUseInput(node) {
1337
+ if (node.type !== import_utils8.AST_NODE_TYPES.MemberExpression || node.computed || node.property.type !== import_utils8.AST_NODE_TYPES.Identifier || node.property.name !== "input") {
1338
+ return false;
1339
+ }
1340
+ const block = unwrap(node.object);
1341
+ const blockText = this.sourceCode.getText(block);
1342
+ if (this.isGuardedAsToolUse(node, blockText)) {
1343
+ return true;
1344
+ }
1345
+ if (block.type !== import_utils8.AST_NODE_TYPES.Identifier) {
1346
+ return false;
1347
+ }
1348
+ const variable = this.resolve(block);
1349
+ if (variable === null) {
1350
+ return false;
1351
+ }
1352
+ const init = this.constInit(variable);
1353
+ if (init !== null) {
1354
+ const receiver = methodCallReceiver(unwrap(init), ["find"]);
1355
+ const call = unwrap(init);
1356
+ return receiver !== null && call.type === import_utils8.AST_NODE_TYPES.CallExpression && this.isToolUsePredicate(call.arguments[0]);
1357
+ }
1358
+ const param = parameterOf(variable);
1359
+ if (param !== null && param.fn.params[0] === param.name) {
1360
+ const call = parentOf(param.fn);
1361
+ if (call?.type !== import_utils8.AST_NODE_TYPES.CallExpression || call.arguments[0] !== param.fn || methodCallReceiver(call, ["map", "flatMap", "forEach"]) === null) {
1362
+ return false;
1363
+ }
1364
+ const filtered = unwrap(call.callee.object);
1365
+ return methodCallReceiver(filtered, ["filter"]) !== null && filtered.type === import_utils8.AST_NODE_TYPES.CallExpression && this.isToolUsePredicate(filtered.arguments[0]);
1366
+ }
1367
+ return false;
1368
+ }
1369
+ /** `(b) => b.type === 'tool_use'`, optionally with a type-predicate return. */
1370
+ isToolUsePredicate(node) {
1371
+ if (node?.type !== import_utils8.AST_NODE_TYPES.ArrowFunctionExpression || node.body.type === import_utils8.AST_NODE_TYPES.BlockStatement) {
1372
+ return false;
1373
+ }
1374
+ const [param] = node.params;
1375
+ return param?.type === import_utils8.AST_NODE_TYPES.Identifier && this.testProvesToolUse(node.body, param.name);
1376
+ }
1377
+ /** An enclosing `if` / `?:` / `&&` / `case 'tool_use':` narrows `blockText`. */
1378
+ isGuardedAsToolUse(from, blockText) {
1379
+ let child = from;
1380
+ for (let parent = parentOf(child); parent !== null; child = parent, parent = parentOf(child)) {
1381
+ switch (parent.type) {
1382
+ case import_utils8.AST_NODE_TYPES.IfStatement:
1383
+ case import_utils8.AST_NODE_TYPES.ConditionalExpression:
1384
+ if (parent.consequent === child && this.testProvesToolUse(parent.test, blockText)) {
1385
+ return true;
1386
+ }
1387
+ break;
1388
+ case import_utils8.AST_NODE_TYPES.LogicalExpression:
1389
+ if (parent.operator === "&&" && parent.right === child && this.testProvesToolUse(parent.left, blockText)) {
1390
+ return true;
1391
+ }
1392
+ break;
1393
+ case import_utils8.AST_NODE_TYPES.SwitchCase: {
1394
+ const statement = parentOf(parent);
1395
+ if (parent.test?.type === import_utils8.AST_NODE_TYPES.Literal && parent.test.value === "tool_use" && statement?.type === import_utils8.AST_NODE_TYPES.SwitchStatement && this.sourceCode.getText(statement.discriminant) === `${blockText}.type`) {
1396
+ return true;
1397
+ }
1398
+ break;
1399
+ }
1400
+ default:
1401
+ break;
1402
+ }
1403
+ }
1404
+ return false;
1405
+ }
1406
+ /** `<blockText>.type === 'tool_use'`, possibly one conjunct of an `&&` chain. */
1407
+ testProvesToolUse(test, blockText) {
1408
+ if (test.type === import_utils8.AST_NODE_TYPES.LogicalExpression && test.operator === "&&") {
1409
+ return this.testProvesToolUse(test.left, blockText) || this.testProvesToolUse(test.right, blockText);
1410
+ }
1411
+ if (test.type !== import_utils8.AST_NODE_TYPES.BinaryExpression || test.operator !== "===" && test.operator !== "==") {
1412
+ return false;
1413
+ }
1414
+ const typeText = `${blockText}.type`;
1415
+ const isToolUse = (node) => node.type === import_utils8.AST_NODE_TYPES.Literal && node.value === "tool_use";
1416
+ const isTypeRead = (node) => this.sourceCode.getText(unwrap(node)) === typeText || this.sourceCode.getText(node) === `${blockText}?.type`;
1417
+ return isTypeRead(test.left) && isToolUse(test.right) || isToolUse(test.left) && isTypeRead(test.right);
1418
+ }
1419
+ };
1420
+ function parameterOf(variable) {
1421
+ const [def] = variable.defs;
1422
+ if (variable.defs.length !== 1 || def === void 0 || def.type !== "Parameter") {
1423
+ return null;
1424
+ }
1425
+ const fn = def.node;
1426
+ if (fn.type !== import_utils8.AST_NODE_TYPES.ArrowFunctionExpression && fn.type !== import_utils8.AST_NODE_TYPES.FunctionDeclaration && fn.type !== import_utils8.AST_NODE_TYPES.FunctionExpression) {
1427
+ return null;
1428
+ }
1429
+ return { fn, name: def.name };
1430
+ }
1431
+ function isMessageListener(fn) {
1432
+ const parent = parentOf(fn);
1433
+ if (parent?.type === import_utils8.AST_NODE_TYPES.CallExpression) {
1434
+ const [event, listener] = parent.arguments;
1435
+ const callee = memberPath(parent.callee);
1436
+ return listener === fn && event?.type === import_utils8.AST_NODE_TYPES.Literal && event.value === "message" && callee !== null && (callee === "addEventListener" || callee.endsWith(".addEventListener"));
1437
+ }
1438
+ if (parent?.type === import_utils8.AST_NODE_TYPES.AssignmentExpression && parent.right === fn) {
1439
+ const target = memberPath(parent.left);
1440
+ return target !== null && (target === "onmessage" || target.endsWith(".onmessage"));
1441
+ }
1442
+ return false;
1151
1443
  }
1152
1444
  function isShapeClaim(annotation) {
1153
1445
  if (annotation.type === import_utils8.AST_NODE_TYPES.TSArrayType) {
1154
1446
  return true;
1155
1447
  }
1448
+ if (annotation.type === import_utils8.AST_NODE_TYPES.TSUnionType || annotation.type === import_utils8.AST_NODE_TYPES.TSIntersectionType) {
1449
+ return annotation.types.some(isShapeClaim);
1450
+ }
1156
1451
  if (annotation.type === import_utils8.AST_NODE_TYPES.TSTypeReference) {
1157
1452
  return !(annotation.typeName.type === import_utils8.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
1158
1453
  }
@@ -1163,22 +1458,22 @@ var requireSchemaParseAtBoundaryRule = createRule({
1163
1458
  meta: {
1164
1459
  type: "problem",
1165
1460
  docs: {
1166
- description: "Disallow asserting external boundary data with `as T` instead of parsing it at runtime. Flags `JSON.parse(...) as T` and `(await res.json()) as T`; use a zod/valibot parse."
1461
+ description: "Disallow asserting external boundary data with `as T` instead of parsing it at runtime. Flags casts of `JSON.parse`, `res.json()`, web storage, URL search params, message-event data and LLM tool input, directly or through a `const`; use a zod/valibot parse."
1167
1462
  },
1168
- schema: [],
1463
+ schema: [optionSchema7],
1169
1464
  messages: {
1170
1465
  castedBoundaryData: "Boundary data is asserted with `as` here, not parsed. A cast is unchecked \u2014 validate this with a runtime schema (e.g. `Schema.parse(...)`) so a wire-shape change fails loudly."
1171
1466
  }
1172
1467
  },
1173
- defaultOptions: [],
1174
- create(context) {
1468
+ defaultOptions: [{}],
1469
+ create(context, [options]) {
1470
+ const matcher = new BoundaryMatcher(context.sourceCode, options.boundaries ?? []);
1175
1471
  return {
1176
1472
  TSAsExpression(node) {
1177
1473
  if (!isShapeClaim(node.typeAnnotation)) {
1178
1474
  return;
1179
1475
  }
1180
- const expr = node.expression;
1181
- if (isJsonParseCall(expr) || isAwaitJsonCall(expr)) {
1476
+ if (matcher.isBoundary(node.expression, node, /* @__PURE__ */ new Set())) {
1182
1477
  context.report({ node, messageId: "castedBoundaryData" });
1183
1478
  }
1184
1479
  }
@@ -1190,7 +1485,7 @@ var requireSchemaParseAtBoundaryRule = createRule({
1190
1485
  var import_utils9 = require("@typescript-eslint/utils");
1191
1486
  var RULE_NAME9 = "restrict-throw-to-taxonomy";
1192
1487
  var DEFAULT_ALLOW = ["Error"];
1193
- var optionSchema7 = {
1488
+ var optionSchema8 = {
1194
1489
  type: "object",
1195
1490
  additionalProperties: false,
1196
1491
  properties: {
@@ -1221,7 +1516,7 @@ var restrictThrowToTaxonomyRule = createRule({
1221
1516
  docs: {
1222
1517
  description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
1223
1518
  },
1224
- schema: [optionSchema7],
1519
+ schema: [optionSchema8],
1225
1520
  messages: {
1226
1521
  disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
1227
1522
  nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
@@ -1276,7 +1571,7 @@ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1276
1571
  var UNION = /* @__PURE__ */ new Set(["union"]);
1277
1572
  var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1278
1573
  var STRING = /* @__PURE__ */ new Set(["string"]);
1279
- var optionSchema8 = {
1574
+ var optionSchema9 = {
1280
1575
  type: "object",
1281
1576
  additionalProperties: false,
1282
1577
  properties: {
@@ -1311,7 +1606,7 @@ var schemaEnumFieldConsistencyRule = createRule({
1311
1606
  docs: {
1312
1607
  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."
1313
1608
  },
1314
- schema: [optionSchema8],
1609
+ schema: [optionSchema9],
1315
1610
  messages: {
1316
1611
  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)."
1317
1612
  }
@@ -1566,7 +1861,7 @@ function catalogHasPrefix(catalog, prefix) {
1566
1861
  var import_utils11 = require("@typescript-eslint/utils");
1567
1862
  var UNRESOLVED = "unresolved";
1568
1863
  var MAX_DEPTH = 8;
1569
- function unwrap(node) {
1864
+ function unwrap2(node) {
1570
1865
  let current = node;
1571
1866
  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) {
1572
1867
  current = current.expression;
@@ -1574,7 +1869,7 @@ function unwrap(node) {
1574
1869
  return current;
1575
1870
  }
1576
1871
  function staticString(node) {
1577
- const inner = unwrap(node);
1872
+ const inner = unwrap2(node);
1578
1873
  if (inner.type === import_utils11.AST_NODE_TYPES.Literal && typeof inner.value === "string") return inner.value;
1579
1874
  if (inner.type === import_utils11.AST_NODE_TYPES.TemplateLiteral && inner.expressions.length === 0) {
1580
1875
  return inner.quasis[0]?.value.cooked ?? null;
@@ -1615,7 +1910,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1615
1910
  }
1616
1911
  function resolveNamespaces(node, depth = 0) {
1617
1912
  if (node === void 0) return defaultBinding.namespaces;
1618
- const inner = unwrap(node);
1913
+ const inner = unwrap2(node);
1619
1914
  const literal = staticString(inner);
1620
1915
  if (literal !== null) return [literal];
1621
1916
  if (inner.type === import_utils11.AST_NODE_TYPES.Literal && inner.value === null) return defaultBinding.namespaces;
@@ -1640,7 +1935,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1640
1935
  }
1641
1936
  const definition = resolveVariable(node)?.defs[0];
1642
1937
  if (definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.id.type === import_utils11.AST_NODE_TYPES.Identifier && definition.node.init !== null) {
1643
- const init = unwrap(definition.node.init);
1938
+ const init = unwrap2(definition.node.init);
1644
1939
  const literal = staticString(init);
1645
1940
  if (literal !== null) return literal;
1646
1941
  const chained = resolveIdentifierString(init, depth + 1);
@@ -1656,7 +1951,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1656
1951
  }
1657
1952
  function staticPrefix(node) {
1658
1953
  if (node === void 0) return null;
1659
- const inner = unwrap(node);
1954
+ const inner = unwrap2(node);
1660
1955
  if (inner.type === import_utils11.AST_NODE_TYPES.Identifier && inner.name === "undefined") return null;
1661
1956
  if (inner.type === import_utils11.AST_NODE_TYPES.Literal && inner.value === null) return null;
1662
1957
  return staticString(inner) ?? UNRESOLVED;
@@ -1667,7 +1962,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1667
1962
  if (namespaces === UNRESOLVED) return UNRESOLVED;
1668
1963
  let keyPrefix = null;
1669
1964
  if (optionsArg !== void 0) {
1670
- const options = unwrap(optionsArg);
1965
+ const options = unwrap2(optionsArg);
1671
1966
  if (options.type !== import_utils11.AST_NODE_TYPES.ObjectExpression) return UNRESOLVED;
1672
1967
  for (const property of options.properties) {
1673
1968
  if (property.type !== import_utils11.AST_NODE_TYPES.Property) return UNRESOLVED;
@@ -1693,11 +1988,11 @@ function createTranslationVisitor(context, settings, onUsage) {
1693
1988
  function hookCallOf(identifier) {
1694
1989
  const definition = resolveVariable(identifier)?.defs[0];
1695
1990
  if (definition?.type !== "Variable" || definition.node.id.type !== import_utils11.AST_NODE_TYPES.Identifier) return null;
1696
- const init = definition.node.init === null ? null : unwrap(definition.node.init);
1991
+ const init = definition.node.init === null ? null : unwrap2(definition.node.init);
1697
1992
  return init !== null && isHookCall(init) ? init : null;
1698
1993
  }
1699
1994
  function bindingOfTSource(object) {
1700
- const inner = unwrap(object);
1995
+ const inner = unwrap2(object);
1701
1996
  if (isHookCall(inner)) return bindingFromHook(inner);
1702
1997
  if (inner.type === import_utils11.AST_NODE_TYPES.Identifier) {
1703
1998
  const hook = hookCallOf(inner);
@@ -1734,7 +2029,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1734
2029
  }
1735
2030
  function bindingFromDeclarator(declarator, name, depth) {
1736
2031
  if (declarator.init === null) return null;
1737
- const init = unwrap(declarator.init);
2032
+ const init = unwrap2(declarator.init);
1738
2033
  const id = declarator.id;
1739
2034
  if (id.type === import_utils11.AST_NODE_TYPES.Identifier) {
1740
2035
  if (isGetFixedT(init)) return bindingFromGetFixedT(init);
@@ -1795,7 +2090,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1795
2090
  function readCallOptions(node) {
1796
2091
  const none = { namespaces: null, plural: false, context: false, returnObjects: false };
1797
2092
  if (node === void 0) return none;
1798
- const inner = unwrap(node);
2093
+ const inner = unwrap2(node);
1799
2094
  if (inner.type !== import_utils11.AST_NODE_TYPES.ObjectExpression) return UNRESOLVED;
1800
2095
  let namespaces = null;
1801
2096
  let plural = false;
@@ -1814,7 +2109,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1814
2109
  } else if (name === "context") {
1815
2110
  context2 = true;
1816
2111
  } else if (name === "returnObjects") {
1817
- const value = unwrap(property.value);
2112
+ const value = unwrap2(property.value);
1818
2113
  returnObjects = !(value.type === import_utils11.AST_NODE_TYPES.Literal && value.value === false);
1819
2114
  }
1820
2115
  }
@@ -1834,7 +2129,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1834
2129
  return { namespaces, key: `${binding.keyPrefix}${keySeparator === false ? "" : keySeparator}${raw}` };
1835
2130
  }
1836
2131
  function emit(node, keyNode, binding, options) {
1837
- const inner = unwrap(keyNode);
2132
+ const inner = unwrap2(keyNode);
1838
2133
  const raws = [];
1839
2134
  const single = staticString(inner);
1840
2135
  if (single !== null) {
@@ -1957,7 +2252,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1957
2252
  var RULE_NAME11 = "translation-key-exists";
1958
2253
  var stringList = { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true };
1959
2254
  var separator = { oneOf: [{ type: "string", minLength: 1 }, { type: "boolean", enum: [false] }] };
1960
- var optionSchema9 = {
2255
+ var optionSchema10 = {
1961
2256
  type: "object",
1962
2257
  additionalProperties: false,
1963
2258
  properties: {
@@ -2019,9 +2314,10 @@ var translationKeyExistsRule = createRule({
2019
2314
  meta: {
2020
2315
  type: "problem",
2021
2316
  docs: {
2022
- description: "Require every static i18next / react-i18next translation key (`t(...)`, `i18n.t(...)`, `<Trans i18nKey>`) to exist in the catalog of the namespace in scope."
2317
+ description: "Require every static i18next / react-i18next translation key (`t(...)`, `i18n.t(...)`, `<Trans i18nKey>`) to exist in the catalog of the namespace in scope.",
2318
+ requiresOptions: true
2023
2319
  },
2024
- schema: [optionSchema9],
2320
+ schema: [optionSchema10],
2025
2321
  messages: {
2026
2322
  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.",
2027
2323
  missingKeyPrefix: "No key in namespace `{{namespace}}` ({{catalogs}}) starts with `{{prefix}}`, so this template key can never resolve.",
@@ -2110,7 +2406,7 @@ var translationKeyExistsRule = createRule({
2110
2406
  // src/rules/wire-message-naming.ts
2111
2407
  var RULE_NAME12 = "wire-message-naming";
2112
2408
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
2113
- var optionSchema10 = {
2409
+ var optionSchema11 = {
2114
2410
  type: "object",
2115
2411
  additionalProperties: false,
2116
2412
  properties: {
@@ -2158,7 +2454,7 @@ var wireMessageNamingRule = createRule({
2158
2454
  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)."
2159
2455
  },
2160
2456
  fixable: "code",
2161
- schema: [optionSchema10],
2457
+ schema: [optionSchema11],
2162
2458
  messages: {
2163
2459
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
2164
2460
  }
@@ -2198,7 +2494,7 @@ var RULE_NAME13 = "zod-schema-naming";
2198
2494
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
2199
2495
  var SUFFIX = "Schema";
2200
2496
  var DEFAULT_ROLE_SUFFIXES2 = [];
2201
- var optionSchema11 = {
2497
+ var optionSchema12 = {
2202
2498
  type: "object",
2203
2499
  additionalProperties: false,
2204
2500
  properties: {
@@ -2237,7 +2533,7 @@ var zodSchemaNamingRule = createRule({
2237
2533
  docs: {
2238
2534
  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>`)."
2239
2535
  },
2240
- schema: [optionSchema11],
2536
+ schema: [optionSchema12],
2241
2537
  messages: {
2242
2538
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
2243
2539
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -2306,7 +2602,7 @@ var rules = {
2306
2602
 
2307
2603
  // src/index.ts
2308
2604
  var NAMESPACE = "noctcore-contracts";
2309
- var VERSION = "0.6.1";
2605
+ var VERSION = "0.7.1";
2310
2606
  var plugin = {
2311
2607
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
2312
2608
  rules,