@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.js CHANGED
@@ -76,7 +76,8 @@ var envVarSchemaParityRule = createRule({
76
76
  meta: {
77
77
  type: "suggestion",
78
78
  docs: {
79
- 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."
79
+ 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.",
80
+ requiresOptions: true
80
81
  },
81
82
  schema: [optionSchema],
82
83
  messages: {
@@ -1043,7 +1044,8 @@ var requireRegisteredKeysRule = createRule({
1043
1044
  meta: {
1044
1045
  type: "suggestion",
1045
1046
  docs: {
1046
- 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."
1047
+ 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.",
1048
+ requiresOptions: true
1047
1049
  },
1048
1050
  schema: [optionSchema6],
1049
1051
  messages: {
@@ -1093,22 +1095,315 @@ var requireRegisteredKeysRule = createRule({
1093
1095
  });
1094
1096
 
1095
1097
  // src/rules/require-schema-parse-at-boundary.ts
1096
- import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
1098
+ import { AST_NODE_TYPES as AST_NODE_TYPES8, ASTUtils } from "@typescript-eslint/utils";
1097
1099
  var RULE_NAME8 = "require-schema-parse-at-boundary";
1100
+ var optionSchema7 = {
1101
+ type: "object",
1102
+ additionalProperties: false,
1103
+ properties: {
1104
+ boundaries: {
1105
+ type: "array",
1106
+ items: { type: "string", minLength: 1 },
1107
+ uniqueItems: true
1108
+ }
1109
+ }
1110
+ };
1111
+ function parentOf(node) {
1112
+ return node.parent ?? null;
1113
+ }
1114
+ function memberPath(node) {
1115
+ if (node.type === AST_NODE_TYPES8.Identifier) {
1116
+ return node.name;
1117
+ }
1118
+ if (node.type === AST_NODE_TYPES8.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES8.Identifier) {
1119
+ const object = memberPath(node.object);
1120
+ return object === null ? null : `${object}.${node.property.name}`;
1121
+ }
1122
+ return null;
1123
+ }
1124
+ function methodCallReceiver(node, names) {
1125
+ if (node.type === AST_NODE_TYPES8.CallExpression && node.callee.type === AST_NODE_TYPES8.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES8.Identifier && names.includes(node.callee.property.name)) {
1126
+ return node.callee.object;
1127
+ }
1128
+ return null;
1129
+ }
1130
+ function unwrap(node) {
1131
+ let current = node;
1132
+ while (current.type === AST_NODE_TYPES8.TSNonNullExpression || current.type === AST_NODE_TYPES8.ChainExpression) {
1133
+ current = current.expression;
1134
+ }
1135
+ return current;
1136
+ }
1098
1137
  function isJsonParseCall(node) {
1099
- 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";
1138
+ return node.type === AST_NODE_TYPES8.CallExpression && memberPath(node.callee) === "JSON.parse";
1100
1139
  }
1101
1140
  function isAwaitJsonCall(node) {
1102
1141
  if (node.type !== AST_NODE_TYPES8.AwaitExpression) {
1103
1142
  return false;
1104
1143
  }
1105
1144
  const call = node.argument;
1106
- 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";
1145
+ return methodCallReceiver(call, ["json"]) !== null && call.type === AST_NODE_TYPES8.CallExpression && call.arguments.length === 0;
1146
+ }
1147
+ function isStorageRead(node) {
1148
+ const receiver = methodCallReceiver(node, ["getItem"]);
1149
+ const path3 = receiver === null ? null : memberPath(receiver);
1150
+ if (path3 === null) {
1151
+ return false;
1152
+ }
1153
+ const last = path3.slice(path3.lastIndexOf(".") + 1);
1154
+ return last === "localStorage" || last === "sessionStorage";
1155
+ }
1156
+ var BoundaryMatcher = class {
1157
+ sourceCode;
1158
+ boundaries;
1159
+ constructor(sourceCode, boundaries) {
1160
+ this.sourceCode = sourceCode;
1161
+ this.boundaries = new Set(boundaries);
1162
+ }
1163
+ /** True when `expr`, the operand of the cast at `cast`, is boundary data. */
1164
+ isBoundary(expr, cast, seen) {
1165
+ const node = unwrap(expr);
1166
+ if (isJsonParseCall(node) || isAwaitJsonCall(node) || isStorageRead(node) || this.isSearchParamsRead(node) || this.isConfiguredBoundary(node) || this.isMessageEventData(node) || this.isToolUseInput(node)) {
1167
+ return true;
1168
+ }
1169
+ if (node.type !== AST_NODE_TYPES8.Identifier) {
1170
+ return false;
1171
+ }
1172
+ const variable = this.resolve(node);
1173
+ if (variable === null || seen.has(variable)) {
1174
+ return false;
1175
+ }
1176
+ seen.add(variable);
1177
+ const init = this.constInit(variable);
1178
+ return init !== null && this.untouchedBefore(variable, node, cast) && this.isBoundary(init, init, seen);
1179
+ }
1180
+ resolve(identifier) {
1181
+ return ASTUtils.findVariable(this.sourceCode.getScope(identifier), identifier) ?? null;
1182
+ }
1183
+ /** The initializer of a single `const name = <init>` declaration, or null. */
1184
+ constInit(variable) {
1185
+ const [def] = variable.defs;
1186
+ if (variable.defs.length !== 1 || def === void 0) {
1187
+ return null;
1188
+ }
1189
+ const declarator = def.node;
1190
+ if (declarator.type !== AST_NODE_TYPES8.VariableDeclarator || declarator.id.type !== AST_NODE_TYPES8.Identifier || declarator.init === null) {
1191
+ return null;
1192
+ }
1193
+ const declaration = parentOf(declarator);
1194
+ if (declaration?.type !== AST_NODE_TYPES8.VariableDeclaration || declaration.kind !== "const") {
1195
+ return null;
1196
+ }
1197
+ return declarator.init;
1198
+ }
1199
+ /**
1200
+ * True when the only reads of `variable` that could run before `cast` are the
1201
+ * `use` itself and other `as` casts, all in the declaring function. Any other
1202
+ * earlier read (a guard, a validator call, a mutation) might have checked the
1203
+ * value, so the cast is given the benefit of the doubt.
1204
+ */
1205
+ untouchedBefore(variable, use, cast) {
1206
+ const home = variable.scope.variableScope;
1207
+ return variable.references.every((reference) => {
1208
+ if (reference.init === true) {
1209
+ return true;
1210
+ }
1211
+ if (reference.from.variableScope !== home) {
1212
+ return false;
1213
+ }
1214
+ const id = reference.identifier;
1215
+ if (id === use || id.range[0] >= cast.range[0]) {
1216
+ return true;
1217
+ }
1218
+ return parentOf(id)?.type === AST_NODE_TYPES8.TSAsExpression;
1219
+ });
1220
+ }
1221
+ /** A call whose callee is listed in the `boundaries` option. */
1222
+ isConfiguredBoundary(node) {
1223
+ if (this.boundaries.size === 0) {
1224
+ return false;
1225
+ }
1226
+ const call = node.type === AST_NODE_TYPES8.AwaitExpression ? unwrap(node.argument) : node;
1227
+ if (call.type !== AST_NODE_TYPES8.CallExpression) {
1228
+ return false;
1229
+ }
1230
+ const path3 = memberPath(call.callee);
1231
+ return path3 !== null && this.boundaries.has(path3);
1232
+ }
1233
+ /** `.get(...)` / `.getAll(...)` on a `URLSearchParams`. */
1234
+ isSearchParamsRead(node) {
1235
+ const receiver = methodCallReceiver(node, ["get", "getAll"]);
1236
+ return receiver !== null && this.isSearchParams(unwrap(receiver), /* @__PURE__ */ new Set());
1237
+ }
1238
+ isSearchParams(node, seen) {
1239
+ if (node.type === AST_NODE_TYPES8.NewExpression) {
1240
+ return node.callee.type === AST_NODE_TYPES8.Identifier && node.callee.name === "URLSearchParams";
1241
+ }
1242
+ if (node.type === AST_NODE_TYPES8.MemberExpression) {
1243
+ return !node.computed && node.property.type === AST_NODE_TYPES8.Identifier && node.property.name === "searchParams";
1244
+ }
1245
+ if (node.type !== AST_NODE_TYPES8.Identifier) {
1246
+ return false;
1247
+ }
1248
+ if (node.name === "searchParams") {
1249
+ return true;
1250
+ }
1251
+ const variable = this.resolve(node);
1252
+ if (variable === null || seen.has(variable)) {
1253
+ return false;
1254
+ }
1255
+ seen.add(variable);
1256
+ const init = this.constInit(variable);
1257
+ return init !== null && this.isSearchParams(unwrap(init), seen);
1258
+ }
1259
+ /** `event.data` (or a `{ data }` destructured parameter) in a `message` listener. */
1260
+ isMessageEventData(node) {
1261
+ let identifier;
1262
+ if (node.type === AST_NODE_TYPES8.MemberExpression) {
1263
+ if (node.computed || node.property.type !== AST_NODE_TYPES8.Identifier || node.property.name !== "data" || node.object.type !== AST_NODE_TYPES8.Identifier) {
1264
+ return false;
1265
+ }
1266
+ identifier = node.object;
1267
+ } else if (node.type === AST_NODE_TYPES8.Identifier) {
1268
+ identifier = node;
1269
+ } else {
1270
+ return false;
1271
+ }
1272
+ const variable = this.resolve(identifier);
1273
+ const param = variable === null ? null : parameterOf(variable);
1274
+ if (param === null) {
1275
+ return false;
1276
+ }
1277
+ const { fn, name } = param;
1278
+ const [first] = fn.params;
1279
+ if (first === void 0) {
1280
+ return false;
1281
+ }
1282
+ const isParamItself = name === first;
1283
+ const isDestructuredData = first.type === AST_NODE_TYPES8.ObjectPattern && first.properties.some(
1284
+ (property) => property.type === AST_NODE_TYPES8.Property && !property.computed && property.key.type === AST_NODE_TYPES8.Identifier && property.key.name === "data" && property.value === name
1285
+ );
1286
+ if (node.type === AST_NODE_TYPES8.MemberExpression ? !isParamItself : !isDestructuredData) {
1287
+ return false;
1288
+ }
1289
+ return isMessageListener(fn);
1290
+ }
1291
+ /** `block.input` where `block` is narrowed to an Anthropic `tool_use` content block. */
1292
+ isToolUseInput(node) {
1293
+ if (node.type !== AST_NODE_TYPES8.MemberExpression || node.computed || node.property.type !== AST_NODE_TYPES8.Identifier || node.property.name !== "input") {
1294
+ return false;
1295
+ }
1296
+ const block = unwrap(node.object);
1297
+ const blockText = this.sourceCode.getText(block);
1298
+ if (this.isGuardedAsToolUse(node, blockText)) {
1299
+ return true;
1300
+ }
1301
+ if (block.type !== AST_NODE_TYPES8.Identifier) {
1302
+ return false;
1303
+ }
1304
+ const variable = this.resolve(block);
1305
+ if (variable === null) {
1306
+ return false;
1307
+ }
1308
+ const init = this.constInit(variable);
1309
+ if (init !== null) {
1310
+ const receiver = methodCallReceiver(unwrap(init), ["find"]);
1311
+ const call = unwrap(init);
1312
+ return receiver !== null && call.type === AST_NODE_TYPES8.CallExpression && this.isToolUsePredicate(call.arguments[0]);
1313
+ }
1314
+ const param = parameterOf(variable);
1315
+ if (param !== null && param.fn.params[0] === param.name) {
1316
+ const call = parentOf(param.fn);
1317
+ if (call?.type !== AST_NODE_TYPES8.CallExpression || call.arguments[0] !== param.fn || methodCallReceiver(call, ["map", "flatMap", "forEach"]) === null) {
1318
+ return false;
1319
+ }
1320
+ const filtered = unwrap(call.callee.object);
1321
+ return methodCallReceiver(filtered, ["filter"]) !== null && filtered.type === AST_NODE_TYPES8.CallExpression && this.isToolUsePredicate(filtered.arguments[0]);
1322
+ }
1323
+ return false;
1324
+ }
1325
+ /** `(b) => b.type === 'tool_use'`, optionally with a type-predicate return. */
1326
+ isToolUsePredicate(node) {
1327
+ if (node?.type !== AST_NODE_TYPES8.ArrowFunctionExpression || node.body.type === AST_NODE_TYPES8.BlockStatement) {
1328
+ return false;
1329
+ }
1330
+ const [param] = node.params;
1331
+ return param?.type === AST_NODE_TYPES8.Identifier && this.testProvesToolUse(node.body, param.name);
1332
+ }
1333
+ /** An enclosing `if` / `?:` / `&&` / `case 'tool_use':` narrows `blockText`. */
1334
+ isGuardedAsToolUse(from, blockText) {
1335
+ let child = from;
1336
+ for (let parent = parentOf(child); parent !== null; child = parent, parent = parentOf(child)) {
1337
+ switch (parent.type) {
1338
+ case AST_NODE_TYPES8.IfStatement:
1339
+ case AST_NODE_TYPES8.ConditionalExpression:
1340
+ if (parent.consequent === child && this.testProvesToolUse(parent.test, blockText)) {
1341
+ return true;
1342
+ }
1343
+ break;
1344
+ case AST_NODE_TYPES8.LogicalExpression:
1345
+ if (parent.operator === "&&" && parent.right === child && this.testProvesToolUse(parent.left, blockText)) {
1346
+ return true;
1347
+ }
1348
+ break;
1349
+ case AST_NODE_TYPES8.SwitchCase: {
1350
+ const statement = parentOf(parent);
1351
+ if (parent.test?.type === AST_NODE_TYPES8.Literal && parent.test.value === "tool_use" && statement?.type === AST_NODE_TYPES8.SwitchStatement && this.sourceCode.getText(statement.discriminant) === `${blockText}.type`) {
1352
+ return true;
1353
+ }
1354
+ break;
1355
+ }
1356
+ default:
1357
+ break;
1358
+ }
1359
+ }
1360
+ return false;
1361
+ }
1362
+ /** `<blockText>.type === 'tool_use'`, possibly one conjunct of an `&&` chain. */
1363
+ testProvesToolUse(test, blockText) {
1364
+ if (test.type === AST_NODE_TYPES8.LogicalExpression && test.operator === "&&") {
1365
+ return this.testProvesToolUse(test.left, blockText) || this.testProvesToolUse(test.right, blockText);
1366
+ }
1367
+ if (test.type !== AST_NODE_TYPES8.BinaryExpression || test.operator !== "===" && test.operator !== "==") {
1368
+ return false;
1369
+ }
1370
+ const typeText = `${blockText}.type`;
1371
+ const isToolUse = (node) => node.type === AST_NODE_TYPES8.Literal && node.value === "tool_use";
1372
+ const isTypeRead = (node) => this.sourceCode.getText(unwrap(node)) === typeText || this.sourceCode.getText(node) === `${blockText}?.type`;
1373
+ return isTypeRead(test.left) && isToolUse(test.right) || isToolUse(test.left) && isTypeRead(test.right);
1374
+ }
1375
+ };
1376
+ function parameterOf(variable) {
1377
+ const [def] = variable.defs;
1378
+ if (variable.defs.length !== 1 || def === void 0 || def.type !== "Parameter") {
1379
+ return null;
1380
+ }
1381
+ const fn = def.node;
1382
+ if (fn.type !== AST_NODE_TYPES8.ArrowFunctionExpression && fn.type !== AST_NODE_TYPES8.FunctionDeclaration && fn.type !== AST_NODE_TYPES8.FunctionExpression) {
1383
+ return null;
1384
+ }
1385
+ return { fn, name: def.name };
1386
+ }
1387
+ function isMessageListener(fn) {
1388
+ const parent = parentOf(fn);
1389
+ if (parent?.type === AST_NODE_TYPES8.CallExpression) {
1390
+ const [event, listener] = parent.arguments;
1391
+ const callee = memberPath(parent.callee);
1392
+ return listener === fn && event?.type === AST_NODE_TYPES8.Literal && event.value === "message" && callee !== null && (callee === "addEventListener" || callee.endsWith(".addEventListener"));
1393
+ }
1394
+ if (parent?.type === AST_NODE_TYPES8.AssignmentExpression && parent.right === fn) {
1395
+ const target = memberPath(parent.left);
1396
+ return target !== null && (target === "onmessage" || target.endsWith(".onmessage"));
1397
+ }
1398
+ return false;
1107
1399
  }
1108
1400
  function isShapeClaim(annotation) {
1109
1401
  if (annotation.type === AST_NODE_TYPES8.TSArrayType) {
1110
1402
  return true;
1111
1403
  }
1404
+ if (annotation.type === AST_NODE_TYPES8.TSUnionType || annotation.type === AST_NODE_TYPES8.TSIntersectionType) {
1405
+ return annotation.types.some(isShapeClaim);
1406
+ }
1112
1407
  if (annotation.type === AST_NODE_TYPES8.TSTypeReference) {
1113
1408
  return !(annotation.typeName.type === AST_NODE_TYPES8.Identifier && annotation.typeName.name === "const");
1114
1409
  }
@@ -1119,22 +1414,22 @@ var requireSchemaParseAtBoundaryRule = createRule({
1119
1414
  meta: {
1120
1415
  type: "problem",
1121
1416
  docs: {
1122
- 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."
1417
+ 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."
1123
1418
  },
1124
- schema: [],
1419
+ schema: [optionSchema7],
1125
1420
  messages: {
1126
1421
  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."
1127
1422
  }
1128
1423
  },
1129
- defaultOptions: [],
1130
- create(context) {
1424
+ defaultOptions: [{}],
1425
+ create(context, [options]) {
1426
+ const matcher = new BoundaryMatcher(context.sourceCode, options.boundaries ?? []);
1131
1427
  return {
1132
1428
  TSAsExpression(node) {
1133
1429
  if (!isShapeClaim(node.typeAnnotation)) {
1134
1430
  return;
1135
1431
  }
1136
- const expr = node.expression;
1137
- if (isJsonParseCall(expr) || isAwaitJsonCall(expr)) {
1432
+ if (matcher.isBoundary(node.expression, node, /* @__PURE__ */ new Set())) {
1138
1433
  context.report({ node, messageId: "castedBoundaryData" });
1139
1434
  }
1140
1435
  }
@@ -1146,7 +1441,7 @@ var requireSchemaParseAtBoundaryRule = createRule({
1146
1441
  import { AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
1147
1442
  var RULE_NAME9 = "restrict-throw-to-taxonomy";
1148
1443
  var DEFAULT_ALLOW = ["Error"];
1149
- var optionSchema7 = {
1444
+ var optionSchema8 = {
1150
1445
  type: "object",
1151
1446
  additionalProperties: false,
1152
1447
  properties: {
@@ -1177,7 +1472,7 @@ var restrictThrowToTaxonomyRule = createRule({
1177
1472
  docs: {
1178
1473
  description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
1179
1474
  },
1180
- schema: [optionSchema7],
1475
+ schema: [optionSchema8],
1181
1476
  messages: {
1182
1477
  disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
1183
1478
  nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
@@ -1232,7 +1527,7 @@ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1232
1527
  var UNION = /* @__PURE__ */ new Set(["union"]);
1233
1528
  var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1234
1529
  var STRING = /* @__PURE__ */ new Set(["string"]);
1235
- var optionSchema8 = {
1530
+ var optionSchema9 = {
1236
1531
  type: "object",
1237
1532
  additionalProperties: false,
1238
1533
  properties: {
@@ -1267,7 +1562,7 @@ var schemaEnumFieldConsistencyRule = createRule({
1267
1562
  docs: {
1268
1563
  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."
1269
1564
  },
1270
- schema: [optionSchema8],
1565
+ schema: [optionSchema9],
1271
1566
  messages: {
1272
1567
  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)."
1273
1568
  }
@@ -1522,7 +1817,7 @@ function catalogHasPrefix(catalog, prefix) {
1522
1817
  import { AST_NODE_TYPES as AST_NODE_TYPES11 } from "@typescript-eslint/utils";
1523
1818
  var UNRESOLVED = "unresolved";
1524
1819
  var MAX_DEPTH = 8;
1525
- function unwrap(node) {
1820
+ function unwrap2(node) {
1526
1821
  let current = node;
1527
1822
  while (current.type === AST_NODE_TYPES11.TSAsExpression || current.type === AST_NODE_TYPES11.TSSatisfiesExpression || current.type === AST_NODE_TYPES11.TSNonNullExpression) {
1528
1823
  current = current.expression;
@@ -1530,7 +1825,7 @@ function unwrap(node) {
1530
1825
  return current;
1531
1826
  }
1532
1827
  function staticString(node) {
1533
- const inner = unwrap(node);
1828
+ const inner = unwrap2(node);
1534
1829
  if (inner.type === AST_NODE_TYPES11.Literal && typeof inner.value === "string") return inner.value;
1535
1830
  if (inner.type === AST_NODE_TYPES11.TemplateLiteral && inner.expressions.length === 0) {
1536
1831
  return inner.quasis[0]?.value.cooked ?? null;
@@ -1571,7 +1866,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1571
1866
  }
1572
1867
  function resolveNamespaces(node, depth = 0) {
1573
1868
  if (node === void 0) return defaultBinding.namespaces;
1574
- const inner = unwrap(node);
1869
+ const inner = unwrap2(node);
1575
1870
  const literal = staticString(inner);
1576
1871
  if (literal !== null) return [literal];
1577
1872
  if (inner.type === AST_NODE_TYPES11.Literal && inner.value === null) return defaultBinding.namespaces;
@@ -1596,7 +1891,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1596
1891
  }
1597
1892
  const definition = resolveVariable(node)?.defs[0];
1598
1893
  if (definition?.type === "Variable" && definition.parent.kind === "const" && definition.node.id.type === AST_NODE_TYPES11.Identifier && definition.node.init !== null) {
1599
- const init = unwrap(definition.node.init);
1894
+ const init = unwrap2(definition.node.init);
1600
1895
  const literal = staticString(init);
1601
1896
  if (literal !== null) return literal;
1602
1897
  const chained = resolveIdentifierString(init, depth + 1);
@@ -1612,7 +1907,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1612
1907
  }
1613
1908
  function staticPrefix(node) {
1614
1909
  if (node === void 0) return null;
1615
- const inner = unwrap(node);
1910
+ const inner = unwrap2(node);
1616
1911
  if (inner.type === AST_NODE_TYPES11.Identifier && inner.name === "undefined") return null;
1617
1912
  if (inner.type === AST_NODE_TYPES11.Literal && inner.value === null) return null;
1618
1913
  return staticString(inner) ?? UNRESOLVED;
@@ -1623,7 +1918,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1623
1918
  if (namespaces === UNRESOLVED) return UNRESOLVED;
1624
1919
  let keyPrefix = null;
1625
1920
  if (optionsArg !== void 0) {
1626
- const options = unwrap(optionsArg);
1921
+ const options = unwrap2(optionsArg);
1627
1922
  if (options.type !== AST_NODE_TYPES11.ObjectExpression) return UNRESOLVED;
1628
1923
  for (const property of options.properties) {
1629
1924
  if (property.type !== AST_NODE_TYPES11.Property) return UNRESOLVED;
@@ -1649,11 +1944,11 @@ function createTranslationVisitor(context, settings, onUsage) {
1649
1944
  function hookCallOf(identifier) {
1650
1945
  const definition = resolveVariable(identifier)?.defs[0];
1651
1946
  if (definition?.type !== "Variable" || definition.node.id.type !== AST_NODE_TYPES11.Identifier) return null;
1652
- const init = definition.node.init === null ? null : unwrap(definition.node.init);
1947
+ const init = definition.node.init === null ? null : unwrap2(definition.node.init);
1653
1948
  return init !== null && isHookCall(init) ? init : null;
1654
1949
  }
1655
1950
  function bindingOfTSource(object) {
1656
- const inner = unwrap(object);
1951
+ const inner = unwrap2(object);
1657
1952
  if (isHookCall(inner)) return bindingFromHook(inner);
1658
1953
  if (inner.type === AST_NODE_TYPES11.Identifier) {
1659
1954
  const hook = hookCallOf(inner);
@@ -1690,7 +1985,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1690
1985
  }
1691
1986
  function bindingFromDeclarator(declarator, name, depth) {
1692
1987
  if (declarator.init === null) return null;
1693
- const init = unwrap(declarator.init);
1988
+ const init = unwrap2(declarator.init);
1694
1989
  const id = declarator.id;
1695
1990
  if (id.type === AST_NODE_TYPES11.Identifier) {
1696
1991
  if (isGetFixedT(init)) return bindingFromGetFixedT(init);
@@ -1751,7 +2046,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1751
2046
  function readCallOptions(node) {
1752
2047
  const none = { namespaces: null, plural: false, context: false, returnObjects: false };
1753
2048
  if (node === void 0) return none;
1754
- const inner = unwrap(node);
2049
+ const inner = unwrap2(node);
1755
2050
  if (inner.type !== AST_NODE_TYPES11.ObjectExpression) return UNRESOLVED;
1756
2051
  let namespaces = null;
1757
2052
  let plural = false;
@@ -1770,7 +2065,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1770
2065
  } else if (name === "context") {
1771
2066
  context2 = true;
1772
2067
  } else if (name === "returnObjects") {
1773
- const value = unwrap(property.value);
2068
+ const value = unwrap2(property.value);
1774
2069
  returnObjects = !(value.type === AST_NODE_TYPES11.Literal && value.value === false);
1775
2070
  }
1776
2071
  }
@@ -1790,7 +2085,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1790
2085
  return { namespaces, key: `${binding.keyPrefix}${keySeparator === false ? "" : keySeparator}${raw}` };
1791
2086
  }
1792
2087
  function emit(node, keyNode, binding, options) {
1793
- const inner = unwrap(keyNode);
2088
+ const inner = unwrap2(keyNode);
1794
2089
  const raws = [];
1795
2090
  const single = staticString(inner);
1796
2091
  if (single !== null) {
@@ -1913,7 +2208,7 @@ function createTranslationVisitor(context, settings, onUsage) {
1913
2208
  var RULE_NAME11 = "translation-key-exists";
1914
2209
  var stringList = { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true };
1915
2210
  var separator = { oneOf: [{ type: "string", minLength: 1 }, { type: "boolean", enum: [false] }] };
1916
- var optionSchema9 = {
2211
+ var optionSchema10 = {
1917
2212
  type: "object",
1918
2213
  additionalProperties: false,
1919
2214
  properties: {
@@ -1975,9 +2270,10 @@ var translationKeyExistsRule = createRule({
1975
2270
  meta: {
1976
2271
  type: "problem",
1977
2272
  docs: {
1978
- description: "Require every static i18next / react-i18next translation key (`t(...)`, `i18n.t(...)`, `<Trans i18nKey>`) to exist in the catalog of the namespace in scope."
2273
+ description: "Require every static i18next / react-i18next translation key (`t(...)`, `i18n.t(...)`, `<Trans i18nKey>`) to exist in the catalog of the namespace in scope.",
2274
+ requiresOptions: true
1979
2275
  },
1980
- schema: [optionSchema9],
2276
+ schema: [optionSchema10],
1981
2277
  messages: {
1982
2278
  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.",
1983
2279
  missingKeyPrefix: "No key in namespace `{{namespace}}` ({{catalogs}}) starts with `{{prefix}}`, so this template key can never resolve.",
@@ -2066,7 +2362,7 @@ var translationKeyExistsRule = createRule({
2066
2362
  // src/rules/wire-message-naming.ts
2067
2363
  var RULE_NAME12 = "wire-message-naming";
2068
2364
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
2069
- var optionSchema10 = {
2365
+ var optionSchema11 = {
2070
2366
  type: "object",
2071
2367
  additionalProperties: false,
2072
2368
  properties: {
@@ -2114,7 +2410,7 @@ var wireMessageNamingRule = createRule({
2114
2410
  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)."
2115
2411
  },
2116
2412
  fixable: "code",
2117
- schema: [optionSchema10],
2413
+ schema: [optionSchema11],
2118
2414
  messages: {
2119
2415
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
2120
2416
  }
@@ -2154,7 +2450,7 @@ var RULE_NAME13 = "zod-schema-naming";
2154
2450
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
2155
2451
  var SUFFIX = "Schema";
2156
2452
  var DEFAULT_ROLE_SUFFIXES2 = [];
2157
- var optionSchema11 = {
2453
+ var optionSchema12 = {
2158
2454
  type: "object",
2159
2455
  additionalProperties: false,
2160
2456
  properties: {
@@ -2193,7 +2489,7 @@ var zodSchemaNamingRule = createRule({
2193
2489
  docs: {
2194
2490
  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>`)."
2195
2491
  },
2196
- schema: [optionSchema11],
2492
+ schema: [optionSchema12],
2197
2493
  messages: {
2198
2494
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
2199
2495
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -2262,7 +2558,7 @@ var rules = {
2262
2558
 
2263
2559
  // src/index.ts
2264
2560
  var NAMESPACE = "noctcore-contracts";
2265
- var VERSION = "0.6.1";
2561
+ var VERSION = "0.7.1";
2266
2562
  var plugin = {
2267
2563
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
2268
2564
  rules,
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Every `process.env.FOO` / `import.meta.env.FOO` key must be declared in a schema file.
4
4
 
5
+ <!-- begin generated rule header -->
6
+ ⚙️ Opt-in: `off` in `recommended`; needs options (see Options) · 💭 Type information: not needed
7
+ <!-- end generated rule header -->
8
+
5
9
  ## Why
6
10
 
7
11
  An env var that is read in code but declared nowhere is config drift waiting to fail in production:
@@ -27,8 +31,12 @@ const url = process.env.DATABASE_URL;
27
31
  const port = import.meta.env.PORT;
28
32
  ```
29
33
 
30
- Only static accesses are policed. Computed (`process.env[dynamic]`) and destructured reads are left
31
- alone.
34
+ ## What it does not flag
35
+
36
+ - Computed reads such as `process.env[dynamicKey]`: only static `.FOO` accesses are policed.
37
+ - Destructured reads (`const { FOO } = process.env`).
38
+ - Anything at all when `schema` is unset, or when the schema file cannot be read: the rule goes inert
39
+ rather than flagging every access.
32
40
 
33
41
  ## Options
34
42
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  > A fetch response must be checked with `.ok` or a status comparison before `.json()` parses its body.
4
4
 
5
- Ported from tsforge's `typescript-core/fetch-must-check-ok` (MIT).
5
+ <!-- begin generated rule header -->
6
+ ✅ In `recommended` at `error` · 💭 Type information: not needed
7
+ <!-- end generated rule header -->
6
8
 
7
9
  ## Why
8
10
 
@@ -85,6 +87,13 @@ Three response shapes are tracked: `const res = await fetch(...)` then `res.json
85
87
  Aliases work (`const ok = res.ok; if (!ok) throw ...`). A bare `if (res.status)` or
86
88
  `typeof res.status === 'number'` is not a check: both are true for a 500.
87
89
 
90
+ ## What it does not flag
91
+
92
+ Purely syntactic, no type information. A response assigned later (`let res; res = await fetch(...)`),
93
+ passed to another function, or returned from a wrapper that is not in `fetchFunctions` is not tracked.
94
+ A nested function that reuses the response's name is treated as the same binding. Clients whose
95
+ `.json()` already throws on a bad status (ky, for example) do not belong in `fetchFunctions`.
96
+
88
97
  ## Options
89
98
 
90
99
  | Option | Type | Default | Meaning |
@@ -99,9 +108,12 @@ List every name you want tracked, including `fetch` itself:
99
108
  }]
100
109
  ```
101
110
 
102
- ## Limits
111
+ ## When not to use it
103
112
 
104
- Purely syntactic, no type information. A response assigned later (`let res; res = await fetch(...)`),
105
- passed to another function, or returned from a wrapper that is not in `fetchFunctions` is not tracked.
106
- A nested function that reuses the response's name is treated as the same binding. Clients whose
107
- `.json()` already throws on a bad status (ky, for example) do not belong in `fetchFunctions`.
113
+ Leave it off if your code never reads a raw `Response`, for example when every request goes through a
114
+ client whose `.json()` already throws on a bad status.
115
+
116
+ ## Credits
117
+
118
+ Based on the `typescript-core/fetch-must-check-ok` rule from
119
+ [tsforge](https://github.com/boringstack-xyz/tsforge) (MIT).