@noctcore/eslint-plugin-contracts 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.cjs +722 -77
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +722 -77
- package/docs/rules/fetch-must-check-ok.md +83 -0
- package/docs/rules/schema-enum-field-consistency.md +75 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,8 @@ var recommended = {
|
|
|
45
45
|
"noctcore-contracts/money-must-be-decimal": "error",
|
|
46
46
|
"noctcore-contracts/require-error-cause": "error",
|
|
47
47
|
"noctcore-contracts/restrict-throw-to-taxonomy": "error",
|
|
48
|
+
"noctcore-contracts/schema-enum-field-consistency": "error",
|
|
49
|
+
"noctcore-contracts/fetch-must-check-ok": "error",
|
|
48
50
|
// Config-required / heuristic rules ship inert. `require-registered-keys` and
|
|
49
51
|
// `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
|
|
50
52
|
// set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
|
|
@@ -151,9 +153,476 @@ var envVarSchemaParityRule = createRule({
|
|
|
151
153
|
}
|
|
152
154
|
});
|
|
153
155
|
|
|
154
|
-
// src/rules/
|
|
156
|
+
// src/rules/fetch-must-check-ok.ts
|
|
155
157
|
var import_utils2 = require("@typescript-eslint/utils");
|
|
156
|
-
var RULE_NAME2 = "
|
|
158
|
+
var RULE_NAME2 = "fetch-must-check-ok";
|
|
159
|
+
var optionSchema2 = {
|
|
160
|
+
type: "object",
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
properties: {
|
|
163
|
+
fetchFunctions: {
|
|
164
|
+
type: "array",
|
|
165
|
+
items: { type: "string", minLength: 1 },
|
|
166
|
+
uniqueItems: true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function isNode(value) {
|
|
171
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
172
|
+
}
|
|
173
|
+
function walkSome(root, keys, predicate) {
|
|
174
|
+
const stack = [root];
|
|
175
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
176
|
+
if (predicate(node)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
for (const key of keys[node.type] ?? []) {
|
|
180
|
+
const value = Reflect.get(node, key);
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
for (const child of value) {
|
|
183
|
+
if (isNode(child)) {
|
|
184
|
+
stack.push(child);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (isNode(value)) {
|
|
188
|
+
stack.push(value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
function calleePath(node) {
|
|
195
|
+
if (node.type === import_utils2.AST_NODE_TYPES.Identifier) {
|
|
196
|
+
return node.name;
|
|
197
|
+
}
|
|
198
|
+
if (node.type === import_utils2.AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
|
|
199
|
+
const object = calleePath(node.object);
|
|
200
|
+
return object === null ? null : `${object}.${node.property.name}`;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
var OK_PROP = "ok";
|
|
205
|
+
var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
|
|
206
|
+
var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
|
|
207
|
+
var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
|
|
208
|
+
var FIRST_ERROR_STATUS = 400;
|
|
209
|
+
function literalValue(node) {
|
|
210
|
+
if (node.type !== import_utils2.AST_NODE_TYPES.Literal) {
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
|
|
214
|
+
}
|
|
215
|
+
function mirror(operator) {
|
|
216
|
+
switch (operator) {
|
|
217
|
+
case "<":
|
|
218
|
+
return ">";
|
|
219
|
+
case "<=":
|
|
220
|
+
return ">=";
|
|
221
|
+
case ">":
|
|
222
|
+
return "<";
|
|
223
|
+
case ">=":
|
|
224
|
+
return "<=";
|
|
225
|
+
default:
|
|
226
|
+
return operator;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function booleanPolarity(operator, value) {
|
|
230
|
+
if (operator === "===" || operator === "==") {
|
|
231
|
+
return value ? "positive" : "negative";
|
|
232
|
+
}
|
|
233
|
+
if (operator === "!==" || operator === "!=") {
|
|
234
|
+
return value ? "negative" : "positive";
|
|
235
|
+
}
|
|
236
|
+
return "opaque";
|
|
237
|
+
}
|
|
238
|
+
function statusPolarity(operator, value) {
|
|
239
|
+
const isSuccessCode = value >= 200 && value < 300;
|
|
240
|
+
switch (operator) {
|
|
241
|
+
case "===":
|
|
242
|
+
case "==":
|
|
243
|
+
return isSuccessCode ? "positive" : "opaque";
|
|
244
|
+
case "!==":
|
|
245
|
+
case "!=":
|
|
246
|
+
return isSuccessCode ? "negative" : "opaque";
|
|
247
|
+
case "<":
|
|
248
|
+
return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
|
|
249
|
+
case "<=":
|
|
250
|
+
return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
|
|
251
|
+
// A failure test is only useful for what it says about the OTHER side, so
|
|
252
|
+
// what matters is that everything below the threshold is a success:
|
|
253
|
+
// `>= 300` and `>= 400` both leave only good responses behind, while
|
|
254
|
+
// `>= 500` leaves every 4xx there.
|
|
255
|
+
case ">=":
|
|
256
|
+
return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
|
|
257
|
+
case ">":
|
|
258
|
+
return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
|
|
259
|
+
default:
|
|
260
|
+
return "opaque";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function comparisonPolarity(node, readIsLeft) {
|
|
264
|
+
const value = literalValue(readIsLeft ? node.right : node.left);
|
|
265
|
+
const operator = readIsLeft ? node.operator : mirror(node.operator);
|
|
266
|
+
if (typeof value === "boolean") {
|
|
267
|
+
return booleanPolarity(operator, value);
|
|
268
|
+
}
|
|
269
|
+
return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
|
|
270
|
+
}
|
|
271
|
+
function propReadOn(node, objectName, props) {
|
|
272
|
+
if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
if (node.object.type !== import_utils2.AST_NODE_TYPES.Identifier || node.object.name !== objectName || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
const name = node.property.name;
|
|
279
|
+
return typeof props === "string" ? name === props : props.has(name);
|
|
280
|
+
}
|
|
281
|
+
function findJsonReads(root, keys, name) {
|
|
282
|
+
const reads = [];
|
|
283
|
+
walkSome(root, keys, (node) => {
|
|
284
|
+
if (propReadOn(node, name, "json")) {
|
|
285
|
+
reads.push(node);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
});
|
|
289
|
+
return reads;
|
|
290
|
+
}
|
|
291
|
+
function isAssertionName(node) {
|
|
292
|
+
return node.type === import_utils2.AST_NODE_TYPES.Identifier && ASSERTION_NAMES.test(node.name);
|
|
293
|
+
}
|
|
294
|
+
function isAssertionCallee(callee) {
|
|
295
|
+
if (callee.type === import_utils2.AST_NODE_TYPES.Identifier) {
|
|
296
|
+
return isAssertionName(callee);
|
|
297
|
+
}
|
|
298
|
+
return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
|
|
299
|
+
}
|
|
300
|
+
var TERMINAL_TYPES = /* @__PURE__ */ new Set([
|
|
301
|
+
import_utils2.AST_NODE_TYPES.IfStatement,
|
|
302
|
+
import_utils2.AST_NODE_TYPES.WhileStatement,
|
|
303
|
+
import_utils2.AST_NODE_TYPES.DoWhileStatement,
|
|
304
|
+
import_utils2.AST_NODE_TYPES.ConditionalExpression,
|
|
305
|
+
import_utils2.AST_NODE_TYPES.SwitchStatement,
|
|
306
|
+
import_utils2.AST_NODE_TYPES.CallExpression
|
|
307
|
+
]);
|
|
308
|
+
var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
|
|
309
|
+
["equal", "==="],
|
|
310
|
+
["equals", "==="],
|
|
311
|
+
["strictEqual", "==="],
|
|
312
|
+
["deepEqual", "==="],
|
|
313
|
+
["deepStrictEqual", "==="],
|
|
314
|
+
["toBe", "==="],
|
|
315
|
+
["toEqual", "==="],
|
|
316
|
+
["notEqual", "!=="],
|
|
317
|
+
["notStrictEqual", "!=="],
|
|
318
|
+
["notDeepEqual", "!=="]
|
|
319
|
+
]);
|
|
320
|
+
function assertionOperator(callee) {
|
|
321
|
+
return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils2.AST_NODE_TYPES.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
|
|
322
|
+
}
|
|
323
|
+
function assertionCheck(parent, child, check) {
|
|
324
|
+
if (check.compared) {
|
|
325
|
+
return check;
|
|
326
|
+
}
|
|
327
|
+
const operator = assertionOperator(parent.callee);
|
|
328
|
+
if (operator === void 0) {
|
|
329
|
+
return check;
|
|
330
|
+
}
|
|
331
|
+
const other = parent.arguments.find((arg) => arg !== child);
|
|
332
|
+
const value = other === void 0 ? void 0 : literalValue(other);
|
|
333
|
+
if (typeof value === "boolean") {
|
|
334
|
+
return {
|
|
335
|
+
...check,
|
|
336
|
+
compared: true,
|
|
337
|
+
polarity: booleanPolarity(operator, value)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
|
|
341
|
+
}
|
|
342
|
+
function terminalCheck(parent, child, state) {
|
|
343
|
+
const check = { owner: parent, ...state };
|
|
344
|
+
switch (parent.type) {
|
|
345
|
+
case import_utils2.AST_NODE_TYPES.IfStatement:
|
|
346
|
+
case import_utils2.AST_NODE_TYPES.WhileStatement:
|
|
347
|
+
case import_utils2.AST_NODE_TYPES.DoWhileStatement:
|
|
348
|
+
case import_utils2.AST_NODE_TYPES.ConditionalExpression:
|
|
349
|
+
return parent.test === child ? check : null;
|
|
350
|
+
case import_utils2.AST_NODE_TYPES.SwitchStatement:
|
|
351
|
+
return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
|
|
352
|
+
// An assertion settles a status only when it COMPARES it.
|
|
353
|
+
// `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
|
|
354
|
+
// number is truthy, which every response that arrived satisfies. Arity
|
|
355
|
+
// cannot tell them apart (`assert.ok(res.status, 'message')` also has two
|
|
356
|
+
// arguments), so the comparison is read from the assertion's own name.
|
|
357
|
+
case import_utils2.AST_NODE_TYPES.CallExpression:
|
|
358
|
+
return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
|
|
359
|
+
default:
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function combinatorStep(parent, child, state, parse) {
|
|
364
|
+
switch (parent.type) {
|
|
365
|
+
case import_utils2.AST_NODE_TYPES.UnaryExpression:
|
|
366
|
+
if (parent.operator === "typeof") {
|
|
367
|
+
return "stop";
|
|
368
|
+
}
|
|
369
|
+
if (parent.operator === "!") {
|
|
370
|
+
state.polarity = state.polarity === "positive" ? "negative" : "positive";
|
|
371
|
+
}
|
|
372
|
+
return "continue";
|
|
373
|
+
case import_utils2.AST_NODE_TYPES.BinaryExpression: {
|
|
374
|
+
if (!COMPARISONS.has(parent.operator)) {
|
|
375
|
+
return "stop";
|
|
376
|
+
}
|
|
377
|
+
const polarity = comparisonPolarity(parent, parent.left === child);
|
|
378
|
+
state.compared = true;
|
|
379
|
+
state.polarity = polarity;
|
|
380
|
+
return polarity === "opaque" ? "stop" : "continue";
|
|
381
|
+
}
|
|
382
|
+
case import_utils2.AST_NODE_TYPES.LogicalExpression:
|
|
383
|
+
if (parent.operator === "&&") {
|
|
384
|
+
state.underAnd = true;
|
|
385
|
+
}
|
|
386
|
+
if (parent.operator === "||") {
|
|
387
|
+
state.underOr = true;
|
|
388
|
+
}
|
|
389
|
+
return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
|
|
390
|
+
case import_utils2.AST_NODE_TYPES.ChainExpression:
|
|
391
|
+
case import_utils2.AST_NODE_TYPES.TSNonNullExpression:
|
|
392
|
+
return "continue";
|
|
393
|
+
default:
|
|
394
|
+
return "stop";
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function climb(read, parse) {
|
|
398
|
+
const state = {
|
|
399
|
+
polarity: "positive",
|
|
400
|
+
compared: false,
|
|
401
|
+
underAnd: false,
|
|
402
|
+
underOr: false
|
|
403
|
+
};
|
|
404
|
+
let child = read;
|
|
405
|
+
let parent = read.parent;
|
|
406
|
+
while (parent !== void 0) {
|
|
407
|
+
if (TERMINAL_TYPES.has(parent.type)) {
|
|
408
|
+
return terminalCheck(parent, child, state);
|
|
409
|
+
}
|
|
410
|
+
const step = combinatorStep(parent, child, state, parse);
|
|
411
|
+
if (step === "stop") {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (step !== "continue") {
|
|
415
|
+
return step;
|
|
416
|
+
}
|
|
417
|
+
child = parent;
|
|
418
|
+
parent = parent.parent;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function contains(outer, inner) {
|
|
423
|
+
return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
|
|
424
|
+
}
|
|
425
|
+
function alwaysExits(node) {
|
|
426
|
+
if (node.type === import_utils2.AST_NODE_TYPES.ReturnStatement || node.type === import_utils2.AST_NODE_TYPES.ThrowStatement) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return node.type === import_utils2.AST_NODE_TYPES.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
|
|
430
|
+
}
|
|
431
|
+
function scopeOfCheck(node) {
|
|
432
|
+
let current = node;
|
|
433
|
+
while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
|
|
434
|
+
current = current.parent;
|
|
435
|
+
}
|
|
436
|
+
return current.parent ?? current;
|
|
437
|
+
}
|
|
438
|
+
function isSuccessCase(arm) {
|
|
439
|
+
if (arm.test === null) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const value = literalValue(arm.test);
|
|
443
|
+
return typeof value === "number" && value >= 200 && value < 300;
|
|
444
|
+
}
|
|
445
|
+
function switchArmProtects(owner, parse) {
|
|
446
|
+
const index = owner.cases.findIndex((arm) => contains(arm, parse));
|
|
447
|
+
const own = owner.cases[index];
|
|
448
|
+
if (own === void 0 || !isSuccessCase(own)) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
452
|
+
const arm = owner.cases[i];
|
|
453
|
+
if (arm === void 0 || arm.consequent.length > 0) {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
if (!isSuccessCase(arm)) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
function branchProtects(check, parse) {
|
|
463
|
+
const { owner } = check;
|
|
464
|
+
if (owner.type === import_utils2.AST_NODE_TYPES.SwitchStatement) {
|
|
465
|
+
return switchArmProtects(owner, parse);
|
|
466
|
+
}
|
|
467
|
+
if (owner.type === import_utils2.AST_NODE_TYPES.LogicalExpression) {
|
|
468
|
+
if (!contains(owner.right, parse)) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
if (owner.operator === "&&") {
|
|
472
|
+
return check.polarity === "positive";
|
|
473
|
+
}
|
|
474
|
+
return owner.operator === "||" && check.polarity === "negative";
|
|
475
|
+
}
|
|
476
|
+
if (owner.type === import_utils2.AST_NODE_TYPES.WhileStatement || owner.type === import_utils2.AST_NODE_TYPES.DoWhileStatement) {
|
|
477
|
+
return contains(owner.body, parse) && entersOnSuccess(check);
|
|
478
|
+
}
|
|
479
|
+
if (owner.type !== import_utils2.AST_NODE_TYPES.ConditionalExpression && owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (contains(owner.consequent, parse)) {
|
|
483
|
+
return entersOnSuccess(check);
|
|
484
|
+
}
|
|
485
|
+
return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
|
|
486
|
+
}
|
|
487
|
+
function entersOnSuccess(check) {
|
|
488
|
+
return check.polarity === "positive" && !check.underOr;
|
|
489
|
+
}
|
|
490
|
+
function skipsOnSuccess(check) {
|
|
491
|
+
return check.polarity === "negative" && !check.underAnd;
|
|
492
|
+
}
|
|
493
|
+
function guardProtects(check, parse) {
|
|
494
|
+
const { owner } = check;
|
|
495
|
+
if (owner.type === import_utils2.AST_NODE_TYPES.CallExpression) {
|
|
496
|
+
return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
|
|
497
|
+
}
|
|
498
|
+
if (owner.type !== import_utils2.AST_NODE_TYPES.IfStatement) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (alwaysExits(owner.consequent)) {
|
|
505
|
+
return skipsOnSuccess(check);
|
|
506
|
+
}
|
|
507
|
+
return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
|
|
508
|
+
}
|
|
509
|
+
function protects(check, parse) {
|
|
510
|
+
if (!check.compared) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
return branchProtects(check, parse) || guardProtects(check, parse);
|
|
514
|
+
}
|
|
515
|
+
function statusAliases(root, keys, name) {
|
|
516
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
517
|
+
walkSome(root, keys, (node) => {
|
|
518
|
+
if (node.type === import_utils2.AST_NODE_TYPES.VariableDeclarator && node.id.type === import_utils2.AST_NODE_TYPES.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === import_utils2.AST_NODE_TYPES.Identifier) {
|
|
519
|
+
aliases.set(node.id.name, node.init.property.name);
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
});
|
|
523
|
+
return aliases;
|
|
524
|
+
}
|
|
525
|
+
function checkedProp(node, name, aliases) {
|
|
526
|
+
if (propReadOn(node, name, OK_PROPS)) {
|
|
527
|
+
return node.property.type === import_utils2.AST_NODE_TYPES.Identifier ? node.property.name : void 0;
|
|
528
|
+
}
|
|
529
|
+
return node.type === import_utils2.AST_NODE_TYPES.Identifier ? aliases.get(node.name) : void 0;
|
|
530
|
+
}
|
|
531
|
+
function isProtected(root, keys, name, aliases, parse) {
|
|
532
|
+
return walkSome(root, keys, (node) => {
|
|
533
|
+
const prop = checkedProp(node, name, aliases);
|
|
534
|
+
if (prop === void 0) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
const check = climb(node, parse);
|
|
538
|
+
if (check === null) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return protects(
|
|
542
|
+
prop === OK_PROP ? { ...check, compared: true } : check,
|
|
543
|
+
parse
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function scopeOf(node) {
|
|
548
|
+
let current = node.parent;
|
|
549
|
+
while (current !== void 0) {
|
|
550
|
+
if (current.type === import_utils2.AST_NODE_TYPES.BlockStatement || current.type === import_utils2.AST_NODE_TYPES.Program) {
|
|
551
|
+
return current;
|
|
552
|
+
}
|
|
553
|
+
current = current.parent;
|
|
554
|
+
}
|
|
555
|
+
return node;
|
|
556
|
+
}
|
|
557
|
+
function skipAwait(node) {
|
|
558
|
+
return node?.type === import_utils2.AST_NODE_TYPES.AwaitExpression ? node.parent : node;
|
|
559
|
+
}
|
|
560
|
+
function thenCallbackParam(node) {
|
|
561
|
+
if (node.type !== import_utils2.AST_NODE_TYPES.MemberExpression || node.computed || node.property.type !== import_utils2.AST_NODE_TYPES.Identifier || node.property.name !== "then" || node.parent.type !== import_utils2.AST_NODE_TYPES.CallExpression) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const callback = node.parent.arguments[0];
|
|
565
|
+
if (callback === void 0 || callback.type !== import_utils2.AST_NODE_TYPES.ArrowFunctionExpression && callback.type !== import_utils2.AST_NODE_TYPES.FunctionExpression) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const param = callback.params[0];
|
|
569
|
+
return param?.type === import_utils2.AST_NODE_TYPES.Identifier ? { name: param.name, body: callback.body } : null;
|
|
570
|
+
}
|
|
571
|
+
var fetchMustCheckOkRule = createRule({
|
|
572
|
+
name: RULE_NAME2,
|
|
573
|
+
meta: {
|
|
574
|
+
type: "problem",
|
|
575
|
+
docs: {
|
|
576
|
+
description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
|
|
577
|
+
},
|
|
578
|
+
schema: [optionSchema2],
|
|
579
|
+
messages: {
|
|
580
|
+
missingOkCheck: "`fetch` resolves on 4xx/5xx too, so `.json()` here can parse an error body as data. Check `response.ok` (or compare the status) and leave early before reading the body."
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
defaultOptions: [{ fetchFunctions: ["fetch"] }],
|
|
584
|
+
create(context, [options]) {
|
|
585
|
+
const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
|
|
586
|
+
const keys = context.sourceCode.visitorKeys;
|
|
587
|
+
function reportUnprotected(root, name) {
|
|
588
|
+
const aliases = statusAliases(root, keys, name);
|
|
589
|
+
for (const read of findJsonReads(root, keys, name)) {
|
|
590
|
+
if (!isProtected(root, keys, name, aliases, read)) {
|
|
591
|
+
context.report({ node: read, messageId: "missingOkCheck" });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
CallExpression(node) {
|
|
597
|
+
const path2 = calleePath(node.callee);
|
|
598
|
+
if (path2 === null || !fetchFunctions.has(path2)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const parent = skipAwait(node.parent);
|
|
602
|
+
if (parent === void 0) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (parent.type === import_utils2.AST_NODE_TYPES.MemberExpression && !parent.computed && parent.property.type === import_utils2.AST_NODE_TYPES.Identifier && parent.property.name === "json") {
|
|
606
|
+
context.report({ node: parent, messageId: "missingOkCheck" });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const callback = thenCallbackParam(parent);
|
|
610
|
+
if (callback !== null) {
|
|
611
|
+
reportUnprotected(callback.body, callback.name);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (parent.type !== import_utils2.AST_NODE_TYPES.VariableDeclarator || parent.id.type !== import_utils2.AST_NODE_TYPES.Identifier) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
reportUnprotected(scopeOf(parent), parent.id.name);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// src/rules/money-must-be-decimal.ts
|
|
624
|
+
var import_utils3 = require("@typescript-eslint/utils");
|
|
625
|
+
var RULE_NAME3 = "money-must-be-decimal";
|
|
157
626
|
var DEFAULT_DECIMAL_TYPE = "Decimal";
|
|
158
627
|
var DEFAULT_FIELD_PATTERNS = [
|
|
159
628
|
"amount",
|
|
@@ -163,7 +632,7 @@ var DEFAULT_FIELD_PATTERNS = [
|
|
|
163
632
|
"balance"
|
|
164
633
|
];
|
|
165
634
|
var DEFAULT_ALLOWED_FILES = [];
|
|
166
|
-
var
|
|
635
|
+
var optionSchema3 = {
|
|
167
636
|
type: "object",
|
|
168
637
|
additionalProperties: false,
|
|
169
638
|
properties: {
|
|
@@ -192,22 +661,22 @@ function isAllowedFile(filename, patterns) {
|
|
|
192
661
|
return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
|
|
193
662
|
}
|
|
194
663
|
function staticName(node) {
|
|
195
|
-
if (node.type ===
|
|
664
|
+
if (node.type === import_utils3.AST_NODE_TYPES.Identifier) {
|
|
196
665
|
return node.name;
|
|
197
666
|
}
|
|
198
667
|
return void 0;
|
|
199
668
|
}
|
|
200
669
|
function isNumberAnnotation(annotation) {
|
|
201
|
-
return annotation?.typeAnnotation.type ===
|
|
670
|
+
return annotation?.typeAnnotation.type === import_utils3.AST_NODE_TYPES.TSNumberKeyword;
|
|
202
671
|
}
|
|
203
672
|
var moneyMustBeDecimalRule = createRule({
|
|
204
|
-
name:
|
|
673
|
+
name: RULE_NAME3,
|
|
205
674
|
meta: {
|
|
206
675
|
type: "problem",
|
|
207
676
|
docs: {
|
|
208
677
|
description: "Disallow monetary values typed as the JS primitive `number`. Money-named fields explicitly typed `: number` lose precision to float rounding; use a Decimal money type instead."
|
|
209
678
|
},
|
|
210
|
-
schema: [
|
|
679
|
+
schema: [optionSchema3],
|
|
211
680
|
messages: {
|
|
212
681
|
moneyMustBeDecimal: "Monetary values must use {{decimalType}}, never the JS `number` primitive, to avoid float rounding errors. Rename or retype this field to a {{decimalType}} money type."
|
|
213
682
|
}
|
|
@@ -243,7 +712,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
243
712
|
},
|
|
244
713
|
// `const total: number = ...`: annotated variable declarator.
|
|
245
714
|
VariableDeclarator(node) {
|
|
246
|
-
if (node.id.type !==
|
|
715
|
+
if (node.id.type !== import_utils3.AST_NODE_TYPES.Identifier) {
|
|
247
716
|
return;
|
|
248
717
|
}
|
|
249
718
|
const name = node.id.name;
|
|
@@ -256,15 +725,15 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
256
725
|
});
|
|
257
726
|
|
|
258
727
|
// src/rules/no-direct-process-env.ts
|
|
259
|
-
var
|
|
260
|
-
var
|
|
728
|
+
var import_utils4 = require("@typescript-eslint/utils");
|
|
729
|
+
var RULE_NAME4 = "no-direct-process-env";
|
|
261
730
|
var DEFAULT_CONFIG_MODULE = "@/config";
|
|
262
731
|
var DEFAULT_ALLOWED_FILES2 = [
|
|
263
732
|
"**/*.config.{ts,js,mjs,cjs}",
|
|
264
733
|
"**/*.{spec,test}.{ts,tsx}",
|
|
265
734
|
"**/scripts/**"
|
|
266
735
|
];
|
|
267
|
-
var
|
|
736
|
+
var optionSchema4 = {
|
|
268
737
|
type: "object",
|
|
269
738
|
additionalProperties: false,
|
|
270
739
|
properties: {
|
|
@@ -319,22 +788,22 @@ function isAllowedFile2(filename, patterns) {
|
|
|
319
788
|
return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
320
789
|
}
|
|
321
790
|
function isProcessEnv2(node) {
|
|
322
|
-
if (node.type !==
|
|
791
|
+
if (node.type !== import_utils4.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils4.AST_NODE_TYPES.Identifier || node.object.name !== "process") {
|
|
323
792
|
return false;
|
|
324
793
|
}
|
|
325
794
|
if (node.computed) {
|
|
326
|
-
return node.property.type ===
|
|
795
|
+
return node.property.type === import_utils4.AST_NODE_TYPES.Literal && node.property.value === "env";
|
|
327
796
|
}
|
|
328
|
-
return node.property.type ===
|
|
797
|
+
return node.property.type === import_utils4.AST_NODE_TYPES.Identifier && node.property.name === "env";
|
|
329
798
|
}
|
|
330
799
|
var noDirectProcessEnvRule = createRule({
|
|
331
|
-
name:
|
|
800
|
+
name: RULE_NAME4,
|
|
332
801
|
meta: {
|
|
333
802
|
type: "problem",
|
|
334
803
|
docs: {
|
|
335
804
|
description: "Disallow direct `process.env` access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use."
|
|
336
805
|
},
|
|
337
|
-
schema: [
|
|
806
|
+
schema: [optionSchema4],
|
|
338
807
|
messages: {
|
|
339
808
|
directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
|
|
340
809
|
}
|
|
@@ -369,10 +838,10 @@ var noDirectProcessEnvRule = createRule({
|
|
|
369
838
|
});
|
|
370
839
|
|
|
371
840
|
// src/rules/no-error-stringify.ts
|
|
372
|
-
var
|
|
373
|
-
var
|
|
841
|
+
var import_utils5 = require("@typescript-eslint/utils");
|
|
842
|
+
var RULE_NAME5 = "no-error-stringify";
|
|
374
843
|
var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
|
|
375
|
-
var
|
|
844
|
+
var optionSchema5 = {
|
|
376
845
|
type: "object",
|
|
377
846
|
additionalProperties: false,
|
|
378
847
|
properties: {
|
|
@@ -385,19 +854,19 @@ var optionSchema4 = {
|
|
|
385
854
|
}
|
|
386
855
|
};
|
|
387
856
|
function isEmptyStringLiteral(node) {
|
|
388
|
-
return node.type ===
|
|
857
|
+
return node.type === import_utils5.AST_NODE_TYPES.Literal && node.value === "";
|
|
389
858
|
}
|
|
390
859
|
function isErrorIdentifier(node, names) {
|
|
391
|
-
return node.type ===
|
|
860
|
+
return node.type === import_utils5.AST_NODE_TYPES.Identifier && names.has(node.name);
|
|
392
861
|
}
|
|
393
862
|
var noErrorStringifyRule = createRule({
|
|
394
|
-
name:
|
|
863
|
+
name: RULE_NAME5,
|
|
395
864
|
meta: {
|
|
396
865
|
type: "problem",
|
|
397
866
|
docs: {
|
|
398
867
|
description: 'Disallow stringifying an error with bare `${error}` interpolation, `error.toString()`, or `error + ""`. These drop the cause chain. Use `error instanceof Error ? error.message : String(error)` instead.'
|
|
399
868
|
},
|
|
400
|
-
schema: [
|
|
869
|
+
schema: [optionSchema5],
|
|
401
870
|
messages: {
|
|
402
871
|
noErrorStringify: "Stringifying an error this way drops its cause chain. Use `{{name}} instanceof Error ? {{name}}.message : String({{name}})` (or pass the Error object straight to the logger)."
|
|
403
872
|
}
|
|
@@ -412,7 +881,7 @@ var noErrorStringifyRule = createRule({
|
|
|
412
881
|
// `error.toString()`
|
|
413
882
|
'CallExpression[callee.type="MemberExpression"]'(node) {
|
|
414
883
|
const callee = node.callee;
|
|
415
|
-
if (!callee.computed && callee.property.type ===
|
|
884
|
+
if (!callee.computed && callee.property.type === import_utils5.AST_NODE_TYPES.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
|
|
416
885
|
report(node, callee.object.name);
|
|
417
886
|
}
|
|
418
887
|
},
|
|
@@ -445,14 +914,14 @@ var noErrorStringifyRule = createRule({
|
|
|
445
914
|
});
|
|
446
915
|
|
|
447
916
|
// src/rules/require-error-cause.ts
|
|
448
|
-
var
|
|
449
|
-
var
|
|
917
|
+
var import_utils6 = require("@typescript-eslint/utils");
|
|
918
|
+
var RULE_NAME6 = "require-error-cause";
|
|
450
919
|
function constructorSimpleName(node) {
|
|
451
920
|
const callee = node.callee;
|
|
452
|
-
if (callee.type ===
|
|
921
|
+
if (callee.type === import_utils6.AST_NODE_TYPES.Identifier) {
|
|
453
922
|
return callee.name;
|
|
454
923
|
}
|
|
455
|
-
if (callee.type ===
|
|
924
|
+
if (callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils6.AST_NODE_TYPES.Identifier) {
|
|
456
925
|
return callee.property.name;
|
|
457
926
|
}
|
|
458
927
|
return null;
|
|
@@ -462,16 +931,16 @@ function isErrorLikeName(name) {
|
|
|
462
931
|
}
|
|
463
932
|
function alreadyHasCause(node) {
|
|
464
933
|
for (const arg of node.arguments) {
|
|
465
|
-
if (arg.type ===
|
|
934
|
+
if (arg.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
|
|
466
935
|
return true;
|
|
467
936
|
}
|
|
468
|
-
if (arg.type ===
|
|
937
|
+
if (arg.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
|
|
469
938
|
for (const prop of arg.properties) {
|
|
470
|
-
if (prop.type ===
|
|
939
|
+
if (prop.type === import_utils6.AST_NODE_TYPES.SpreadElement) {
|
|
471
940
|
return true;
|
|
472
941
|
}
|
|
473
942
|
const key = prop.key;
|
|
474
|
-
const isCause = key.type ===
|
|
943
|
+
const isCause = key.type === import_utils6.AST_NODE_TYPES.Identifier && key.name === "cause" || key.type === import_utils6.AST_NODE_TYPES.Literal && key.value === "cause";
|
|
475
944
|
if (isCause) {
|
|
476
945
|
return true;
|
|
477
946
|
}
|
|
@@ -489,7 +958,7 @@ function buildFix(node, binding) {
|
|
|
489
958
|
if (last === void 0) {
|
|
490
959
|
return null;
|
|
491
960
|
}
|
|
492
|
-
if (last.type ===
|
|
961
|
+
if (last.type === import_utils6.AST_NODE_TYPES.ObjectExpression) {
|
|
493
962
|
const props = last.properties;
|
|
494
963
|
if (props.length === 0) {
|
|
495
964
|
return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
|
|
@@ -503,7 +972,7 @@ function buildFix(node, binding) {
|
|
|
503
972
|
return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
|
|
504
973
|
}
|
|
505
974
|
var requireErrorCauseRule = createRule({
|
|
506
|
-
name:
|
|
975
|
+
name: RULE_NAME6,
|
|
507
976
|
meta: {
|
|
508
977
|
type: "problem",
|
|
509
978
|
docs: {
|
|
@@ -522,7 +991,7 @@ var requireErrorCauseRule = createRule({
|
|
|
522
991
|
CatchClause(node) {
|
|
523
992
|
const param = node.param;
|
|
524
993
|
catchBindings.push(
|
|
525
|
-
param && param.type ===
|
|
994
|
+
param && param.type === import_utils6.AST_NODE_TYPES.Identifier ? param.name : null
|
|
526
995
|
);
|
|
527
996
|
},
|
|
528
997
|
"CatchClause:exit"() {
|
|
@@ -534,7 +1003,7 @@ var requireErrorCauseRule = createRule({
|
|
|
534
1003
|
return;
|
|
535
1004
|
}
|
|
536
1005
|
const arg = node.argument;
|
|
537
|
-
if (arg.type !==
|
|
1006
|
+
if (arg.type !== import_utils6.AST_NODE_TYPES.NewExpression) {
|
|
538
1007
|
return;
|
|
539
1008
|
}
|
|
540
1009
|
const ctor = constructorSimpleName(arg);
|
|
@@ -557,9 +1026,9 @@ var requireErrorCauseRule = createRule({
|
|
|
557
1026
|
});
|
|
558
1027
|
|
|
559
1028
|
// src/rules/require-registered-keys.ts
|
|
560
|
-
var
|
|
561
|
-
var
|
|
562
|
-
var
|
|
1029
|
+
var import_utils7 = require("@typescript-eslint/utils");
|
|
1030
|
+
var RULE_NAME7 = "require-registered-keys";
|
|
1031
|
+
var optionSchema6 = {
|
|
563
1032
|
type: "object",
|
|
564
1033
|
additionalProperties: false,
|
|
565
1034
|
properties: {
|
|
@@ -578,30 +1047,30 @@ var optionSchema5 = {
|
|
|
578
1047
|
registry: { type: "string", minLength: 1 }
|
|
579
1048
|
}
|
|
580
1049
|
};
|
|
581
|
-
function
|
|
582
|
-
if (callee.type ===
|
|
1050
|
+
function calleePath2(callee) {
|
|
1051
|
+
if (callee.type === import_utils7.AST_NODE_TYPES.Identifier) {
|
|
583
1052
|
return callee.name;
|
|
584
1053
|
}
|
|
585
|
-
if (callee.type ===
|
|
586
|
-
if (callee.property.type !==
|
|
1054
|
+
if (callee.type === import_utils7.AST_NODE_TYPES.MemberExpression && !callee.computed) {
|
|
1055
|
+
if (callee.property.type !== import_utils7.AST_NODE_TYPES.Identifier) {
|
|
587
1056
|
return null;
|
|
588
1057
|
}
|
|
589
|
-
const objectPath =
|
|
1058
|
+
const objectPath = calleePath2(callee.object);
|
|
590
1059
|
return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
|
|
591
1060
|
}
|
|
592
1061
|
return null;
|
|
593
1062
|
}
|
|
594
1063
|
function isStringLiteral(node) {
|
|
595
|
-
return node.type ===
|
|
1064
|
+
return node.type === import_utils7.AST_NODE_TYPES.Literal && typeof node.value === "string";
|
|
596
1065
|
}
|
|
597
1066
|
var requireRegisteredKeysRule = createRule({
|
|
598
|
-
name:
|
|
1067
|
+
name: RULE_NAME7,
|
|
599
1068
|
meta: {
|
|
600
1069
|
type: "suggestion",
|
|
601
1070
|
docs: {
|
|
602
1071
|
description: "Require the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal."
|
|
603
1072
|
},
|
|
604
|
-
schema: [
|
|
1073
|
+
schema: [optionSchema6],
|
|
605
1074
|
messages: {
|
|
606
1075
|
unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
|
|
607
1076
|
}
|
|
@@ -625,7 +1094,7 @@ var requireRegisteredKeysRule = createRule({
|
|
|
625
1094
|
const registryHint = registry ? ` (import it from '${registry}')` : "";
|
|
626
1095
|
return {
|
|
627
1096
|
CallExpression(node) {
|
|
628
|
-
const path2 =
|
|
1097
|
+
const path2 = calleePath2(node.callee);
|
|
629
1098
|
if (path2 === null) {
|
|
630
1099
|
return;
|
|
631
1100
|
}
|
|
@@ -649,29 +1118,29 @@ var requireRegisteredKeysRule = createRule({
|
|
|
649
1118
|
});
|
|
650
1119
|
|
|
651
1120
|
// src/rules/require-schema-parse-at-boundary.ts
|
|
652
|
-
var
|
|
653
|
-
var
|
|
1121
|
+
var import_utils8 = require("@typescript-eslint/utils");
|
|
1122
|
+
var RULE_NAME8 = "require-schema-parse-at-boundary";
|
|
654
1123
|
function isJsonParseCall(node) {
|
|
655
|
-
return node.type ===
|
|
1124
|
+
return node.type === import_utils8.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.object.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && node.callee.property.name === "parse";
|
|
656
1125
|
}
|
|
657
1126
|
function isAwaitJsonCall(node) {
|
|
658
|
-
if (node.type !==
|
|
1127
|
+
if (node.type !== import_utils8.AST_NODE_TYPES.AwaitExpression) {
|
|
659
1128
|
return false;
|
|
660
1129
|
}
|
|
661
1130
|
const call = node.argument;
|
|
662
|
-
return call.type ===
|
|
1131
|
+
return call.type === import_utils8.AST_NODE_TYPES.CallExpression && call.arguments.length === 0 && call.callee.type === import_utils8.AST_NODE_TYPES.MemberExpression && !call.callee.computed && call.callee.property.type === import_utils8.AST_NODE_TYPES.Identifier && call.callee.property.name === "json";
|
|
663
1132
|
}
|
|
664
1133
|
function isShapeClaim(annotation) {
|
|
665
|
-
if (annotation.type ===
|
|
1134
|
+
if (annotation.type === import_utils8.AST_NODE_TYPES.TSArrayType) {
|
|
666
1135
|
return true;
|
|
667
1136
|
}
|
|
668
|
-
if (annotation.type ===
|
|
669
|
-
return !(annotation.typeName.type ===
|
|
1137
|
+
if (annotation.type === import_utils8.AST_NODE_TYPES.TSTypeReference) {
|
|
1138
|
+
return !(annotation.typeName.type === import_utils8.AST_NODE_TYPES.Identifier && annotation.typeName.name === "const");
|
|
670
1139
|
}
|
|
671
1140
|
return false;
|
|
672
1141
|
}
|
|
673
1142
|
var requireSchemaParseAtBoundaryRule = createRule({
|
|
674
|
-
name:
|
|
1143
|
+
name: RULE_NAME8,
|
|
675
1144
|
meta: {
|
|
676
1145
|
type: "problem",
|
|
677
1146
|
docs: {
|
|
@@ -699,10 +1168,10 @@ var requireSchemaParseAtBoundaryRule = createRule({
|
|
|
699
1168
|
});
|
|
700
1169
|
|
|
701
1170
|
// src/rules/restrict-throw-to-taxonomy.ts
|
|
702
|
-
var
|
|
703
|
-
var
|
|
1171
|
+
var import_utils9 = require("@typescript-eslint/utils");
|
|
1172
|
+
var RULE_NAME9 = "restrict-throw-to-taxonomy";
|
|
704
1173
|
var DEFAULT_ALLOW = ["Error"];
|
|
705
|
-
var
|
|
1174
|
+
var optionSchema7 = {
|
|
706
1175
|
type: "object",
|
|
707
1176
|
additionalProperties: false,
|
|
708
1177
|
properties: {
|
|
@@ -715,25 +1184,25 @@ var optionSchema6 = {
|
|
|
715
1184
|
};
|
|
716
1185
|
function constructorSimpleName2(node) {
|
|
717
1186
|
const callee = node.callee;
|
|
718
|
-
if (callee.type ===
|
|
1187
|
+
if (callee.type === import_utils9.AST_NODE_TYPES.Identifier) {
|
|
719
1188
|
return callee.name;
|
|
720
1189
|
}
|
|
721
|
-
if (callee.type ===
|
|
1190
|
+
if (callee.type === import_utils9.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils9.AST_NODE_TYPES.Identifier) {
|
|
722
1191
|
return callee.property.name;
|
|
723
1192
|
}
|
|
724
1193
|
return null;
|
|
725
1194
|
}
|
|
726
1195
|
function isNonErrorValue(node) {
|
|
727
|
-
return node.type ===
|
|
1196
|
+
return node.type === import_utils9.AST_NODE_TYPES.Literal || node.type === import_utils9.AST_NODE_TYPES.TemplateLiteral || node.type === import_utils9.AST_NODE_TYPES.ObjectExpression || node.type === import_utils9.AST_NODE_TYPES.ArrayExpression;
|
|
728
1197
|
}
|
|
729
1198
|
var restrictThrowToTaxonomyRule = createRule({
|
|
730
|
-
name:
|
|
1199
|
+
name: RULE_NAME9,
|
|
731
1200
|
meta: {
|
|
732
1201
|
type: "problem",
|
|
733
1202
|
docs: {
|
|
734
1203
|
description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
|
|
735
1204
|
},
|
|
736
|
-
schema: [
|
|
1205
|
+
schema: [optionSchema7],
|
|
737
1206
|
messages: {
|
|
738
1207
|
disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
|
|
739
1208
|
nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
|
|
@@ -746,7 +1215,7 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
746
1215
|
return {
|
|
747
1216
|
ThrowStatement(node) {
|
|
748
1217
|
const arg = node.argument;
|
|
749
|
-
if (arg.type ===
|
|
1218
|
+
if (arg.type === import_utils9.AST_NODE_TYPES.NewExpression) {
|
|
750
1219
|
const name = constructorSimpleName2(arg);
|
|
751
1220
|
if (name !== null && !allow.has(name)) {
|
|
752
1221
|
context.report({
|
|
@@ -758,7 +1227,7 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
758
1227
|
return;
|
|
759
1228
|
}
|
|
760
1229
|
if (isNonErrorValue(arg)) {
|
|
761
|
-
const kind = arg.type ===
|
|
1230
|
+
const kind = arg.type === import_utils9.AST_NODE_TYPES.ObjectExpression ? "object" : arg.type === import_utils9.AST_NODE_TYPES.ArrayExpression ? "array" : "literal";
|
|
762
1231
|
context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
|
|
763
1232
|
}
|
|
764
1233
|
}
|
|
@@ -766,10 +1235,184 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
766
1235
|
}
|
|
767
1236
|
});
|
|
768
1237
|
|
|
1238
|
+
// src/rules/schema-enum-field-consistency.ts
|
|
1239
|
+
var import_utils10 = require("@typescript-eslint/utils");
|
|
1240
|
+
var RULE_NAME10 = "schema-enum-field-consistency";
|
|
1241
|
+
var MODIFIERS = /* @__PURE__ */ new Set([
|
|
1242
|
+
"optional",
|
|
1243
|
+
"nullable",
|
|
1244
|
+
"nullish",
|
|
1245
|
+
"default",
|
|
1246
|
+
"prefault",
|
|
1247
|
+
"catch",
|
|
1248
|
+
"describe",
|
|
1249
|
+
"meta",
|
|
1250
|
+
"readonly"
|
|
1251
|
+
]);
|
|
1252
|
+
var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
|
|
1253
|
+
var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
|
|
1254
|
+
var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
|
|
1255
|
+
var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
|
|
1256
|
+
var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
|
|
1257
|
+
var UNION = /* @__PURE__ */ new Set(["union"]);
|
|
1258
|
+
var LITERAL = /* @__PURE__ */ new Set(["literal"]);
|
|
1259
|
+
var STRING = /* @__PURE__ */ new Set(["string"]);
|
|
1260
|
+
var optionSchema8 = {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
additionalProperties: false,
|
|
1263
|
+
properties: {
|
|
1264
|
+
zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
1265
|
+
ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
1266
|
+
enumIdentifierPattern: { type: "string" }
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
var OTHER = { kind: "other" };
|
|
1270
|
+
function isEnumOccurrence(occurrence) {
|
|
1271
|
+
return occurrence.kind.kind === "enum";
|
|
1272
|
+
}
|
|
1273
|
+
function methodCall(node) {
|
|
1274
|
+
if (node.type !== import_utils10.AST_NODE_TYPES.CallExpression) return null;
|
|
1275
|
+
const callee = node.callee;
|
|
1276
|
+
if (callee.type !== import_utils10.AST_NODE_TYPES.MemberExpression || callee.computed) return null;
|
|
1277
|
+
if (callee.property.type !== import_utils10.AST_NODE_TYPES.Identifier) return null;
|
|
1278
|
+
return { receiver: callee.object, method: callee.property.name, call: node };
|
|
1279
|
+
}
|
|
1280
|
+
function propertyName(property) {
|
|
1281
|
+
if (property.computed) return null;
|
|
1282
|
+
if (property.key.type === import_utils10.AST_NODE_TYPES.Identifier) return property.key.name;
|
|
1283
|
+
if (property.key.type === import_utils10.AST_NODE_TYPES.Literal && typeof property.key.value === "string") {
|
|
1284
|
+
return property.key.value;
|
|
1285
|
+
}
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
var schemaEnumFieldConsistencyRule = createRule({
|
|
1289
|
+
name: RULE_NAME10,
|
|
1290
|
+
meta: {
|
|
1291
|
+
type: "problem",
|
|
1292
|
+
docs: {
|
|
1293
|
+
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."
|
|
1294
|
+
},
|
|
1295
|
+
schema: [optionSchema8],
|
|
1296
|
+
messages: {
|
|
1297
|
+
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)."
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
|
|
1301
|
+
create(context, [options]) {
|
|
1302
|
+
const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
|
|
1303
|
+
const ignoreFields = new Set(options.ignoreFields ?? []);
|
|
1304
|
+
const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
|
|
1305
|
+
const sourceCode = context.sourceCode;
|
|
1306
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1307
|
+
function isZodCall(node, names) {
|
|
1308
|
+
const call = methodCall(node);
|
|
1309
|
+
return call !== null && call.receiver.type === import_utils10.AST_NODE_TYPES.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
|
|
1310
|
+
}
|
|
1311
|
+
function resolveVariable(identifier) {
|
|
1312
|
+
let scope = sourceCode.getScope(identifier);
|
|
1313
|
+
while (scope !== null) {
|
|
1314
|
+
const variable = scope.set.get(identifier.name);
|
|
1315
|
+
if (variable !== void 0) return variable;
|
|
1316
|
+
scope = scope.upper;
|
|
1317
|
+
}
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
function identifierIsEnum(identifier, seen) {
|
|
1321
|
+
const definition = resolveVariable(identifier)?.defs[0];
|
|
1322
|
+
if (definition === void 0) return false;
|
|
1323
|
+
if (definition.type === "ImportBinding") {
|
|
1324
|
+
return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
|
|
1325
|
+
}
|
|
1326
|
+
if (definition.type !== "Variable") return false;
|
|
1327
|
+
const init = definition.node.init;
|
|
1328
|
+
if (init === null || seen.has(init)) return false;
|
|
1329
|
+
return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
|
|
1330
|
+
}
|
|
1331
|
+
function isLiteralUnion(node) {
|
|
1332
|
+
if (!isZodCall(node, UNION)) return false;
|
|
1333
|
+
const members = node.arguments[0];
|
|
1334
|
+
if (members?.type !== import_utils10.AST_NODE_TYPES.ArrayExpression || members.elements.length === 0) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
|
|
1338
|
+
}
|
|
1339
|
+
function isMultiLiteral(node) {
|
|
1340
|
+
if (!isZodCall(node, LITERAL)) return false;
|
|
1341
|
+
const value = node.arguments[0];
|
|
1342
|
+
return value?.type === import_utils10.AST_NODE_TYPES.ArrayExpression && value.elements.length > 1;
|
|
1343
|
+
}
|
|
1344
|
+
function classify(node, seen) {
|
|
1345
|
+
let current = node;
|
|
1346
|
+
for (; ; ) {
|
|
1347
|
+
const call = methodCall(current);
|
|
1348
|
+
if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
current = call.receiver;
|
|
1352
|
+
}
|
|
1353
|
+
if (current.type === import_utils10.AST_NODE_TYPES.Identifier) {
|
|
1354
|
+
return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
|
|
1355
|
+
}
|
|
1356
|
+
if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
|
|
1357
|
+
return { kind: "enum", identifier: null };
|
|
1358
|
+
}
|
|
1359
|
+
current = node;
|
|
1360
|
+
for (; ; ) {
|
|
1361
|
+
if (isZodCall(current, STRING)) return { kind: "string" };
|
|
1362
|
+
const call = methodCall(current);
|
|
1363
|
+
if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
|
|
1364
|
+
current = call.receiver;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function collectShape(shape) {
|
|
1368
|
+
if (shape?.type !== import_utils10.AST_NODE_TYPES.ObjectExpression) return;
|
|
1369
|
+
for (const property of shape.properties) {
|
|
1370
|
+
if (property.type !== import_utils10.AST_NODE_TYPES.Property) continue;
|
|
1371
|
+
const name = propertyName(property);
|
|
1372
|
+
if (name === null || ignoreFields.has(name)) continue;
|
|
1373
|
+
const kind = classify(property.value, /* @__PURE__ */ new Set());
|
|
1374
|
+
if (kind.kind === "other") continue;
|
|
1375
|
+
const occurrences = fields.get(name) ?? [];
|
|
1376
|
+
occurrences.push({ property, kind });
|
|
1377
|
+
fields.set(name, occurrences);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
CallExpression(node) {
|
|
1382
|
+
if (isZodCall(node, OBJECT_FACTORIES)) {
|
|
1383
|
+
collectShape(node.arguments[0]);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const call = methodCall(node);
|
|
1387
|
+
if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
|
|
1388
|
+
},
|
|
1389
|
+
"Program:exit"() {
|
|
1390
|
+
for (const [field, occurrences] of fields) {
|
|
1391
|
+
const enumOccurrence = occurrences.find(isEnumOccurrence);
|
|
1392
|
+
if (enumOccurrence === void 0) continue;
|
|
1393
|
+
const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
|
|
1394
|
+
for (const occurrence of occurrences) {
|
|
1395
|
+
if (occurrence.kind.kind !== "string") continue;
|
|
1396
|
+
context.report({
|
|
1397
|
+
node: occurrence.property,
|
|
1398
|
+
messageId: "widenedEnumField",
|
|
1399
|
+
data: {
|
|
1400
|
+
field,
|
|
1401
|
+
line: String(enumOccurrence.property.loc.start.line),
|
|
1402
|
+
suggestion
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
|
|
769
1412
|
// src/rules/wire-message-naming.ts
|
|
770
|
-
var
|
|
1413
|
+
var RULE_NAME11 = "wire-message-naming";
|
|
771
1414
|
var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
|
|
772
|
-
var
|
|
1415
|
+
var optionSchema9 = {
|
|
773
1416
|
type: "object",
|
|
774
1417
|
additionalProperties: false,
|
|
775
1418
|
properties: {
|
|
@@ -810,14 +1453,14 @@ function typeLiteralNode(obj) {
|
|
|
810
1453
|
return null;
|
|
811
1454
|
}
|
|
812
1455
|
var wireMessageNamingRule = createRule({
|
|
813
|
-
name:
|
|
1456
|
+
name: RULE_NAME11,
|
|
814
1457
|
meta: {
|
|
815
1458
|
type: "problem",
|
|
816
1459
|
docs: {
|
|
817
1460
|
description: "A message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares `type: z.literal(...)` must set that literal to kebab-case(const name minus its role suffix)."
|
|
818
1461
|
},
|
|
819
1462
|
fixable: "code",
|
|
820
|
-
schema: [
|
|
1463
|
+
schema: [optionSchema9],
|
|
821
1464
|
messages: {
|
|
822
1465
|
typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
|
|
823
1466
|
}
|
|
@@ -853,11 +1496,11 @@ var wireMessageNamingRule = createRule({
|
|
|
853
1496
|
});
|
|
854
1497
|
|
|
855
1498
|
// src/rules/zod-schema-naming.ts
|
|
856
|
-
var
|
|
1499
|
+
var RULE_NAME12 = "zod-schema-naming";
|
|
857
1500
|
var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
|
|
858
1501
|
var SUFFIX = "Schema";
|
|
859
1502
|
var DEFAULT_ROLE_SUFFIXES2 = [];
|
|
860
|
-
var
|
|
1503
|
+
var optionSchema10 = {
|
|
861
1504
|
type: "object",
|
|
862
1505
|
additionalProperties: false,
|
|
863
1506
|
properties: {
|
|
@@ -890,13 +1533,13 @@ function rootIdentifierName(node) {
|
|
|
890
1533
|
return null;
|
|
891
1534
|
}
|
|
892
1535
|
var zodSchemaNamingRule = createRule({
|
|
893
|
-
name:
|
|
1536
|
+
name: RULE_NAME12,
|
|
894
1537
|
meta: {
|
|
895
1538
|
type: "problem",
|
|
896
1539
|
docs: {
|
|
897
1540
|
description: "Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type (`export type Foo = z.infer<typeof FooSchema>`)."
|
|
898
1541
|
},
|
|
899
|
-
schema: [
|
|
1542
|
+
schema: [optionSchema10],
|
|
900
1543
|
messages: {
|
|
901
1544
|
schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
|
|
902
1545
|
missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
|
|
@@ -957,12 +1600,14 @@ var rules = {
|
|
|
957
1600
|
"restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
|
|
958
1601
|
"require-registered-keys": requireRegisteredKeysRule,
|
|
959
1602
|
"env-var-schema-parity": envVarSchemaParityRule,
|
|
960
|
-
"require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
|
|
1603
|
+
"require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
|
|
1604
|
+
"schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
|
|
1605
|
+
"fetch-must-check-ok": fetchMustCheckOkRule
|
|
961
1606
|
};
|
|
962
1607
|
|
|
963
1608
|
// src/index.ts
|
|
964
1609
|
var NAMESPACE = "noctcore-contracts";
|
|
965
|
-
var VERSION = "0.
|
|
1610
|
+
var VERSION = "0.3.0";
|
|
966
1611
|
var plugin = {
|
|
967
1612
|
meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
|
|
968
1613
|
rules,
|