@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.js
CHANGED
|
@@ -7,6 +7,8 @@ var recommended = {
|
|
|
7
7
|
"noctcore-contracts/money-must-be-decimal": "error",
|
|
8
8
|
"noctcore-contracts/require-error-cause": "error",
|
|
9
9
|
"noctcore-contracts/restrict-throw-to-taxonomy": "error",
|
|
10
|
+
"noctcore-contracts/schema-enum-field-consistency": "error",
|
|
11
|
+
"noctcore-contracts/fetch-must-check-ok": "error",
|
|
10
12
|
// Config-required / heuristic rules ship inert. `require-registered-keys` and
|
|
11
13
|
// `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
|
|
12
14
|
// set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
|
|
@@ -113,9 +115,476 @@ var envVarSchemaParityRule = createRule({
|
|
|
113
115
|
}
|
|
114
116
|
});
|
|
115
117
|
|
|
116
|
-
// src/rules/
|
|
118
|
+
// src/rules/fetch-must-check-ok.ts
|
|
117
119
|
import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
|
|
118
|
-
var RULE_NAME2 = "
|
|
120
|
+
var RULE_NAME2 = "fetch-must-check-ok";
|
|
121
|
+
var optionSchema2 = {
|
|
122
|
+
type: "object",
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
properties: {
|
|
125
|
+
fetchFunctions: {
|
|
126
|
+
type: "array",
|
|
127
|
+
items: { type: "string", minLength: 1 },
|
|
128
|
+
uniqueItems: true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function isNode(value) {
|
|
133
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
134
|
+
}
|
|
135
|
+
function walkSome(root, keys, predicate) {
|
|
136
|
+
const stack = [root];
|
|
137
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
138
|
+
if (predicate(node)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
for (const key of keys[node.type] ?? []) {
|
|
142
|
+
const value = Reflect.get(node, key);
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
for (const child of value) {
|
|
145
|
+
if (isNode(child)) {
|
|
146
|
+
stack.push(child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (isNode(value)) {
|
|
150
|
+
stack.push(value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function calleePath(node) {
|
|
157
|
+
if (node.type === AST_NODE_TYPES2.Identifier) {
|
|
158
|
+
return node.name;
|
|
159
|
+
}
|
|
160
|
+
if (node.type === AST_NODE_TYPES2.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES2.Identifier) {
|
|
161
|
+
const object = calleePath(node.object);
|
|
162
|
+
return object === null ? null : `${object}.${node.property.name}`;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
var OK_PROP = "ok";
|
|
167
|
+
var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
|
|
168
|
+
var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
|
|
169
|
+
var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
|
|
170
|
+
var FIRST_ERROR_STATUS = 400;
|
|
171
|
+
function literalValue(node) {
|
|
172
|
+
if (node.type !== AST_NODE_TYPES2.Literal) {
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
|
|
176
|
+
}
|
|
177
|
+
function mirror(operator) {
|
|
178
|
+
switch (operator) {
|
|
179
|
+
case "<":
|
|
180
|
+
return ">";
|
|
181
|
+
case "<=":
|
|
182
|
+
return ">=";
|
|
183
|
+
case ">":
|
|
184
|
+
return "<";
|
|
185
|
+
case ">=":
|
|
186
|
+
return "<=";
|
|
187
|
+
default:
|
|
188
|
+
return operator;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function booleanPolarity(operator, value) {
|
|
192
|
+
if (operator === "===" || operator === "==") {
|
|
193
|
+
return value ? "positive" : "negative";
|
|
194
|
+
}
|
|
195
|
+
if (operator === "!==" || operator === "!=") {
|
|
196
|
+
return value ? "negative" : "positive";
|
|
197
|
+
}
|
|
198
|
+
return "opaque";
|
|
199
|
+
}
|
|
200
|
+
function statusPolarity(operator, value) {
|
|
201
|
+
const isSuccessCode = value >= 200 && value < 300;
|
|
202
|
+
switch (operator) {
|
|
203
|
+
case "===":
|
|
204
|
+
case "==":
|
|
205
|
+
return isSuccessCode ? "positive" : "opaque";
|
|
206
|
+
case "!==":
|
|
207
|
+
case "!=":
|
|
208
|
+
return isSuccessCode ? "negative" : "opaque";
|
|
209
|
+
case "<":
|
|
210
|
+
return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
|
|
211
|
+
case "<=":
|
|
212
|
+
return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
|
|
213
|
+
// A failure test is only useful for what it says about the OTHER side, so
|
|
214
|
+
// what matters is that everything below the threshold is a success:
|
|
215
|
+
// `>= 300` and `>= 400` both leave only good responses behind, while
|
|
216
|
+
// `>= 500` leaves every 4xx there.
|
|
217
|
+
case ">=":
|
|
218
|
+
return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
|
|
219
|
+
case ">":
|
|
220
|
+
return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
|
|
221
|
+
default:
|
|
222
|
+
return "opaque";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function comparisonPolarity(node, readIsLeft) {
|
|
226
|
+
const value = literalValue(readIsLeft ? node.right : node.left);
|
|
227
|
+
const operator = readIsLeft ? node.operator : mirror(node.operator);
|
|
228
|
+
if (typeof value === "boolean") {
|
|
229
|
+
return booleanPolarity(operator, value);
|
|
230
|
+
}
|
|
231
|
+
return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
|
|
232
|
+
}
|
|
233
|
+
function propReadOn(node, objectName, props) {
|
|
234
|
+
if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (node.object.type !== AST_NODE_TYPES2.Identifier || node.object.name !== objectName || node.property.type !== AST_NODE_TYPES2.Identifier) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const name = node.property.name;
|
|
241
|
+
return typeof props === "string" ? name === props : props.has(name);
|
|
242
|
+
}
|
|
243
|
+
function findJsonReads(root, keys, name) {
|
|
244
|
+
const reads = [];
|
|
245
|
+
walkSome(root, keys, (node) => {
|
|
246
|
+
if (propReadOn(node, name, "json")) {
|
|
247
|
+
reads.push(node);
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
});
|
|
251
|
+
return reads;
|
|
252
|
+
}
|
|
253
|
+
function isAssertionName(node) {
|
|
254
|
+
return node.type === AST_NODE_TYPES2.Identifier && ASSERTION_NAMES.test(node.name);
|
|
255
|
+
}
|
|
256
|
+
function isAssertionCallee(callee) {
|
|
257
|
+
if (callee.type === AST_NODE_TYPES2.Identifier) {
|
|
258
|
+
return isAssertionName(callee);
|
|
259
|
+
}
|
|
260
|
+
return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
|
|
261
|
+
}
|
|
262
|
+
var TERMINAL_TYPES = /* @__PURE__ */ new Set([
|
|
263
|
+
AST_NODE_TYPES2.IfStatement,
|
|
264
|
+
AST_NODE_TYPES2.WhileStatement,
|
|
265
|
+
AST_NODE_TYPES2.DoWhileStatement,
|
|
266
|
+
AST_NODE_TYPES2.ConditionalExpression,
|
|
267
|
+
AST_NODE_TYPES2.SwitchStatement,
|
|
268
|
+
AST_NODE_TYPES2.CallExpression
|
|
269
|
+
]);
|
|
270
|
+
var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
|
|
271
|
+
["equal", "==="],
|
|
272
|
+
["equals", "==="],
|
|
273
|
+
["strictEqual", "==="],
|
|
274
|
+
["deepEqual", "==="],
|
|
275
|
+
["deepStrictEqual", "==="],
|
|
276
|
+
["toBe", "==="],
|
|
277
|
+
["toEqual", "==="],
|
|
278
|
+
["notEqual", "!=="],
|
|
279
|
+
["notStrictEqual", "!=="],
|
|
280
|
+
["notDeepEqual", "!=="]
|
|
281
|
+
]);
|
|
282
|
+
function assertionOperator(callee) {
|
|
283
|
+
return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES2.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
|
|
284
|
+
}
|
|
285
|
+
function assertionCheck(parent, child, check) {
|
|
286
|
+
if (check.compared) {
|
|
287
|
+
return check;
|
|
288
|
+
}
|
|
289
|
+
const operator = assertionOperator(parent.callee);
|
|
290
|
+
if (operator === void 0) {
|
|
291
|
+
return check;
|
|
292
|
+
}
|
|
293
|
+
const other = parent.arguments.find((arg) => arg !== child);
|
|
294
|
+
const value = other === void 0 ? void 0 : literalValue(other);
|
|
295
|
+
if (typeof value === "boolean") {
|
|
296
|
+
return {
|
|
297
|
+
...check,
|
|
298
|
+
compared: true,
|
|
299
|
+
polarity: booleanPolarity(operator, value)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
|
|
303
|
+
}
|
|
304
|
+
function terminalCheck(parent, child, state) {
|
|
305
|
+
const check = { owner: parent, ...state };
|
|
306
|
+
switch (parent.type) {
|
|
307
|
+
case AST_NODE_TYPES2.IfStatement:
|
|
308
|
+
case AST_NODE_TYPES2.WhileStatement:
|
|
309
|
+
case AST_NODE_TYPES2.DoWhileStatement:
|
|
310
|
+
case AST_NODE_TYPES2.ConditionalExpression:
|
|
311
|
+
return parent.test === child ? check : null;
|
|
312
|
+
case AST_NODE_TYPES2.SwitchStatement:
|
|
313
|
+
return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
|
|
314
|
+
// An assertion settles a status only when it COMPARES it.
|
|
315
|
+
// `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
|
|
316
|
+
// number is truthy, which every response that arrived satisfies. Arity
|
|
317
|
+
// cannot tell them apart (`assert.ok(res.status, 'message')` also has two
|
|
318
|
+
// arguments), so the comparison is read from the assertion's own name.
|
|
319
|
+
case AST_NODE_TYPES2.CallExpression:
|
|
320
|
+
return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
|
|
321
|
+
default:
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function combinatorStep(parent, child, state, parse) {
|
|
326
|
+
switch (parent.type) {
|
|
327
|
+
case AST_NODE_TYPES2.UnaryExpression:
|
|
328
|
+
if (parent.operator === "typeof") {
|
|
329
|
+
return "stop";
|
|
330
|
+
}
|
|
331
|
+
if (parent.operator === "!") {
|
|
332
|
+
state.polarity = state.polarity === "positive" ? "negative" : "positive";
|
|
333
|
+
}
|
|
334
|
+
return "continue";
|
|
335
|
+
case AST_NODE_TYPES2.BinaryExpression: {
|
|
336
|
+
if (!COMPARISONS.has(parent.operator)) {
|
|
337
|
+
return "stop";
|
|
338
|
+
}
|
|
339
|
+
const polarity = comparisonPolarity(parent, parent.left === child);
|
|
340
|
+
state.compared = true;
|
|
341
|
+
state.polarity = polarity;
|
|
342
|
+
return polarity === "opaque" ? "stop" : "continue";
|
|
343
|
+
}
|
|
344
|
+
case AST_NODE_TYPES2.LogicalExpression:
|
|
345
|
+
if (parent.operator === "&&") {
|
|
346
|
+
state.underAnd = true;
|
|
347
|
+
}
|
|
348
|
+
if (parent.operator === "||") {
|
|
349
|
+
state.underOr = true;
|
|
350
|
+
}
|
|
351
|
+
return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
|
|
352
|
+
case AST_NODE_TYPES2.ChainExpression:
|
|
353
|
+
case AST_NODE_TYPES2.TSNonNullExpression:
|
|
354
|
+
return "continue";
|
|
355
|
+
default:
|
|
356
|
+
return "stop";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function climb(read, parse) {
|
|
360
|
+
const state = {
|
|
361
|
+
polarity: "positive",
|
|
362
|
+
compared: false,
|
|
363
|
+
underAnd: false,
|
|
364
|
+
underOr: false
|
|
365
|
+
};
|
|
366
|
+
let child = read;
|
|
367
|
+
let parent = read.parent;
|
|
368
|
+
while (parent !== void 0) {
|
|
369
|
+
if (TERMINAL_TYPES.has(parent.type)) {
|
|
370
|
+
return terminalCheck(parent, child, state);
|
|
371
|
+
}
|
|
372
|
+
const step = combinatorStep(parent, child, state, parse);
|
|
373
|
+
if (step === "stop") {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (step !== "continue") {
|
|
377
|
+
return step;
|
|
378
|
+
}
|
|
379
|
+
child = parent;
|
|
380
|
+
parent = parent.parent;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function contains(outer, inner) {
|
|
385
|
+
return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
|
|
386
|
+
}
|
|
387
|
+
function alwaysExits(node) {
|
|
388
|
+
if (node.type === AST_NODE_TYPES2.ReturnStatement || node.type === AST_NODE_TYPES2.ThrowStatement) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return node.type === AST_NODE_TYPES2.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
|
|
392
|
+
}
|
|
393
|
+
function scopeOfCheck(node) {
|
|
394
|
+
let current = node;
|
|
395
|
+
while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
|
|
396
|
+
current = current.parent;
|
|
397
|
+
}
|
|
398
|
+
return current.parent ?? current;
|
|
399
|
+
}
|
|
400
|
+
function isSuccessCase(arm) {
|
|
401
|
+
if (arm.test === null) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
const value = literalValue(arm.test);
|
|
405
|
+
return typeof value === "number" && value >= 200 && value < 300;
|
|
406
|
+
}
|
|
407
|
+
function switchArmProtects(owner, parse) {
|
|
408
|
+
const index = owner.cases.findIndex((arm) => contains(arm, parse));
|
|
409
|
+
const own = owner.cases[index];
|
|
410
|
+
if (own === void 0 || !isSuccessCase(own)) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
414
|
+
const arm = owner.cases[i];
|
|
415
|
+
if (arm === void 0 || arm.consequent.length > 0) {
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
if (!isSuccessCase(arm)) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
function branchProtects(check, parse) {
|
|
425
|
+
const { owner } = check;
|
|
426
|
+
if (owner.type === AST_NODE_TYPES2.SwitchStatement) {
|
|
427
|
+
return switchArmProtects(owner, parse);
|
|
428
|
+
}
|
|
429
|
+
if (owner.type === AST_NODE_TYPES2.LogicalExpression) {
|
|
430
|
+
if (!contains(owner.right, parse)) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (owner.operator === "&&") {
|
|
434
|
+
return check.polarity === "positive";
|
|
435
|
+
}
|
|
436
|
+
return owner.operator === "||" && check.polarity === "negative";
|
|
437
|
+
}
|
|
438
|
+
if (owner.type === AST_NODE_TYPES2.WhileStatement || owner.type === AST_NODE_TYPES2.DoWhileStatement) {
|
|
439
|
+
return contains(owner.body, parse) && entersOnSuccess(check);
|
|
440
|
+
}
|
|
441
|
+
if (owner.type !== AST_NODE_TYPES2.ConditionalExpression && owner.type !== AST_NODE_TYPES2.IfStatement) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (contains(owner.consequent, parse)) {
|
|
445
|
+
return entersOnSuccess(check);
|
|
446
|
+
}
|
|
447
|
+
return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
|
|
448
|
+
}
|
|
449
|
+
function entersOnSuccess(check) {
|
|
450
|
+
return check.polarity === "positive" && !check.underOr;
|
|
451
|
+
}
|
|
452
|
+
function skipsOnSuccess(check) {
|
|
453
|
+
return check.polarity === "negative" && !check.underAnd;
|
|
454
|
+
}
|
|
455
|
+
function guardProtects(check, parse) {
|
|
456
|
+
const { owner } = check;
|
|
457
|
+
if (owner.type === AST_NODE_TYPES2.CallExpression) {
|
|
458
|
+
return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
|
|
459
|
+
}
|
|
460
|
+
if (owner.type !== AST_NODE_TYPES2.IfStatement) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (alwaysExits(owner.consequent)) {
|
|
467
|
+
return skipsOnSuccess(check);
|
|
468
|
+
}
|
|
469
|
+
return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
|
|
470
|
+
}
|
|
471
|
+
function protects(check, parse) {
|
|
472
|
+
if (!check.compared) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
return branchProtects(check, parse) || guardProtects(check, parse);
|
|
476
|
+
}
|
|
477
|
+
function statusAliases(root, keys, name) {
|
|
478
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
479
|
+
walkSome(root, keys, (node) => {
|
|
480
|
+
if (node.type === AST_NODE_TYPES2.VariableDeclarator && node.id.type === AST_NODE_TYPES2.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === AST_NODE_TYPES2.Identifier) {
|
|
481
|
+
aliases.set(node.id.name, node.init.property.name);
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
});
|
|
485
|
+
return aliases;
|
|
486
|
+
}
|
|
487
|
+
function checkedProp(node, name, aliases) {
|
|
488
|
+
if (propReadOn(node, name, OK_PROPS)) {
|
|
489
|
+
return node.property.type === AST_NODE_TYPES2.Identifier ? node.property.name : void 0;
|
|
490
|
+
}
|
|
491
|
+
return node.type === AST_NODE_TYPES2.Identifier ? aliases.get(node.name) : void 0;
|
|
492
|
+
}
|
|
493
|
+
function isProtected(root, keys, name, aliases, parse) {
|
|
494
|
+
return walkSome(root, keys, (node) => {
|
|
495
|
+
const prop = checkedProp(node, name, aliases);
|
|
496
|
+
if (prop === void 0) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
const check = climb(node, parse);
|
|
500
|
+
if (check === null) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
return protects(
|
|
504
|
+
prop === OK_PROP ? { ...check, compared: true } : check,
|
|
505
|
+
parse
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
function scopeOf(node) {
|
|
510
|
+
let current = node.parent;
|
|
511
|
+
while (current !== void 0) {
|
|
512
|
+
if (current.type === AST_NODE_TYPES2.BlockStatement || current.type === AST_NODE_TYPES2.Program) {
|
|
513
|
+
return current;
|
|
514
|
+
}
|
|
515
|
+
current = current.parent;
|
|
516
|
+
}
|
|
517
|
+
return node;
|
|
518
|
+
}
|
|
519
|
+
function skipAwait(node) {
|
|
520
|
+
return node?.type === AST_NODE_TYPES2.AwaitExpression ? node.parent : node;
|
|
521
|
+
}
|
|
522
|
+
function thenCallbackParam(node) {
|
|
523
|
+
if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed || node.property.type !== AST_NODE_TYPES2.Identifier || node.property.name !== "then" || node.parent.type !== AST_NODE_TYPES2.CallExpression) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
const callback = node.parent.arguments[0];
|
|
527
|
+
if (callback === void 0 || callback.type !== AST_NODE_TYPES2.ArrowFunctionExpression && callback.type !== AST_NODE_TYPES2.FunctionExpression) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const param = callback.params[0];
|
|
531
|
+
return param?.type === AST_NODE_TYPES2.Identifier ? { name: param.name, body: callback.body } : null;
|
|
532
|
+
}
|
|
533
|
+
var fetchMustCheckOkRule = createRule({
|
|
534
|
+
name: RULE_NAME2,
|
|
535
|
+
meta: {
|
|
536
|
+
type: "problem",
|
|
537
|
+
docs: {
|
|
538
|
+
description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
|
|
539
|
+
},
|
|
540
|
+
schema: [optionSchema2],
|
|
541
|
+
messages: {
|
|
542
|
+
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."
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
defaultOptions: [{ fetchFunctions: ["fetch"] }],
|
|
546
|
+
create(context, [options]) {
|
|
547
|
+
const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
|
|
548
|
+
const keys = context.sourceCode.visitorKeys;
|
|
549
|
+
function reportUnprotected(root, name) {
|
|
550
|
+
const aliases = statusAliases(root, keys, name);
|
|
551
|
+
for (const read of findJsonReads(root, keys, name)) {
|
|
552
|
+
if (!isProtected(root, keys, name, aliases, read)) {
|
|
553
|
+
context.report({ node: read, messageId: "missingOkCheck" });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
CallExpression(node) {
|
|
559
|
+
const path2 = calleePath(node.callee);
|
|
560
|
+
if (path2 === null || !fetchFunctions.has(path2)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const parent = skipAwait(node.parent);
|
|
564
|
+
if (parent === void 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (parent.type === AST_NODE_TYPES2.MemberExpression && !parent.computed && parent.property.type === AST_NODE_TYPES2.Identifier && parent.property.name === "json") {
|
|
568
|
+
context.report({ node: parent, messageId: "missingOkCheck" });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const callback = thenCallbackParam(parent);
|
|
572
|
+
if (callback !== null) {
|
|
573
|
+
reportUnprotected(callback.body, callback.name);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (parent.type !== AST_NODE_TYPES2.VariableDeclarator || parent.id.type !== AST_NODE_TYPES2.Identifier) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
reportUnprotected(scopeOf(parent), parent.id.name);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// src/rules/money-must-be-decimal.ts
|
|
586
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
|
|
587
|
+
var RULE_NAME3 = "money-must-be-decimal";
|
|
119
588
|
var DEFAULT_DECIMAL_TYPE = "Decimal";
|
|
120
589
|
var DEFAULT_FIELD_PATTERNS = [
|
|
121
590
|
"amount",
|
|
@@ -125,7 +594,7 @@ var DEFAULT_FIELD_PATTERNS = [
|
|
|
125
594
|
"balance"
|
|
126
595
|
];
|
|
127
596
|
var DEFAULT_ALLOWED_FILES = [];
|
|
128
|
-
var
|
|
597
|
+
var optionSchema3 = {
|
|
129
598
|
type: "object",
|
|
130
599
|
additionalProperties: false,
|
|
131
600
|
properties: {
|
|
@@ -154,22 +623,22 @@ function isAllowedFile(filename, patterns) {
|
|
|
154
623
|
return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
|
|
155
624
|
}
|
|
156
625
|
function staticName(node) {
|
|
157
|
-
if (node.type ===
|
|
626
|
+
if (node.type === AST_NODE_TYPES3.Identifier) {
|
|
158
627
|
return node.name;
|
|
159
628
|
}
|
|
160
629
|
return void 0;
|
|
161
630
|
}
|
|
162
631
|
function isNumberAnnotation(annotation) {
|
|
163
|
-
return annotation?.typeAnnotation.type ===
|
|
632
|
+
return annotation?.typeAnnotation.type === AST_NODE_TYPES3.TSNumberKeyword;
|
|
164
633
|
}
|
|
165
634
|
var moneyMustBeDecimalRule = createRule({
|
|
166
|
-
name:
|
|
635
|
+
name: RULE_NAME3,
|
|
167
636
|
meta: {
|
|
168
637
|
type: "problem",
|
|
169
638
|
docs: {
|
|
170
639
|
description: "Disallow monetary values typed as the JS primitive `number`. Money-named fields explicitly typed `: number` lose precision to float rounding; use a Decimal money type instead."
|
|
171
640
|
},
|
|
172
|
-
schema: [
|
|
641
|
+
schema: [optionSchema3],
|
|
173
642
|
messages: {
|
|
174
643
|
moneyMustBeDecimal: "Monetary values must use {{decimalType}}, never the JS `number` primitive, to avoid float rounding errors. Rename or retype this field to a {{decimalType}} money type."
|
|
175
644
|
}
|
|
@@ -205,7 +674,7 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
205
674
|
},
|
|
206
675
|
// `const total: number = ...`: annotated variable declarator.
|
|
207
676
|
VariableDeclarator(node) {
|
|
208
|
-
if (node.id.type !==
|
|
677
|
+
if (node.id.type !== AST_NODE_TYPES3.Identifier) {
|
|
209
678
|
return;
|
|
210
679
|
}
|
|
211
680
|
const name = node.id.name;
|
|
@@ -218,15 +687,15 @@ var moneyMustBeDecimalRule = createRule({
|
|
|
218
687
|
});
|
|
219
688
|
|
|
220
689
|
// src/rules/no-direct-process-env.ts
|
|
221
|
-
import { AST_NODE_TYPES as
|
|
222
|
-
var
|
|
690
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
|
|
691
|
+
var RULE_NAME4 = "no-direct-process-env";
|
|
223
692
|
var DEFAULT_CONFIG_MODULE = "@/config";
|
|
224
693
|
var DEFAULT_ALLOWED_FILES2 = [
|
|
225
694
|
"**/*.config.{ts,js,mjs,cjs}",
|
|
226
695
|
"**/*.{spec,test}.{ts,tsx}",
|
|
227
696
|
"**/scripts/**"
|
|
228
697
|
];
|
|
229
|
-
var
|
|
698
|
+
var optionSchema4 = {
|
|
230
699
|
type: "object",
|
|
231
700
|
additionalProperties: false,
|
|
232
701
|
properties: {
|
|
@@ -281,22 +750,22 @@ function isAllowedFile2(filename, patterns) {
|
|
|
281
750
|
return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
|
|
282
751
|
}
|
|
283
752
|
function isProcessEnv2(node) {
|
|
284
|
-
if (node.type !==
|
|
753
|
+
if (node.type !== AST_NODE_TYPES4.MemberExpression || node.object.type !== AST_NODE_TYPES4.Identifier || node.object.name !== "process") {
|
|
285
754
|
return false;
|
|
286
755
|
}
|
|
287
756
|
if (node.computed) {
|
|
288
|
-
return node.property.type ===
|
|
757
|
+
return node.property.type === AST_NODE_TYPES4.Literal && node.property.value === "env";
|
|
289
758
|
}
|
|
290
|
-
return node.property.type ===
|
|
759
|
+
return node.property.type === AST_NODE_TYPES4.Identifier && node.property.name === "env";
|
|
291
760
|
}
|
|
292
761
|
var noDirectProcessEnvRule = createRule({
|
|
293
|
-
name:
|
|
762
|
+
name: RULE_NAME4,
|
|
294
763
|
meta: {
|
|
295
764
|
type: "problem",
|
|
296
765
|
docs: {
|
|
297
766
|
description: "Disallow direct `process.env` access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use."
|
|
298
767
|
},
|
|
299
|
-
schema: [
|
|
768
|
+
schema: [optionSchema4],
|
|
300
769
|
messages: {
|
|
301
770
|
directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
|
|
302
771
|
}
|
|
@@ -331,10 +800,10 @@ var noDirectProcessEnvRule = createRule({
|
|
|
331
800
|
});
|
|
332
801
|
|
|
333
802
|
// src/rules/no-error-stringify.ts
|
|
334
|
-
import { AST_NODE_TYPES as
|
|
335
|
-
var
|
|
803
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
|
|
804
|
+
var RULE_NAME5 = "no-error-stringify";
|
|
336
805
|
var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
|
|
337
|
-
var
|
|
806
|
+
var optionSchema5 = {
|
|
338
807
|
type: "object",
|
|
339
808
|
additionalProperties: false,
|
|
340
809
|
properties: {
|
|
@@ -347,19 +816,19 @@ var optionSchema4 = {
|
|
|
347
816
|
}
|
|
348
817
|
};
|
|
349
818
|
function isEmptyStringLiteral(node) {
|
|
350
|
-
return node.type ===
|
|
819
|
+
return node.type === AST_NODE_TYPES5.Literal && node.value === "";
|
|
351
820
|
}
|
|
352
821
|
function isErrorIdentifier(node, names) {
|
|
353
|
-
return node.type ===
|
|
822
|
+
return node.type === AST_NODE_TYPES5.Identifier && names.has(node.name);
|
|
354
823
|
}
|
|
355
824
|
var noErrorStringifyRule = createRule({
|
|
356
|
-
name:
|
|
825
|
+
name: RULE_NAME5,
|
|
357
826
|
meta: {
|
|
358
827
|
type: "problem",
|
|
359
828
|
docs: {
|
|
360
829
|
description: 'Disallow stringifying an error with bare `${error}` interpolation, `error.toString()`, or `error + ""`. These drop the cause chain. Use `error instanceof Error ? error.message : String(error)` instead.'
|
|
361
830
|
},
|
|
362
|
-
schema: [
|
|
831
|
+
schema: [optionSchema5],
|
|
363
832
|
messages: {
|
|
364
833
|
noErrorStringify: "Stringifying an error this way drops its cause chain. Use `{{name}} instanceof Error ? {{name}}.message : String({{name}})` (or pass the Error object straight to the logger)."
|
|
365
834
|
}
|
|
@@ -374,7 +843,7 @@ var noErrorStringifyRule = createRule({
|
|
|
374
843
|
// `error.toString()`
|
|
375
844
|
'CallExpression[callee.type="MemberExpression"]'(node) {
|
|
376
845
|
const callee = node.callee;
|
|
377
|
-
if (!callee.computed && callee.property.type ===
|
|
846
|
+
if (!callee.computed && callee.property.type === AST_NODE_TYPES5.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
|
|
378
847
|
report(node, callee.object.name);
|
|
379
848
|
}
|
|
380
849
|
},
|
|
@@ -407,14 +876,14 @@ var noErrorStringifyRule = createRule({
|
|
|
407
876
|
});
|
|
408
877
|
|
|
409
878
|
// src/rules/require-error-cause.ts
|
|
410
|
-
import { AST_NODE_TYPES as
|
|
411
|
-
var
|
|
879
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
|
|
880
|
+
var RULE_NAME6 = "require-error-cause";
|
|
412
881
|
function constructorSimpleName(node) {
|
|
413
882
|
const callee = node.callee;
|
|
414
|
-
if (callee.type ===
|
|
883
|
+
if (callee.type === AST_NODE_TYPES6.Identifier) {
|
|
415
884
|
return callee.name;
|
|
416
885
|
}
|
|
417
|
-
if (callee.type ===
|
|
886
|
+
if (callee.type === AST_NODE_TYPES6.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES6.Identifier) {
|
|
418
887
|
return callee.property.name;
|
|
419
888
|
}
|
|
420
889
|
return null;
|
|
@@ -424,16 +893,16 @@ function isErrorLikeName(name) {
|
|
|
424
893
|
}
|
|
425
894
|
function alreadyHasCause(node) {
|
|
426
895
|
for (const arg of node.arguments) {
|
|
427
|
-
if (arg.type ===
|
|
896
|
+
if (arg.type === AST_NODE_TYPES6.SpreadElement) {
|
|
428
897
|
return true;
|
|
429
898
|
}
|
|
430
|
-
if (arg.type ===
|
|
899
|
+
if (arg.type === AST_NODE_TYPES6.ObjectExpression) {
|
|
431
900
|
for (const prop of arg.properties) {
|
|
432
|
-
if (prop.type ===
|
|
901
|
+
if (prop.type === AST_NODE_TYPES6.SpreadElement) {
|
|
433
902
|
return true;
|
|
434
903
|
}
|
|
435
904
|
const key = prop.key;
|
|
436
|
-
const isCause = key.type ===
|
|
905
|
+
const isCause = key.type === AST_NODE_TYPES6.Identifier && key.name === "cause" || key.type === AST_NODE_TYPES6.Literal && key.value === "cause";
|
|
437
906
|
if (isCause) {
|
|
438
907
|
return true;
|
|
439
908
|
}
|
|
@@ -451,7 +920,7 @@ function buildFix(node, binding) {
|
|
|
451
920
|
if (last === void 0) {
|
|
452
921
|
return null;
|
|
453
922
|
}
|
|
454
|
-
if (last.type ===
|
|
923
|
+
if (last.type === AST_NODE_TYPES6.ObjectExpression) {
|
|
455
924
|
const props = last.properties;
|
|
456
925
|
if (props.length === 0) {
|
|
457
926
|
return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
|
|
@@ -465,7 +934,7 @@ function buildFix(node, binding) {
|
|
|
465
934
|
return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
|
|
466
935
|
}
|
|
467
936
|
var requireErrorCauseRule = createRule({
|
|
468
|
-
name:
|
|
937
|
+
name: RULE_NAME6,
|
|
469
938
|
meta: {
|
|
470
939
|
type: "problem",
|
|
471
940
|
docs: {
|
|
@@ -484,7 +953,7 @@ var requireErrorCauseRule = createRule({
|
|
|
484
953
|
CatchClause(node) {
|
|
485
954
|
const param = node.param;
|
|
486
955
|
catchBindings.push(
|
|
487
|
-
param && param.type ===
|
|
956
|
+
param && param.type === AST_NODE_TYPES6.Identifier ? param.name : null
|
|
488
957
|
);
|
|
489
958
|
},
|
|
490
959
|
"CatchClause:exit"() {
|
|
@@ -496,7 +965,7 @@ var requireErrorCauseRule = createRule({
|
|
|
496
965
|
return;
|
|
497
966
|
}
|
|
498
967
|
const arg = node.argument;
|
|
499
|
-
if (arg.type !==
|
|
968
|
+
if (arg.type !== AST_NODE_TYPES6.NewExpression) {
|
|
500
969
|
return;
|
|
501
970
|
}
|
|
502
971
|
const ctor = constructorSimpleName(arg);
|
|
@@ -519,9 +988,9 @@ var requireErrorCauseRule = createRule({
|
|
|
519
988
|
});
|
|
520
989
|
|
|
521
990
|
// src/rules/require-registered-keys.ts
|
|
522
|
-
import { AST_NODE_TYPES as
|
|
523
|
-
var
|
|
524
|
-
var
|
|
991
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
|
|
992
|
+
var RULE_NAME7 = "require-registered-keys";
|
|
993
|
+
var optionSchema6 = {
|
|
525
994
|
type: "object",
|
|
526
995
|
additionalProperties: false,
|
|
527
996
|
properties: {
|
|
@@ -540,30 +1009,30 @@ var optionSchema5 = {
|
|
|
540
1009
|
registry: { type: "string", minLength: 1 }
|
|
541
1010
|
}
|
|
542
1011
|
};
|
|
543
|
-
function
|
|
544
|
-
if (callee.type ===
|
|
1012
|
+
function calleePath2(callee) {
|
|
1013
|
+
if (callee.type === AST_NODE_TYPES7.Identifier) {
|
|
545
1014
|
return callee.name;
|
|
546
1015
|
}
|
|
547
|
-
if (callee.type ===
|
|
548
|
-
if (callee.property.type !==
|
|
1016
|
+
if (callee.type === AST_NODE_TYPES7.MemberExpression && !callee.computed) {
|
|
1017
|
+
if (callee.property.type !== AST_NODE_TYPES7.Identifier) {
|
|
549
1018
|
return null;
|
|
550
1019
|
}
|
|
551
|
-
const objectPath =
|
|
1020
|
+
const objectPath = calleePath2(callee.object);
|
|
552
1021
|
return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
|
|
553
1022
|
}
|
|
554
1023
|
return null;
|
|
555
1024
|
}
|
|
556
1025
|
function isStringLiteral(node) {
|
|
557
|
-
return node.type ===
|
|
1026
|
+
return node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string";
|
|
558
1027
|
}
|
|
559
1028
|
var requireRegisteredKeysRule = createRule({
|
|
560
|
-
name:
|
|
1029
|
+
name: RULE_NAME7,
|
|
561
1030
|
meta: {
|
|
562
1031
|
type: "suggestion",
|
|
563
1032
|
docs: {
|
|
564
1033
|
description: "Require the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal."
|
|
565
1034
|
},
|
|
566
|
-
schema: [
|
|
1035
|
+
schema: [optionSchema6],
|
|
567
1036
|
messages: {
|
|
568
1037
|
unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
|
|
569
1038
|
}
|
|
@@ -587,7 +1056,7 @@ var requireRegisteredKeysRule = createRule({
|
|
|
587
1056
|
const registryHint = registry ? ` (import it from '${registry}')` : "";
|
|
588
1057
|
return {
|
|
589
1058
|
CallExpression(node) {
|
|
590
|
-
const path2 =
|
|
1059
|
+
const path2 = calleePath2(node.callee);
|
|
591
1060
|
if (path2 === null) {
|
|
592
1061
|
return;
|
|
593
1062
|
}
|
|
@@ -611,29 +1080,29 @@ var requireRegisteredKeysRule = createRule({
|
|
|
611
1080
|
});
|
|
612
1081
|
|
|
613
1082
|
// src/rules/require-schema-parse-at-boundary.ts
|
|
614
|
-
import { AST_NODE_TYPES as
|
|
615
|
-
var
|
|
1083
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
|
|
1084
|
+
var RULE_NAME8 = "require-schema-parse-at-boundary";
|
|
616
1085
|
function isJsonParseCall(node) {
|
|
617
|
-
return node.type ===
|
|
1086
|
+
return node.type === AST_NODE_TYPES8.CallExpression && node.callee.type === AST_NODE_TYPES8.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES8.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === AST_NODE_TYPES8.Identifier && node.callee.property.name === "parse";
|
|
618
1087
|
}
|
|
619
1088
|
function isAwaitJsonCall(node) {
|
|
620
|
-
if (node.type !==
|
|
1089
|
+
if (node.type !== AST_NODE_TYPES8.AwaitExpression) {
|
|
621
1090
|
return false;
|
|
622
1091
|
}
|
|
623
1092
|
const call = node.argument;
|
|
624
|
-
return call.type ===
|
|
1093
|
+
return call.type === AST_NODE_TYPES8.CallExpression && call.arguments.length === 0 && call.callee.type === AST_NODE_TYPES8.MemberExpression && !call.callee.computed && call.callee.property.type === AST_NODE_TYPES8.Identifier && call.callee.property.name === "json";
|
|
625
1094
|
}
|
|
626
1095
|
function isShapeClaim(annotation) {
|
|
627
|
-
if (annotation.type ===
|
|
1096
|
+
if (annotation.type === AST_NODE_TYPES8.TSArrayType) {
|
|
628
1097
|
return true;
|
|
629
1098
|
}
|
|
630
|
-
if (annotation.type ===
|
|
631
|
-
return !(annotation.typeName.type ===
|
|
1099
|
+
if (annotation.type === AST_NODE_TYPES8.TSTypeReference) {
|
|
1100
|
+
return !(annotation.typeName.type === AST_NODE_TYPES8.Identifier && annotation.typeName.name === "const");
|
|
632
1101
|
}
|
|
633
1102
|
return false;
|
|
634
1103
|
}
|
|
635
1104
|
var requireSchemaParseAtBoundaryRule = createRule({
|
|
636
|
-
name:
|
|
1105
|
+
name: RULE_NAME8,
|
|
637
1106
|
meta: {
|
|
638
1107
|
type: "problem",
|
|
639
1108
|
docs: {
|
|
@@ -661,10 +1130,10 @@ var requireSchemaParseAtBoundaryRule = createRule({
|
|
|
661
1130
|
});
|
|
662
1131
|
|
|
663
1132
|
// src/rules/restrict-throw-to-taxonomy.ts
|
|
664
|
-
import { AST_NODE_TYPES as
|
|
665
|
-
var
|
|
1133
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
|
|
1134
|
+
var RULE_NAME9 = "restrict-throw-to-taxonomy";
|
|
666
1135
|
var DEFAULT_ALLOW = ["Error"];
|
|
667
|
-
var
|
|
1136
|
+
var optionSchema7 = {
|
|
668
1137
|
type: "object",
|
|
669
1138
|
additionalProperties: false,
|
|
670
1139
|
properties: {
|
|
@@ -677,25 +1146,25 @@ var optionSchema6 = {
|
|
|
677
1146
|
};
|
|
678
1147
|
function constructorSimpleName2(node) {
|
|
679
1148
|
const callee = node.callee;
|
|
680
|
-
if (callee.type ===
|
|
1149
|
+
if (callee.type === AST_NODE_TYPES9.Identifier) {
|
|
681
1150
|
return callee.name;
|
|
682
1151
|
}
|
|
683
|
-
if (callee.type ===
|
|
1152
|
+
if (callee.type === AST_NODE_TYPES9.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES9.Identifier) {
|
|
684
1153
|
return callee.property.name;
|
|
685
1154
|
}
|
|
686
1155
|
return null;
|
|
687
1156
|
}
|
|
688
1157
|
function isNonErrorValue(node) {
|
|
689
|
-
return node.type ===
|
|
1158
|
+
return node.type === AST_NODE_TYPES9.Literal || node.type === AST_NODE_TYPES9.TemplateLiteral || node.type === AST_NODE_TYPES9.ObjectExpression || node.type === AST_NODE_TYPES9.ArrayExpression;
|
|
690
1159
|
}
|
|
691
1160
|
var restrictThrowToTaxonomyRule = createRule({
|
|
692
|
-
name:
|
|
1161
|
+
name: RULE_NAME9,
|
|
693
1162
|
meta: {
|
|
694
1163
|
type: "problem",
|
|
695
1164
|
docs: {
|
|
696
1165
|
description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
|
|
697
1166
|
},
|
|
698
|
-
schema: [
|
|
1167
|
+
schema: [optionSchema7],
|
|
699
1168
|
messages: {
|
|
700
1169
|
disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
|
|
701
1170
|
nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
|
|
@@ -708,7 +1177,7 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
708
1177
|
return {
|
|
709
1178
|
ThrowStatement(node) {
|
|
710
1179
|
const arg = node.argument;
|
|
711
|
-
if (arg.type ===
|
|
1180
|
+
if (arg.type === AST_NODE_TYPES9.NewExpression) {
|
|
712
1181
|
const name = constructorSimpleName2(arg);
|
|
713
1182
|
if (name !== null && !allow.has(name)) {
|
|
714
1183
|
context.report({
|
|
@@ -720,7 +1189,7 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
720
1189
|
return;
|
|
721
1190
|
}
|
|
722
1191
|
if (isNonErrorValue(arg)) {
|
|
723
|
-
const kind = arg.type ===
|
|
1192
|
+
const kind = arg.type === AST_NODE_TYPES9.ObjectExpression ? "object" : arg.type === AST_NODE_TYPES9.ArrayExpression ? "array" : "literal";
|
|
724
1193
|
context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
|
|
725
1194
|
}
|
|
726
1195
|
}
|
|
@@ -728,10 +1197,184 @@ var restrictThrowToTaxonomyRule = createRule({
|
|
|
728
1197
|
}
|
|
729
1198
|
});
|
|
730
1199
|
|
|
1200
|
+
// src/rules/schema-enum-field-consistency.ts
|
|
1201
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
|
|
1202
|
+
var RULE_NAME10 = "schema-enum-field-consistency";
|
|
1203
|
+
var MODIFIERS = /* @__PURE__ */ new Set([
|
|
1204
|
+
"optional",
|
|
1205
|
+
"nullable",
|
|
1206
|
+
"nullish",
|
|
1207
|
+
"default",
|
|
1208
|
+
"prefault",
|
|
1209
|
+
"catch",
|
|
1210
|
+
"describe",
|
|
1211
|
+
"meta",
|
|
1212
|
+
"readonly"
|
|
1213
|
+
]);
|
|
1214
|
+
var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
|
|
1215
|
+
var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
|
|
1216
|
+
var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
|
|
1217
|
+
var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
|
|
1218
|
+
var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
|
|
1219
|
+
var UNION = /* @__PURE__ */ new Set(["union"]);
|
|
1220
|
+
var LITERAL = /* @__PURE__ */ new Set(["literal"]);
|
|
1221
|
+
var STRING = /* @__PURE__ */ new Set(["string"]);
|
|
1222
|
+
var optionSchema8 = {
|
|
1223
|
+
type: "object",
|
|
1224
|
+
additionalProperties: false,
|
|
1225
|
+
properties: {
|
|
1226
|
+
zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
1227
|
+
ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
1228
|
+
enumIdentifierPattern: { type: "string" }
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
var OTHER = { kind: "other" };
|
|
1232
|
+
function isEnumOccurrence(occurrence) {
|
|
1233
|
+
return occurrence.kind.kind === "enum";
|
|
1234
|
+
}
|
|
1235
|
+
function methodCall(node) {
|
|
1236
|
+
if (node.type !== AST_NODE_TYPES10.CallExpression) return null;
|
|
1237
|
+
const callee = node.callee;
|
|
1238
|
+
if (callee.type !== AST_NODE_TYPES10.MemberExpression || callee.computed) return null;
|
|
1239
|
+
if (callee.property.type !== AST_NODE_TYPES10.Identifier) return null;
|
|
1240
|
+
return { receiver: callee.object, method: callee.property.name, call: node };
|
|
1241
|
+
}
|
|
1242
|
+
function propertyName(property) {
|
|
1243
|
+
if (property.computed) return null;
|
|
1244
|
+
if (property.key.type === AST_NODE_TYPES10.Identifier) return property.key.name;
|
|
1245
|
+
if (property.key.type === AST_NODE_TYPES10.Literal && typeof property.key.value === "string") {
|
|
1246
|
+
return property.key.value;
|
|
1247
|
+
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
var schemaEnumFieldConsistencyRule = createRule({
|
|
1251
|
+
name: RULE_NAME10,
|
|
1252
|
+
meta: {
|
|
1253
|
+
type: "problem",
|
|
1254
|
+
docs: {
|
|
1255
|
+
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."
|
|
1256
|
+
},
|
|
1257
|
+
schema: [optionSchema8],
|
|
1258
|
+
messages: {
|
|
1259
|
+
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)."
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
|
|
1263
|
+
create(context, [options]) {
|
|
1264
|
+
const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
|
|
1265
|
+
const ignoreFields = new Set(options.ignoreFields ?? []);
|
|
1266
|
+
const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
|
|
1267
|
+
const sourceCode = context.sourceCode;
|
|
1268
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1269
|
+
function isZodCall(node, names) {
|
|
1270
|
+
const call = methodCall(node);
|
|
1271
|
+
return call !== null && call.receiver.type === AST_NODE_TYPES10.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
|
|
1272
|
+
}
|
|
1273
|
+
function resolveVariable(identifier) {
|
|
1274
|
+
let scope = sourceCode.getScope(identifier);
|
|
1275
|
+
while (scope !== null) {
|
|
1276
|
+
const variable = scope.set.get(identifier.name);
|
|
1277
|
+
if (variable !== void 0) return variable;
|
|
1278
|
+
scope = scope.upper;
|
|
1279
|
+
}
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
function identifierIsEnum(identifier, seen) {
|
|
1283
|
+
const definition = resolveVariable(identifier)?.defs[0];
|
|
1284
|
+
if (definition === void 0) return false;
|
|
1285
|
+
if (definition.type === "ImportBinding") {
|
|
1286
|
+
return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
|
|
1287
|
+
}
|
|
1288
|
+
if (definition.type !== "Variable") return false;
|
|
1289
|
+
const init = definition.node.init;
|
|
1290
|
+
if (init === null || seen.has(init)) return false;
|
|
1291
|
+
return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
|
|
1292
|
+
}
|
|
1293
|
+
function isLiteralUnion(node) {
|
|
1294
|
+
if (!isZodCall(node, UNION)) return false;
|
|
1295
|
+
const members = node.arguments[0];
|
|
1296
|
+
if (members?.type !== AST_NODE_TYPES10.ArrayExpression || members.elements.length === 0) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
|
|
1300
|
+
}
|
|
1301
|
+
function isMultiLiteral(node) {
|
|
1302
|
+
if (!isZodCall(node, LITERAL)) return false;
|
|
1303
|
+
const value = node.arguments[0];
|
|
1304
|
+
return value?.type === AST_NODE_TYPES10.ArrayExpression && value.elements.length > 1;
|
|
1305
|
+
}
|
|
1306
|
+
function classify(node, seen) {
|
|
1307
|
+
let current = node;
|
|
1308
|
+
for (; ; ) {
|
|
1309
|
+
const call = methodCall(current);
|
|
1310
|
+
if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
current = call.receiver;
|
|
1314
|
+
}
|
|
1315
|
+
if (current.type === AST_NODE_TYPES10.Identifier) {
|
|
1316
|
+
return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
|
|
1317
|
+
}
|
|
1318
|
+
if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
|
|
1319
|
+
return { kind: "enum", identifier: null };
|
|
1320
|
+
}
|
|
1321
|
+
current = node;
|
|
1322
|
+
for (; ; ) {
|
|
1323
|
+
if (isZodCall(current, STRING)) return { kind: "string" };
|
|
1324
|
+
const call = methodCall(current);
|
|
1325
|
+
if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
|
|
1326
|
+
current = call.receiver;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function collectShape(shape) {
|
|
1330
|
+
if (shape?.type !== AST_NODE_TYPES10.ObjectExpression) return;
|
|
1331
|
+
for (const property of shape.properties) {
|
|
1332
|
+
if (property.type !== AST_NODE_TYPES10.Property) continue;
|
|
1333
|
+
const name = propertyName(property);
|
|
1334
|
+
if (name === null || ignoreFields.has(name)) continue;
|
|
1335
|
+
const kind = classify(property.value, /* @__PURE__ */ new Set());
|
|
1336
|
+
if (kind.kind === "other") continue;
|
|
1337
|
+
const occurrences = fields.get(name) ?? [];
|
|
1338
|
+
occurrences.push({ property, kind });
|
|
1339
|
+
fields.set(name, occurrences);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
CallExpression(node) {
|
|
1344
|
+
if (isZodCall(node, OBJECT_FACTORIES)) {
|
|
1345
|
+
collectShape(node.arguments[0]);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const call = methodCall(node);
|
|
1349
|
+
if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
|
|
1350
|
+
},
|
|
1351
|
+
"Program:exit"() {
|
|
1352
|
+
for (const [field, occurrences] of fields) {
|
|
1353
|
+
const enumOccurrence = occurrences.find(isEnumOccurrence);
|
|
1354
|
+
if (enumOccurrence === void 0) continue;
|
|
1355
|
+
const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
|
|
1356
|
+
for (const occurrence of occurrences) {
|
|
1357
|
+
if (occurrence.kind.kind !== "string") continue;
|
|
1358
|
+
context.report({
|
|
1359
|
+
node: occurrence.property,
|
|
1360
|
+
messageId: "widenedEnumField",
|
|
1361
|
+
data: {
|
|
1362
|
+
field,
|
|
1363
|
+
line: String(enumOccurrence.property.loc.start.line),
|
|
1364
|
+
suggestion
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
|
|
731
1374
|
// src/rules/wire-message-naming.ts
|
|
732
|
-
var
|
|
1375
|
+
var RULE_NAME11 = "wire-message-naming";
|
|
733
1376
|
var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
|
|
734
|
-
var
|
|
1377
|
+
var optionSchema9 = {
|
|
735
1378
|
type: "object",
|
|
736
1379
|
additionalProperties: false,
|
|
737
1380
|
properties: {
|
|
@@ -772,14 +1415,14 @@ function typeLiteralNode(obj) {
|
|
|
772
1415
|
return null;
|
|
773
1416
|
}
|
|
774
1417
|
var wireMessageNamingRule = createRule({
|
|
775
|
-
name:
|
|
1418
|
+
name: RULE_NAME11,
|
|
776
1419
|
meta: {
|
|
777
1420
|
type: "problem",
|
|
778
1421
|
docs: {
|
|
779
1422
|
description: "A message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares `type: z.literal(...)` must set that literal to kebab-case(const name minus its role suffix)."
|
|
780
1423
|
},
|
|
781
1424
|
fixable: "code",
|
|
782
|
-
schema: [
|
|
1425
|
+
schema: [optionSchema9],
|
|
783
1426
|
messages: {
|
|
784
1427
|
typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
|
|
785
1428
|
}
|
|
@@ -815,11 +1458,11 @@ var wireMessageNamingRule = createRule({
|
|
|
815
1458
|
});
|
|
816
1459
|
|
|
817
1460
|
// src/rules/zod-schema-naming.ts
|
|
818
|
-
var
|
|
1461
|
+
var RULE_NAME12 = "zod-schema-naming";
|
|
819
1462
|
var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
|
|
820
1463
|
var SUFFIX = "Schema";
|
|
821
1464
|
var DEFAULT_ROLE_SUFFIXES2 = [];
|
|
822
|
-
var
|
|
1465
|
+
var optionSchema10 = {
|
|
823
1466
|
type: "object",
|
|
824
1467
|
additionalProperties: false,
|
|
825
1468
|
properties: {
|
|
@@ -852,13 +1495,13 @@ function rootIdentifierName(node) {
|
|
|
852
1495
|
return null;
|
|
853
1496
|
}
|
|
854
1497
|
var zodSchemaNamingRule = createRule({
|
|
855
|
-
name:
|
|
1498
|
+
name: RULE_NAME12,
|
|
856
1499
|
meta: {
|
|
857
1500
|
type: "problem",
|
|
858
1501
|
docs: {
|
|
859
1502
|
description: "Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type (`export type Foo = z.infer<typeof FooSchema>`)."
|
|
860
1503
|
},
|
|
861
|
-
schema: [
|
|
1504
|
+
schema: [optionSchema10],
|
|
862
1505
|
messages: {
|
|
863
1506
|
schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
|
|
864
1507
|
missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
|
|
@@ -919,12 +1562,14 @@ var rules = {
|
|
|
919
1562
|
"restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
|
|
920
1563
|
"require-registered-keys": requireRegisteredKeysRule,
|
|
921
1564
|
"env-var-schema-parity": envVarSchemaParityRule,
|
|
922
|
-
"require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule
|
|
1565
|
+
"require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
|
|
1566
|
+
"schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
|
|
1567
|
+
"fetch-must-check-ok": fetchMustCheckOkRule
|
|
923
1568
|
};
|
|
924
1569
|
|
|
925
1570
|
// src/index.ts
|
|
926
1571
|
var NAMESPACE = "noctcore-contracts";
|
|
927
|
-
var VERSION = "0.
|
|
1572
|
+
var VERSION = "0.3.0";
|
|
928
1573
|
var plugin = {
|
|
929
1574
|
meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
|
|
930
1575
|
rules,
|