@noctcore/eslint-plugin-code-quality 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -35,60 +45,22 @@ var recommended = {
35
45
  "noctcore-code-quality/no-narration-comments": "error",
36
46
  "noctcore-code-quality/no-pr-reference-comments": "error",
37
47
  "noctcore-code-quality/no-focused-tests": "error",
38
- "noctcore-code-quality/skipped-tests-need-tracking": "error"
48
+ "noctcore-code-quality/skipped-tests-need-tracking": "error",
49
+ "noctcore-code-quality/no-vacuous-expect": "error",
50
+ "noctcore-code-quality/no-conditional-expect": "error",
51
+ "noctcore-code-quality/fake-timers-must-be-restored": "error",
52
+ "noctcore-code-quality/no-real-network-in-unit-tests": "error"
39
53
  };
40
54
 
41
- // src/rules/interface-prefix-i.ts
55
+ // src/rules/fake-timers-must-be-restored.ts
56
+ var import_node_fs = __toESM(require("fs"), 1);
57
+ var import_node_path = __toESM(require("path"), 1);
42
58
  var import_utils = require("@typescript-eslint/utils");
43
59
 
44
60
  // src/createRule.ts
45
61
  var import_eslint_utils = require("@noctcore/eslint-utils");
46
62
  var createRule = (0, import_eslint_utils.makeCreateRule)("code-quality");
47
63
 
48
- // src/rules/interface-prefix-i.ts
49
- var RULE_NAME = "interface-prefix-i";
50
- function isAlreadyPrefixed(name) {
51
- return /^I[A-Z]/.test(name);
52
- }
53
- function isInsideAmbientModule(node) {
54
- let current = node.parent;
55
- while (current) {
56
- if (current.type === import_utils.AST_NODE_TYPES.TSModuleDeclaration) {
57
- return true;
58
- }
59
- current = current.parent;
60
- }
61
- return false;
62
- }
63
- var interfacePrefixIRule = createRule({
64
- name: RULE_NAME,
65
- meta: {
66
- type: "suggestion",
67
- docs: {
68
- description: "Interface names must be prefixed with `I` followed by an uppercase letter. Module/global augmentations are exempt."
69
- },
70
- schema: [],
71
- messages: {
72
- missingPrefix: "Interface `{{name}}` must be prefixed with `I` (e.g. `I{{name}}`). Rename the declaration and its references."
73
- }
74
- },
75
- defaultOptions: [],
76
- create(context) {
77
- return {
78
- TSInterfaceDeclaration(node) {
79
- const name = node.id.name;
80
- if (isAlreadyPrefixed(name) || isInsideAmbientModule(node)) {
81
- return;
82
- }
83
- context.report({ node: node.id, messageId: "missingPrefix", data: { name } });
84
- }
85
- };
86
- }
87
- });
88
-
89
- // src/rules/no-bare-date-now.ts
90
- var import_utils2 = require("@typescript-eslint/utils");
91
-
92
64
  // src/utils/allowMatch.ts
93
65
  var REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/gu;
94
66
  function escapeLiteral(text) {
@@ -153,10 +125,216 @@ function matchesAny(filename, globs) {
153
125
  return globs.some((glob) => compile(glob).test(normalized));
154
126
  }
155
127
 
128
+ // src/rules/fake-timers-must-be-restored.ts
129
+ var RULE_NAME = "fake-timers-must-be-restored";
130
+ var DEFAULT_FAKE_TIMER_METHODS = ["useFakeTimers"];
131
+ var DEFAULT_RESTORE_TIMER_METHODS = ["useRealTimers"];
132
+ var DEFAULT_FOLLOW_IMPORTED_SUITES = true;
133
+ var DEFAULT_SHARED_SUITE_MODULES = [];
134
+ var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
135
+ var methodList = {
136
+ type: "array",
137
+ items: { type: "string", minLength: 1 },
138
+ uniqueItems: true,
139
+ minItems: 1
140
+ };
141
+ var optionSchema = {
142
+ type: "object",
143
+ additionalProperties: false,
144
+ properties: {
145
+ fakeTimerMethods: methodList,
146
+ restoreTimerMethods: methodList,
147
+ followImportedSuites: { type: "boolean" },
148
+ sharedSuiteModules: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true }
149
+ }
150
+ };
151
+ var sourceCache = /* @__PURE__ */ new Map();
152
+ function readSource(file) {
153
+ try {
154
+ const stat = import_node_fs.default.statSync(file);
155
+ if (!stat.isFile()) {
156
+ return null;
157
+ }
158
+ const cached = sourceCache.get(file);
159
+ if (cached !== void 0 && cached.mtimeMs === stat.mtimeMs) {
160
+ return cached.text;
161
+ }
162
+ const text = import_node_fs.default.readFileSync(file, "utf8");
163
+ sourceCache.set(file, { mtimeMs: stat.mtimeMs, text });
164
+ return text;
165
+ } catch (error) {
166
+ if (error instanceof Error && "code" in error) {
167
+ return null;
168
+ }
169
+ throw error;
170
+ }
171
+ }
172
+ function resolveRelative(fromFile, specifier) {
173
+ const base = import_node_path.default.resolve(import_node_path.default.dirname(fromFile), specifier);
174
+ const stem = base.replace(/\.(?:[cm]?js|jsx)$/u, "");
175
+ const candidates = [
176
+ base,
177
+ ...RESOLVE_EXTENSIONS.map((ext) => stem + ext),
178
+ ...RESOLVE_EXTENSIONS.map((ext) => import_node_path.default.join(base, `index${ext}`))
179
+ ];
180
+ for (const candidate of candidates) {
181
+ if (readSource(candidate) !== null) {
182
+ return candidate;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ function escapeRegExp(text) {
188
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
189
+ }
190
+ function importIsCalled(sourceCode, declaration) {
191
+ return sourceCode.getDeclaredVariables(declaration).some(
192
+ (variable) => variable.references.some((reference) => {
193
+ const parent = reference.identifier.parent;
194
+ if (parent.type === import_utils.AST_NODE_TYPES.CallExpression && parent.callee === reference.identifier) {
195
+ return true;
196
+ }
197
+ return parent.type === import_utils.AST_NODE_TYPES.MemberExpression && parent.object === reference.identifier && parent.parent.type === import_utils.AST_NODE_TYPES.CallExpression && parent.parent.callee === parent;
198
+ })
199
+ );
200
+ }
201
+ function calledMethodName(node) {
202
+ const callee = node.callee;
203
+ if (callee.type === import_utils.AST_NODE_TYPES.Identifier) {
204
+ return callee.name;
205
+ }
206
+ if (callee.type === import_utils.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils.AST_NODE_TYPES.Identifier) {
207
+ return callee.property.name;
208
+ }
209
+ return null;
210
+ }
211
+ var fakeTimersMustBeRestoredRule = createRule({
212
+ name: RULE_NAME,
213
+ meta: {
214
+ type: "problem",
215
+ docs: {
216
+ description: "A test file that calls `useFakeTimers()` must also call `useRealTimers()`, so fake timers do not leak into later tests."
217
+ },
218
+ schema: [optionSchema],
219
+ messages: {
220
+ timersNotRestored: "`{{method}}()` is called without a matching restore call, so fake timers leak into other tests. Call `useRealTimers()` in an `afterEach`, here or in the shared suite this file runs."
221
+ }
222
+ },
223
+ defaultOptions: [
224
+ {
225
+ fakeTimerMethods: [...DEFAULT_FAKE_TIMER_METHODS],
226
+ restoreTimerMethods: [...DEFAULT_RESTORE_TIMER_METHODS],
227
+ followImportedSuites: DEFAULT_FOLLOW_IMPORTED_SUITES,
228
+ sharedSuiteModules: [...DEFAULT_SHARED_SUITE_MODULES]
229
+ }
230
+ ],
231
+ create(context, [options]) {
232
+ const fakeMethods = new Set(options.fakeTimerMethods ?? DEFAULT_FAKE_TIMER_METHODS);
233
+ const restoreMethodList = options.restoreTimerMethods ?? DEFAULT_RESTORE_TIMER_METHODS;
234
+ const restoreMethods = new Set(restoreMethodList);
235
+ const followImportedSuites = options.followImportedSuites ?? DEFAULT_FOLLOW_IMPORTED_SUITES;
236
+ const sharedSuiteModules = options.sharedSuiteModules ?? DEFAULT_SHARED_SUITE_MODULES;
237
+ const restoreCallPattern = new RegExp(
238
+ `\\b(?:${restoreMethodList.map(escapeRegExp).join("|")})\\s*\\(`,
239
+ "u"
240
+ );
241
+ const fakeCalls = [];
242
+ const imports = [];
243
+ let hasRestore = false;
244
+ function restoredByImportedSuite() {
245
+ return imports.some((declaration) => {
246
+ const specifier = declaration.source.value;
247
+ if (!importIsCalled(context.sourceCode, declaration)) {
248
+ return false;
249
+ }
250
+ if (matchesAny(specifier, sharedSuiteModules)) {
251
+ return true;
252
+ }
253
+ if (!followImportedSuites || !specifier.startsWith(".")) {
254
+ return false;
255
+ }
256
+ const resolved = resolveRelative(context.filename, specifier);
257
+ const text = resolved === null ? null : readSource(resolved);
258
+ return text !== null && restoreCallPattern.test(text);
259
+ });
260
+ }
261
+ return {
262
+ ImportDeclaration(node) {
263
+ if (node.importKind !== "type") {
264
+ imports.push(node);
265
+ }
266
+ },
267
+ CallExpression(node) {
268
+ const method = calledMethodName(node);
269
+ if (method === null) {
270
+ return;
271
+ }
272
+ if (fakeMethods.has(method)) {
273
+ fakeCalls.push({ node, method });
274
+ }
275
+ if (restoreMethods.has(method)) {
276
+ hasRestore = true;
277
+ }
278
+ },
279
+ "Program:exit"() {
280
+ if (fakeCalls.length === 0 || hasRestore || restoredByImportedSuite()) {
281
+ return;
282
+ }
283
+ for (const { node, method } of fakeCalls) {
284
+ context.report({ node, messageId: "timersNotRestored", data: { method } });
285
+ }
286
+ }
287
+ };
288
+ }
289
+ });
290
+
291
+ // src/rules/interface-prefix-i.ts
292
+ var import_utils2 = require("@typescript-eslint/utils");
293
+ var RULE_NAME2 = "interface-prefix-i";
294
+ function isAlreadyPrefixed(name) {
295
+ return /^I[A-Z]/.test(name);
296
+ }
297
+ function isInsideAmbientModule(node) {
298
+ let current = node.parent;
299
+ while (current) {
300
+ if (current.type === import_utils2.AST_NODE_TYPES.TSModuleDeclaration) {
301
+ return true;
302
+ }
303
+ current = current.parent;
304
+ }
305
+ return false;
306
+ }
307
+ var interfacePrefixIRule = createRule({
308
+ name: RULE_NAME2,
309
+ meta: {
310
+ type: "suggestion",
311
+ docs: {
312
+ description: "Interface names must be prefixed with `I` followed by an uppercase letter. Module/global augmentations are exempt."
313
+ },
314
+ schema: [],
315
+ messages: {
316
+ missingPrefix: "Interface `{{name}}` must be prefixed with `I` (e.g. `I{{name}}`). Rename the declaration and its references."
317
+ }
318
+ },
319
+ defaultOptions: [],
320
+ create(context) {
321
+ return {
322
+ TSInterfaceDeclaration(node) {
323
+ const name = node.id.name;
324
+ if (isAlreadyPrefixed(name) || isInsideAmbientModule(node)) {
325
+ return;
326
+ }
327
+ context.report({ node: node.id, messageId: "missingPrefix", data: { name } });
328
+ }
329
+ };
330
+ }
331
+ });
332
+
156
333
  // src/rules/no-bare-date-now.ts
157
- var RULE_NAME2 = "no-bare-date-now";
334
+ var import_utils3 = require("@typescript-eslint/utils");
335
+ var RULE_NAME3 = "no-bare-date-now";
158
336
  var DEFAULT_ALLOW_IN = ["**/clock.ts", "**/clock/**"];
159
- var optionSchema = {
337
+ var optionSchema2 = {
160
338
  type: "object",
161
339
  additionalProperties: false,
162
340
  properties: {
@@ -169,19 +347,19 @@ var optionSchema = {
169
347
  };
170
348
  function isDateNowCall(node) {
171
349
  const callee = node.callee;
172
- return callee.type === import_utils2.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils2.AST_NODE_TYPES.Identifier && callee.object.name === "Date" && callee.property.type === import_utils2.AST_NODE_TYPES.Identifier && callee.property.name === "now";
350
+ return callee.type === import_utils3.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils3.AST_NODE_TYPES.Identifier && callee.object.name === "Date" && callee.property.type === import_utils3.AST_NODE_TYPES.Identifier && callee.property.name === "now";
173
351
  }
174
352
  function isBareNewDate(node) {
175
- return node.callee.type === import_utils2.AST_NODE_TYPES.Identifier && node.callee.name === "Date" && node.arguments.length === 0;
353
+ return node.callee.type === import_utils3.AST_NODE_TYPES.Identifier && node.callee.name === "Date" && node.arguments.length === 0;
176
354
  }
177
355
  var noBareDateNowRule = createRule({
178
- name: RULE_NAME2,
356
+ name: RULE_NAME3,
179
357
  meta: {
180
358
  type: "problem",
181
359
  docs: {
182
360
  description: "Disallow bare `Date.now()` / `new Date()` in business logic. Read wall-clock time through a shared `clock` util (`nowMs()` / `now()`) so time is mockable."
183
361
  },
184
- schema: [optionSchema],
362
+ schema: [optionSchema2],
185
363
  messages: {
186
364
  dateNow: "Use `nowMs()` from the shared `clock` util instead of bare `Date.now()`.",
187
365
  newDate: "Use `now()` from the shared `clock` util instead of bare `new Date()`."
@@ -208,20 +386,169 @@ var noBareDateNowRule = createRule({
208
386
  }
209
387
  });
210
388
 
389
+ // src/rules/no-conditional-expect.ts
390
+ var import_utils4 = require("@typescript-eslint/utils");
391
+ var RULE_NAME4 = "no-conditional-expect";
392
+ var DEFAULT_CHECK_LOOPS = false;
393
+ var BRANCH_TYPES = /* @__PURE__ */ new Set([
394
+ import_utils4.AST_NODE_TYPES.IfStatement,
395
+ import_utils4.AST_NODE_TYPES.SwitchCase,
396
+ import_utils4.AST_NODE_TYPES.ConditionalExpression,
397
+ import_utils4.AST_NODE_TYPES.LogicalExpression,
398
+ import_utils4.AST_NODE_TYPES.CatchClause
399
+ ]);
400
+ var LOOP_TYPES = /* @__PURE__ */ new Set([
401
+ import_utils4.AST_NODE_TYPES.ForStatement,
402
+ import_utils4.AST_NODE_TYPES.ForInStatement,
403
+ import_utils4.AST_NODE_TYPES.ForOfStatement,
404
+ import_utils4.AST_NODE_TYPES.WhileStatement,
405
+ import_utils4.AST_NODE_TYPES.DoWhileStatement
406
+ ]);
407
+ var RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
408
+ var ASSERTION_COUNT_GUARDS = /* @__PURE__ */ new Set(["assertions", "hasAssertions"]);
409
+ var optionSchema3 = {
410
+ type: "object",
411
+ additionalProperties: false,
412
+ properties: {
413
+ checkLoops: { type: "boolean" }
414
+ }
415
+ };
416
+ function isExpectCall(node) {
417
+ const callee = node.callee;
418
+ if (callee.type === import_utils4.AST_NODE_TYPES.Identifier) {
419
+ return callee.name === "expect";
420
+ }
421
+ return callee.type === import_utils4.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils4.AST_NODE_TYPES.Identifier && callee.property.type === import_utils4.AST_NODE_TYPES.Identifier && callee.property.name === "expect";
422
+ }
423
+ function isAssertionCountGuard(node) {
424
+ const callee = node.callee;
425
+ return callee.type === import_utils4.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.object.type === import_utils4.AST_NODE_TYPES.Identifier && callee.object.name === "expect" && callee.property.type === import_utils4.AST_NODE_TYPES.Identifier && ASSERTION_COUNT_GUARDS.has(callee.property.name);
426
+ }
427
+ function runnerName(callee) {
428
+ let current = callee;
429
+ for (; ; ) {
430
+ if (current.type === import_utils4.AST_NODE_TYPES.MemberExpression) {
431
+ current = current.object;
432
+ } else if (current.type === import_utils4.AST_NODE_TYPES.CallExpression) {
433
+ current = current.callee;
434
+ } else if (current.type === import_utils4.AST_NODE_TYPES.TaggedTemplateExpression) {
435
+ current = current.tag;
436
+ } else {
437
+ break;
438
+ }
439
+ }
440
+ return current.type === import_utils4.AST_NODE_TYPES.Identifier ? current.name : null;
441
+ }
442
+ function isRunnerCallback(node) {
443
+ const parent = node.parent;
444
+ if (parent.type !== import_utils4.AST_NODE_TYPES.CallExpression || !parent.arguments.some((argument) => argument === node)) {
445
+ return false;
446
+ }
447
+ const name = runnerName(parent.callee);
448
+ return name !== null && RUNNERS.has(name);
449
+ }
450
+ function isFunctionNode(node) {
451
+ return node.type === import_utils4.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils4.AST_NODE_TYPES.FunctionExpression || node.type === import_utils4.AST_NODE_TYPES.FunctionDeclaration;
452
+ }
453
+ function isUnconditionalPart(node, child) {
454
+ switch (node.type) {
455
+ case import_utils4.AST_NODE_TYPES.IfStatement:
456
+ case import_utils4.AST_NODE_TYPES.ConditionalExpression:
457
+ return node.test === child;
458
+ case import_utils4.AST_NODE_TYPES.LogicalExpression:
459
+ return node.left === child;
460
+ case import_utils4.AST_NODE_TYPES.SwitchCase:
461
+ return node.test === child;
462
+ case import_utils4.AST_NODE_TYPES.ForStatement:
463
+ return node.init === child;
464
+ case import_utils4.AST_NODE_TYPES.ForInStatement:
465
+ case import_utils4.AST_NODE_TYPES.ForOfStatement:
466
+ return node.right === child;
467
+ default:
468
+ return false;
469
+ }
470
+ }
471
+ var noConditionalExpectRule = createRule({
472
+ name: RULE_NAME4,
473
+ meta: {
474
+ type: "problem",
475
+ docs: {
476
+ description: "Disallow `expect()` inside a branch, `catch` or loop that may not run: a skipped assertion lets a broken test pass."
477
+ },
478
+ schema: [optionSchema3],
479
+ messages: {
480
+ conditionalExpect: "This `expect()` sits inside a branch that may not run, so the test can pass without asserting anything. Assert unconditionally, or add `expect.assertions(n)` to the test."
481
+ }
482
+ },
483
+ defaultOptions: [{ checkLoops: DEFAULT_CHECK_LOOPS }],
484
+ create(context, [options]) {
485
+ const checkLoops = options.checkLoops ?? DEFAULT_CHECK_LOOPS;
486
+ const pending = /* @__PURE__ */ new Map();
487
+ const guarded = /* @__PURE__ */ new Set();
488
+ function isConditional(type) {
489
+ return BRANCH_TYPES.has(type) || checkLoops && LOOP_TYPES.has(type);
490
+ }
491
+ function scan(node) {
492
+ let conditional = false;
493
+ let child = node;
494
+ let current = node.parent;
495
+ while (current !== void 0 && current !== null) {
496
+ if (isFunctionNode(current) && isRunnerCallback(current)) {
497
+ return { boundary: current, conditional };
498
+ }
499
+ if (isConditional(current.type) && !isUnconditionalPart(current, child)) {
500
+ conditional = true;
501
+ }
502
+ child = current;
503
+ current = current.parent;
504
+ }
505
+ return { boundary: null, conditional };
506
+ }
507
+ return {
508
+ CallExpression(node) {
509
+ if (isAssertionCountGuard(node)) {
510
+ guarded.add(scan(node).boundary);
511
+ return;
512
+ }
513
+ if (!isExpectCall(node)) {
514
+ return;
515
+ }
516
+ const { boundary, conditional } = scan(node);
517
+ if (!conditional) {
518
+ return;
519
+ }
520
+ const list = pending.get(boundary) ?? [];
521
+ list.push(node);
522
+ pending.set(boundary, list);
523
+ },
524
+ "Program:exit"() {
525
+ for (const [boundary, calls] of pending) {
526
+ if (guarded.has(boundary)) {
527
+ continue;
528
+ }
529
+ for (const call of calls) {
530
+ context.report({ node: call, messageId: "conditionalExpect" });
531
+ }
532
+ }
533
+ }
534
+ };
535
+ }
536
+ });
537
+
211
538
  // src/rules/no-focused-tests.ts
212
- var import_utils3 = require("@typescript-eslint/utils");
213
- var RULE_NAME3 = "no-focused-tests";
539
+ var import_utils5 = require("@typescript-eslint/utils");
540
+ var RULE_NAME5 = "no-focused-tests";
214
541
  var FOCUSABLE_RUNNERS = /* @__PURE__ */ new Set(["it", "describe", "test"]);
215
542
  var FOCUSED_CALL_NAMES = /* @__PURE__ */ new Set(["fdescribe", "fit", "ddescribe"]);
216
543
  function rootRunnerName(node) {
217
544
  let current = node;
218
- while (current.type === import_utils3.AST_NODE_TYPES.MemberExpression && !current.computed) {
545
+ while (current.type === import_utils5.AST_NODE_TYPES.MemberExpression && !current.computed) {
219
546
  current = current.object;
220
547
  }
221
- return current.type === import_utils3.AST_NODE_TYPES.Identifier ? current.name : null;
548
+ return current.type === import_utils5.AST_NODE_TYPES.Identifier ? current.name : null;
222
549
  }
223
550
  var noFocusedTestsRule = createRule({
224
- name: RULE_NAME3,
551
+ name: RULE_NAME5,
225
552
  meta: {
226
553
  type: "problem",
227
554
  docs: {
@@ -237,7 +564,7 @@ var noFocusedTestsRule = createRule({
237
564
  create(context) {
238
565
  return {
239
566
  MemberExpression(node) {
240
- if (node.property.type !== import_utils3.AST_NODE_TYPES.Identifier || node.property.name !== "only") {
567
+ if (node.property.type !== import_utils5.AST_NODE_TYPES.Identifier || node.property.name !== "only") {
241
568
  return;
242
569
  }
243
570
  const runner = rootRunnerName(node.object);
@@ -250,7 +577,7 @@ var noFocusedTestsRule = createRule({
250
577
  }
251
578
  },
252
579
  CallExpression(node) {
253
- if (node.callee.type === import_utils3.AST_NODE_TYPES.Identifier && FOCUSED_CALL_NAMES.has(node.callee.name)) {
580
+ if (node.callee.type === import_utils5.AST_NODE_TYPES.Identifier && FOCUSED_CALL_NAMES.has(node.callee.name)) {
254
581
  const name = node.callee.name;
255
582
  context.report({
256
583
  node: node.callee,
@@ -275,7 +602,7 @@ function looksLikeJsDoc(comment) {
275
602
  }
276
603
 
277
604
  // src/rules/no-historical-comments.ts
278
- var RULE_NAME4 = "no-historical-comments";
605
+ var RULE_NAME6 = "no-historical-comments";
279
606
  var HISTORICAL_PATTERNS = [
280
607
  /\bbefore\s+the\s+fix\b/iu,
281
608
  /\bafter\s+the\s+fix\b/iu,
@@ -290,7 +617,7 @@ var HISTORICAL_PATTERNS = [
290
617
  /\bhistorical(?:ly)?\b/iu
291
618
  ];
292
619
  var noHistoricalCommentsRule = createRule({
293
- name: RULE_NAME4,
620
+ name: RULE_NAME6,
294
621
  meta: {
295
622
  type: "suggestion",
296
623
  docs: {
@@ -333,7 +660,7 @@ var noHistoricalCommentsRule = createRule({
333
660
  });
334
661
 
335
662
  // src/rules/no-narration-comments.ts
336
- var RULE_NAME5 = "no-narration-comments";
663
+ var RULE_NAME7 = "no-narration-comments";
337
664
  var NARRATION_PATTERNS = [
338
665
  /^here\s+we\b/iu,
339
666
  /^now\s+we\b/iu,
@@ -345,7 +672,7 @@ var NARRATION_PATTERNS = [
345
672
  /^let\s+me\b/iu
346
673
  ];
347
674
  var noNarrationCommentsRule = createRule({
348
- name: RULE_NAME5,
675
+ name: RULE_NAME7,
349
676
  meta: {
350
677
  type: "suggestion",
351
678
  docs: {
@@ -386,7 +713,7 @@ var noNarrationCommentsRule = createRule({
386
713
  });
387
714
 
388
715
  // src/rules/no-pr-reference-comments.ts
389
- var RULE_NAME6 = "no-pr-reference-comments";
716
+ var RULE_NAME8 = "no-pr-reference-comments";
390
717
  var PR_PATTERNS = [
391
718
  {
392
719
  pattern: /https?:\/\/github\.com\/[^\s)]+\/(?:pull|issues)\/\d+/iu,
@@ -406,7 +733,7 @@ var PR_PATTERNS = [
406
733
  }
407
734
  ];
408
735
  var noPrReferenceCommentsRule = createRule({
409
- name: RULE_NAME6,
736
+ name: RULE_NAME8,
410
737
  meta: {
411
738
  type: "suggestion",
412
739
  docs: {
@@ -447,32 +774,32 @@ var noPrReferenceCommentsRule = createRule({
447
774
  });
448
775
 
449
776
  // src/rules/no-process-exit.ts
450
- var import_utils5 = require("@typescript-eslint/utils");
777
+ var import_utils7 = require("@typescript-eslint/utils");
451
778
 
452
779
  // src/utils/ast.ts
453
- var import_utils4 = require("@typescript-eslint/utils");
780
+ var import_utils6 = require("@typescript-eslint/utils");
454
781
  function isEmptyStringLiteral(node) {
455
- return node.type === import_utils4.AST_NODE_TYPES.Literal && node.value === "";
782
+ return node.type === import_utils6.AST_NODE_TYPES.Literal && node.value === "";
456
783
  }
457
784
  function isStaticMemberAccess(node, objectName, propertyName) {
458
- if (node.type !== import_utils4.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils4.AST_NODE_TYPES.Identifier || node.object.name !== objectName) {
785
+ if (node.type !== import_utils6.AST_NODE_TYPES.MemberExpression || node.object.type !== import_utils6.AST_NODE_TYPES.Identifier || node.object.name !== objectName) {
459
786
  return false;
460
787
  }
461
788
  if (node.computed) {
462
- return node.property.type === import_utils4.AST_NODE_TYPES.Literal && node.property.value === propertyName;
789
+ return node.property.type === import_utils6.AST_NODE_TYPES.Literal && node.property.value === propertyName;
463
790
  }
464
- return node.property.type === import_utils4.AST_NODE_TYPES.Identifier && node.property.name === propertyName;
791
+ return node.property.type === import_utils6.AST_NODE_TYPES.Identifier && node.property.name === propertyName;
465
792
  }
466
793
 
467
794
  // src/rules/no-process-exit.ts
468
- var RULE_NAME7 = "no-process-exit";
795
+ var RULE_NAME9 = "no-process-exit";
469
796
  var DEFAULT_ALLOW_IN2 = [
470
797
  "**/scripts/**",
471
798
  "**/bin/**",
472
799
  "**/cli/**",
473
800
  "**/*.config.{ts,js,mjs,cjs,cts,mts}"
474
801
  ];
475
- var optionSchema2 = {
802
+ var optionSchema4 = {
476
803
  type: "object",
477
804
  additionalProperties: false,
478
805
  properties: {
@@ -487,13 +814,13 @@ function isProcessExit(node) {
487
814
  return isStaticMemberAccess(node.callee, "process", "exit");
488
815
  }
489
816
  var noProcessExitRule = createRule({
490
- name: RULE_NAME7,
817
+ name: RULE_NAME9,
491
818
  meta: {
492
819
  type: "problem",
493
820
  docs: {
494
821
  description: "Disallow `process.exit()` outside bootstrap/shutdown paths and standalone CLIs. Application and service code must throw or reject so the lifecycle can shut down gracefully."
495
822
  },
496
- schema: [optionSchema2],
823
+ schema: [optionSchema4],
497
824
  messages: {
498
825
  processExit: "`process.exit()` is reserved for bootstrap/shutdown and CLI entrypoints. Throw or reject and let the lifecycle handle teardown."
499
826
  }
@@ -514,14 +841,207 @@ var noProcessExitRule = createRule({
514
841
  }
515
842
  });
516
843
 
844
+ // src/rules/no-real-network-in-unit-tests.ts
845
+ var import_node_path2 = __toESM(require("path"), 1);
846
+ var import_utils8 = require("@typescript-eslint/utils");
847
+ var RULE_NAME10 = "no-real-network-in-unit-tests";
848
+ var DEFAULT_TEST_FILE_SUFFIXES = [
849
+ ".test.ts",
850
+ ".test.tsx",
851
+ ".spec.ts",
852
+ ".spec.tsx",
853
+ ".test.js",
854
+ ".test.jsx",
855
+ ".spec.js",
856
+ ".spec.jsx"
857
+ ];
858
+ var DEFAULT_INTEGRATION_MARKERS = [
859
+ ".integration.test.",
860
+ ".integration.spec.",
861
+ ".e2e.test.",
862
+ ".e2e.spec.",
863
+ ".e2e-spec.",
864
+ "/integration/",
865
+ "/e2e/"
866
+ ];
867
+ var DEFAULT_NETWORK_CALLEES = ["fetch"];
868
+ var DEFAULT_HTTP_CLIENTS = ["axios"];
869
+ var HTTP_CLIENT_METHODS = /* @__PURE__ */ new Set([
870
+ "get",
871
+ "post",
872
+ "put",
873
+ "patch",
874
+ "delete",
875
+ "head",
876
+ "options",
877
+ "request"
878
+ ]);
879
+ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set(["globalThis", "window", "self", "global"]);
880
+ var MODULE_MOCKERS = /* @__PURE__ */ new Set(["mock", "doMock", "unstable_mockModule"]);
881
+ var GLOBAL_STUBBERS = /* @__PURE__ */ new Set(["stubGlobal", "spyOn", "replaceProperty"]);
882
+ var stringList = {
883
+ type: "array",
884
+ items: { type: "string", minLength: 1 },
885
+ uniqueItems: true
886
+ };
887
+ var optionSchema5 = {
888
+ type: "object",
889
+ additionalProperties: false,
890
+ properties: {
891
+ testFileSuffixes: stringList,
892
+ integrationMarkers: stringList,
893
+ networkCallees: stringList,
894
+ httpClients: stringList
895
+ }
896
+ };
897
+ function toPosixRelative(filename, cwd) {
898
+ const relative = import_node_path2.default.isAbsolute(filename) ? import_node_path2.default.relative(cwd, filename) : filename;
899
+ return `/${relative.split(import_node_path2.default.sep).join("/")}`;
900
+ }
901
+ function staticPropertyName(member) {
902
+ if (!member.computed && member.property.type === import_utils8.AST_NODE_TYPES.Identifier) {
903
+ return member.property.name;
904
+ }
905
+ if (member.property.type === import_utils8.AST_NODE_TYPES.Literal && typeof member.property.value === "string") {
906
+ return member.property.value;
907
+ }
908
+ return null;
909
+ }
910
+ function stringArgument(node, index) {
911
+ const argument = node.arguments[index];
912
+ return argument?.type === import_utils8.AST_NODE_TYPES.Literal && typeof argument.value === "string" ? argument.value : null;
913
+ }
914
+ var noRealNetworkInUnitTestsRule = createRule({
915
+ name: RULE_NAME10,
916
+ meta: {
917
+ type: "problem",
918
+ docs: {
919
+ description: "Unit tests must not perform real network I/O: mock the HTTP client, or move the test to an integration suite."
920
+ },
921
+ schema: [optionSchema5],
922
+ messages: {
923
+ realNetworkInUnitTest: "Real network call `{{callee}}` in a unit test. Mock it, or move this test to an integration file."
924
+ }
925
+ },
926
+ defaultOptions: [
927
+ {
928
+ testFileSuffixes: [...DEFAULT_TEST_FILE_SUFFIXES],
929
+ integrationMarkers: [...DEFAULT_INTEGRATION_MARKERS],
930
+ networkCallees: [...DEFAULT_NETWORK_CALLEES],
931
+ httpClients: [...DEFAULT_HTTP_CLIENTS]
932
+ }
933
+ ],
934
+ create(context, [options]) {
935
+ const testSuffixes = options.testFileSuffixes ?? DEFAULT_TEST_FILE_SUFFIXES;
936
+ const integrationMarkers = options.integrationMarkers ?? DEFAULT_INTEGRATION_MARKERS;
937
+ const networkCallees = new Set(options.networkCallees ?? DEFAULT_NETWORK_CALLEES);
938
+ const httpClients = new Set(options.httpClients ?? DEFAULT_HTTP_CLIENTS);
939
+ const relPath = toPosixRelative(context.filename, context.cwd);
940
+ if (!testSuffixes.some((suffix) => relPath.endsWith(suffix))) {
941
+ return {};
942
+ }
943
+ if (integrationMarkers.some((marker) => relPath.includes(marker))) {
944
+ return {};
945
+ }
946
+ const findings = [];
947
+ const mocked = /* @__PURE__ */ new Set();
948
+ const importSources = /* @__PURE__ */ new Map();
949
+ function isLocalDouble(node, name) {
950
+ let scope = context.sourceCode.getScope(node);
951
+ while (scope !== null) {
952
+ const variable = scope.set.get(name);
953
+ if (variable !== void 0 && variable.defs.length > 0) {
954
+ return variable.defs.every((def) => def.type !== "ImportBinding");
955
+ }
956
+ scope = scope.upper;
957
+ }
958
+ return false;
959
+ }
960
+ function recordMock(node) {
961
+ const callee = node.callee;
962
+ if (callee.type !== import_utils8.AST_NODE_TYPES.MemberExpression) {
963
+ return;
964
+ }
965
+ const method = staticPropertyName(callee);
966
+ if (method !== null && MODULE_MOCKERS.has(method)) {
967
+ const moduleName = stringArgument(node, 0);
968
+ if (moduleName !== null) {
969
+ mocked.add(moduleName);
970
+ }
971
+ }
972
+ if (method !== null && GLOBAL_STUBBERS.has(method)) {
973
+ const name = method === "stubGlobal" ? stringArgument(node, 0) : stringArgument(node, 1);
974
+ if (name !== null) {
975
+ mocked.add(name);
976
+ }
977
+ }
978
+ }
979
+ function networkCallee(node) {
980
+ const callee = node.callee;
981
+ if (callee.type === import_utils8.AST_NODE_TYPES.Identifier) {
982
+ const isNetworkName = networkCallees.has(callee.name) || httpClients.has(callee.name);
983
+ return isNetworkName && !isLocalDouble(callee, callee.name) ? callee.name : null;
984
+ }
985
+ if (callee.type !== import_utils8.AST_NODE_TYPES.MemberExpression) {
986
+ return null;
987
+ }
988
+ const method = staticPropertyName(callee);
989
+ if (method === null || callee.object.type !== import_utils8.AST_NODE_TYPES.Identifier) {
990
+ return null;
991
+ }
992
+ const receiver = callee.object.name;
993
+ if (GLOBAL_RECEIVERS.has(receiver) && networkCallees.has(method)) {
994
+ return method;
995
+ }
996
+ if (httpClients.has(receiver) && HTTP_CLIENT_METHODS.has(method) && !isLocalDouble(callee.object, receiver)) {
997
+ return `${receiver}.${method}`;
998
+ }
999
+ return null;
1000
+ }
1001
+ return {
1002
+ ImportDeclaration(node) {
1003
+ for (const specifier of node.specifiers) {
1004
+ importSources.set(specifier.local.name, node.source.value);
1005
+ }
1006
+ },
1007
+ CallExpression(node) {
1008
+ recordMock(node);
1009
+ const callee = networkCallee(node);
1010
+ if (callee !== null) {
1011
+ findings.push({ node, callee });
1012
+ }
1013
+ },
1014
+ AssignmentExpression(node) {
1015
+ const target = node.left;
1016
+ if (target.type === import_utils8.AST_NODE_TYPES.MemberExpression && target.object.type === import_utils8.AST_NODE_TYPES.Identifier && GLOBAL_RECEIVERS.has(target.object.name)) {
1017
+ const name = staticPropertyName(target);
1018
+ if (name !== null) {
1019
+ mocked.add(name);
1020
+ }
1021
+ }
1022
+ },
1023
+ "Program:exit"() {
1024
+ for (const { node, callee } of findings) {
1025
+ const root = callee.split(".")[0] ?? callee;
1026
+ const source = importSources.get(root);
1027
+ if (mocked.has(root) || source !== void 0 && mocked.has(source)) {
1028
+ continue;
1029
+ }
1030
+ context.report({ node, messageId: "realNetworkInUnitTest", data: { callee } });
1031
+ }
1032
+ }
1033
+ };
1034
+ }
1035
+ });
1036
+
517
1037
  // src/rules/no-template-trim-empty-ternary.ts
518
- var import_utils6 = require("@typescript-eslint/utils");
519
- var RULE_NAME8 = "no-template-trim-empty-ternary";
1038
+ var import_utils9 = require("@typescript-eslint/utils");
1039
+ var RULE_NAME11 = "no-template-trim-empty-ternary";
520
1040
  function isTrimCallOnTemplate(node) {
521
- return node.type === import_utils6.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils6.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils6.AST_NODE_TYPES.Identifier && node.callee.property.name === "trim" && node.callee.object.type === import_utils6.AST_NODE_TYPES.TemplateLiteral;
1041
+ return node.type === import_utils9.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils9.AST_NODE_TYPES.MemberExpression && node.callee.property.type === import_utils9.AST_NODE_TYPES.Identifier && node.callee.property.name === "trim" && node.callee.object.type === import_utils9.AST_NODE_TYPES.TemplateLiteral;
522
1042
  }
523
1043
  function matchesTemplateTrimEmptyTest(test) {
524
- if (test.type !== import_utils6.AST_NODE_TYPES.BinaryExpression) {
1044
+ if (test.type !== import_utils9.AST_NODE_TYPES.BinaryExpression) {
525
1045
  return false;
526
1046
  }
527
1047
  if (test.operator !== "===" && test.operator !== "!==") {
@@ -530,7 +1050,7 @@ function matchesTemplateTrimEmptyTest(test) {
530
1050
  return isTrimCallOnTemplate(test.left) && isEmptyStringLiteral(test.right) || isEmptyStringLiteral(test.left) && isTrimCallOnTemplate(test.right);
531
1051
  }
532
1052
  var noTemplateTrimEmptyTernaryRule = createRule({
533
- name: RULE_NAME8,
1053
+ name: RULE_NAME11,
534
1054
  meta: {
535
1055
  type: "suggestion",
536
1056
  docs: {
@@ -554,10 +1074,10 @@ var noTemplateTrimEmptyTernaryRule = createRule({
554
1074
  });
555
1075
 
556
1076
  // src/utils/preferEarlyReturn.ts
557
- var import_utils7 = require("@typescript-eslint/utils");
1077
+ var import_utils10 = require("@typescript-eslint/utils");
558
1078
  var MIN_CONSEQUENT_STATEMENTS = 2;
559
1079
  function getFunctionBlockBody(node) {
560
- if (node.body.type !== import_utils7.AST_NODE_TYPES.BlockStatement) {
1080
+ if (node.body.type !== import_utils10.AST_NODE_TYPES.BlockStatement) {
561
1081
  return null;
562
1082
  }
563
1083
  return node.body;
@@ -567,13 +1087,13 @@ function findWrappedHappyPathIf(body) {
567
1087
  return null;
568
1088
  }
569
1089
  const lastStatement = body.body[body.body.length - 1];
570
- if (lastStatement === void 0 || lastStatement.type !== import_utils7.AST_NODE_TYPES.IfStatement) {
1090
+ if (lastStatement === void 0 || lastStatement.type !== import_utils10.AST_NODE_TYPES.IfStatement) {
571
1091
  return null;
572
1092
  }
573
1093
  if (lastStatement.alternate !== null) {
574
1094
  return null;
575
1095
  }
576
- if (lastStatement.consequent.type !== import_utils7.AST_NODE_TYPES.BlockStatement) {
1096
+ if (lastStatement.consequent.type !== import_utils10.AST_NODE_TYPES.BlockStatement) {
577
1097
  return null;
578
1098
  }
579
1099
  if (lastStatement.consequent.body.length < MIN_CONSEQUENT_STATEMENTS) {
@@ -583,9 +1103,9 @@ function findWrappedHappyPathIf(body) {
583
1103
  }
584
1104
 
585
1105
  // src/rules/prefer-early-return.ts
586
- var RULE_NAME9 = "prefer-early-return";
1106
+ var RULE_NAME12 = "prefer-early-return";
587
1107
  var preferEarlyReturnRule = createRule({
588
- name: RULE_NAME9,
1108
+ name: RULE_NAME12,
589
1109
  meta: {
590
1110
  type: "problem",
591
1111
  docs: {
@@ -616,8 +1136,189 @@ var preferEarlyReturnRule = createRule({
616
1136
  }
617
1137
  });
618
1138
 
1139
+ // src/rules/no-vacuous-expect.ts
1140
+ var import_utils11 = require("@typescript-eslint/utils");
1141
+ var RULE_NAME13 = "no-vacuous-expect";
1142
+ var DEFAULT_WEAK_MATCHERS = [
1143
+ "toBeDefined",
1144
+ "toBeTruthy",
1145
+ "toBeFalsy",
1146
+ "not.toBeUndefined"
1147
+ ];
1148
+ var DEFAULT_ASSERTION_CALLEES = ["^assert", "^expect\\w", "\\.expect$"];
1149
+ var TYPEOF_RESULTS = /* @__PURE__ */ new Set([
1150
+ "undefined",
1151
+ "object",
1152
+ "boolean",
1153
+ "number",
1154
+ "bigint",
1155
+ "string",
1156
+ "symbol",
1157
+ "function"
1158
+ ]);
1159
+ var EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1160
+ var TEST_RUNNERS = /* @__PURE__ */ new Set(["it", "test"]);
1161
+ var optionSchema6 = {
1162
+ type: "object",
1163
+ additionalProperties: false,
1164
+ properties: {
1165
+ weakMatchers: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true },
1166
+ assertionCallees: {
1167
+ type: "array",
1168
+ items: { type: "string", minLength: 1 },
1169
+ uniqueItems: true
1170
+ }
1171
+ }
1172
+ };
1173
+ function isExpectCall2(node) {
1174
+ return node.type === import_utils11.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils11.AST_NODE_TYPES.Identifier && node.callee.name === "expect";
1175
+ }
1176
+ function matcherCall(node) {
1177
+ const callee = node.callee;
1178
+ if (callee.type !== import_utils11.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils11.AST_NODE_TYPES.Identifier) {
1179
+ return null;
1180
+ }
1181
+ let negated = false;
1182
+ let current = callee.object;
1183
+ while (current.type === import_utils11.AST_NODE_TYPES.MemberExpression) {
1184
+ if (!current.computed && current.property.type === import_utils11.AST_NODE_TYPES.Identifier && current.property.name === "not") {
1185
+ negated = !negated;
1186
+ }
1187
+ current = current.object;
1188
+ }
1189
+ if (!isExpectCall2(current)) {
1190
+ return null;
1191
+ }
1192
+ const name = callee.property.name;
1193
+ return { root: current, matcher: negated ? `not.${name}` : name };
1194
+ }
1195
+ function calleePath(callee) {
1196
+ if (callee.type === import_utils11.AST_NODE_TYPES.Identifier) {
1197
+ return callee.name;
1198
+ }
1199
+ if (callee.type === import_utils11.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils11.AST_NODE_TYPES.Identifier) {
1200
+ const owner = callee.object.type === import_utils11.AST_NODE_TYPES.Identifier ? callee.object.name : "";
1201
+ return `${owner}.${callee.property.name}`;
1202
+ }
1203
+ return null;
1204
+ }
1205
+ function runnerName2(callee) {
1206
+ let current = callee;
1207
+ for (; ; ) {
1208
+ if (current.type === import_utils11.AST_NODE_TYPES.MemberExpression) {
1209
+ current = current.object;
1210
+ } else if (current.type === import_utils11.AST_NODE_TYPES.CallExpression) {
1211
+ current = current.callee;
1212
+ } else if (current.type === import_utils11.AST_NODE_TYPES.TaggedTemplateExpression) {
1213
+ current = current.tag;
1214
+ } else {
1215
+ break;
1216
+ }
1217
+ }
1218
+ return current.type === import_utils11.AST_NODE_TYPES.Identifier ? current.name : null;
1219
+ }
1220
+ function isTestCallback(node) {
1221
+ const parent = node.parent;
1222
+ if (parent.type !== import_utils11.AST_NODE_TYPES.CallExpression || parent.arguments[1] !== node) {
1223
+ return false;
1224
+ }
1225
+ const name = runnerName2(parent.callee);
1226
+ return name !== null && TEST_RUNNERS.has(name);
1227
+ }
1228
+ function isTypeofExpression(node) {
1229
+ return node?.type === import_utils11.AST_NODE_TYPES.UnaryExpression && node.operator === "typeof";
1230
+ }
1231
+ function isTypeofResult(node) {
1232
+ return node?.type === import_utils11.AST_NODE_TYPES.Literal && typeof node.value === "string" && TYPEOF_RESULTS.has(node.value);
1233
+ }
1234
+ function isLiteralTautology(actual, expected) {
1235
+ return actual?.type === import_utils11.AST_NODE_TYPES.Literal && expected?.type === import_utils11.AST_NODE_TYPES.Literal && !("regex" in actual) && actual.value === expected.value;
1236
+ }
1237
+ var noVacuousExpectRule = createRule({
1238
+ name: RULE_NAME13,
1239
+ meta: {
1240
+ type: "problem",
1241
+ docs: {
1242
+ description: "Disallow vacuous expects (`typeof` checks, literal tautologies, a sole `toBeDefined`/`toBeTruthy`): a test must assert behaviour that a real regression would break."
1243
+ },
1244
+ schema: [optionSchema6],
1245
+ messages: {
1246
+ typeofExpect: "Do not assert on `typeof`; that only proves a binding exists. Assert a result, a thrown error or an observable effect.",
1247
+ tautologyExpect: "This expect compares a literal with itself and can never fail. Assert something a real regression would break.",
1248
+ soleWeakExpect: "A sole `{{matcher}}` does not pin behaviour. Assert on the value or the outcome, or delete the test."
1249
+ }
1250
+ },
1251
+ defaultOptions: [
1252
+ {
1253
+ weakMatchers: [...DEFAULT_WEAK_MATCHERS],
1254
+ assertionCallees: [...DEFAULT_ASSERTION_CALLEES]
1255
+ }
1256
+ ],
1257
+ create(context, [options]) {
1258
+ const weakMatchers = new Set(options.weakMatchers ?? DEFAULT_WEAK_MATCHERS);
1259
+ const assertionCallees = (options.assertionCallees ?? DEFAULT_ASSERTION_CALLEES).map(
1260
+ (source) => new RegExp(source, "u")
1261
+ );
1262
+ const stack = [];
1263
+ function enter(node) {
1264
+ if (isTestCallback(node)) {
1265
+ stack.push({ node, assertions: 0, weak: null });
1266
+ }
1267
+ }
1268
+ function exit(node) {
1269
+ const frame = stack.at(-1);
1270
+ if (frame?.node !== node) {
1271
+ return;
1272
+ }
1273
+ stack.pop();
1274
+ if (frame.assertions === 1 && frame.weak !== null) {
1275
+ context.report({
1276
+ node: frame.weak.node,
1277
+ messageId: "soleWeakExpect",
1278
+ data: { matcher: frame.weak.matcher }
1279
+ });
1280
+ }
1281
+ }
1282
+ return {
1283
+ ArrowFunctionExpression: enter,
1284
+ FunctionExpression: enter,
1285
+ "ArrowFunctionExpression:exit": exit,
1286
+ "FunctionExpression:exit": exit,
1287
+ CallExpression(node) {
1288
+ const frame = stack.at(-1);
1289
+ const call = matcherCall(node);
1290
+ if (call === null) {
1291
+ const path3 = calleePath(node.callee);
1292
+ if (frame !== void 0 && path3 !== null && assertionCallees.some((re) => re.test(path3))) {
1293
+ frame.assertions += 1;
1294
+ }
1295
+ return;
1296
+ }
1297
+ if (frame !== void 0) {
1298
+ frame.assertions += 1;
1299
+ if (weakMatchers.has(call.matcher)) {
1300
+ frame.weak = { node, matcher: call.matcher };
1301
+ }
1302
+ }
1303
+ const negated = call.matcher.startsWith("not.");
1304
+ const base = negated ? call.matcher.slice("not.".length) : call.matcher;
1305
+ if (!EQUALITY_MATCHERS.has(base)) {
1306
+ return;
1307
+ }
1308
+ const actual = call.root.arguments[0];
1309
+ const expected = node.arguments[0];
1310
+ if (isTypeofExpression(actual) && isTypeofResult(expected)) {
1311
+ context.report({ node, messageId: "typeofExpect" });
1312
+ } else if (!negated && isLiteralTautology(actual, expected)) {
1313
+ context.report({ node, messageId: "tautologyExpect" });
1314
+ }
1315
+ }
1316
+ };
1317
+ }
1318
+ });
1319
+
619
1320
  // src/rules/skipped-tests-need-tracking.ts
620
- var RULE_NAME10 = "skipped-tests-need-tracking";
1321
+ var RULE_NAME14 = "skipped-tests-need-tracking";
621
1322
  var SKIP_PATTERNS = [
622
1323
  { pattern: /\b(?:it|test|describe)\.skip\s*\(/u, label: ".skip(" },
623
1324
  { pattern: /\b(?:it|test|describe)\.fixme\s*\(/u, label: ".fixme(" },
@@ -627,7 +1328,7 @@ var SKIP_PATTERNS = [
627
1328
  ];
628
1329
  var DEFAULT_MARKERS = ["https?://\\S+", "TODO\\(@?\\S+\\)"];
629
1330
  var DEFAULT_LOOKBACK = 30;
630
- var optionSchema3 = {
1331
+ var optionSchema7 = {
631
1332
  type: "object",
632
1333
  additionalProperties: false,
633
1334
  properties: {
@@ -641,13 +1342,13 @@ var optionSchema3 = {
641
1342
  }
642
1343
  };
643
1344
  var skippedTestsNeedTrackingRule = createRule({
644
- name: RULE_NAME10,
1345
+ name: RULE_NAME14,
645
1346
  meta: {
646
1347
  type: "problem",
647
1348
  docs: {
648
1349
  description: "Skipped tests (`.skip` / `.fixme` / `xit` / `xdescribe`) must carry a tracking marker (an issue URL or `TODO(@owner)`) on or above the line, so the debt has an owner instead of rotting silently."
649
1350
  },
650
- schema: [optionSchema3],
1351
+ schema: [optionSchema7],
651
1352
  messages: {
652
1353
  needsTracking: "Skipped test `{{label}}` has no tracking marker. Add an issue URL or `TODO(@owner)` on the same line or above so the skip has an owner."
653
1354
  }
@@ -699,6 +1400,10 @@ var rules = {
699
1400
  "no-pr-reference-comments": noPrReferenceCommentsRule,
700
1401
  "no-focused-tests": noFocusedTestsRule,
701
1402
  "skipped-tests-need-tracking": skippedTestsNeedTrackingRule,
1403
+ "no-vacuous-expect": noVacuousExpectRule,
1404
+ "no-conditional-expect": noConditionalExpectRule,
1405
+ "fake-timers-must-be-restored": fakeTimersMustBeRestoredRule,
1406
+ "no-real-network-in-unit-tests": noRealNetworkInUnitTestsRule,
702
1407
  // Available but omitted from `recommended` (opinionated / niche).
703
1408
  "interface-prefix-i": interfacePrefixIRule,
704
1409
  "no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule
@@ -706,7 +1411,7 @@ var rules = {
706
1411
 
707
1412
  // src/index.ts
708
1413
  var NAMESPACE = "noctcore-code-quality";
709
- var VERSION = "0.1.0";
1414
+ var VERSION = "0.2.0";
710
1415
  var plugin = {
711
1416
  meta: { name: "@noctcore/eslint-plugin-code-quality", version: VERSION },
712
1417
  rules,