@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/README.md +74 -32
- package/dist/index.cjs +330 -34
- package/dist/index.d.cts +36 -26
- package/dist/index.d.ts +36 -26
- package/dist/index.js +331 -35
- package/docs/rules/env-var-schema-parity.md +10 -2
- package/docs/rules/fetch-must-check-ok.md +18 -6
- package/docs/rules/money-must-be-decimal.md +14 -5
- package/docs/rules/no-direct-process-env.md +10 -2
- package/docs/rules/no-error-stringify.md +10 -0
- package/docs/rules/require-error-cause.md +17 -13
- package/docs/rules/require-registered-keys.md +13 -4
- package/docs/rules/require-schema-parse-at-boundary.md +101 -7
- package/docs/rules/restrict-throw-to-taxonomy.md +10 -4
- package/docs/rules/schema-enum-field-consistency.md +14 -0
- package/docs/rules/translation-key-exists.md +14 -10
- package/docs/rules/wire-message-naming.md +9 -1
- package/docs/rules/zod-schema-naming.md +10 -0
- package/package.json +7 -7
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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
|
|
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: [
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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: [
|
|
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
|
|
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: [
|
|
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
|
|
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: [
|
|
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.
|
|
2605
|
+
var VERSION = "0.7.1";
|
|
2310
2606
|
var plugin = {
|
|
2311
2607
|
meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
|
|
2312
2608
|
rules,
|