@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.js CHANGED
@@ -7,60 +7,22 @@ var recommended = {
7
7
  "noctcore-code-quality/no-narration-comments": "error",
8
8
  "noctcore-code-quality/no-pr-reference-comments": "error",
9
9
  "noctcore-code-quality/no-focused-tests": "error",
10
- "noctcore-code-quality/skipped-tests-need-tracking": "error"
10
+ "noctcore-code-quality/skipped-tests-need-tracking": "error",
11
+ "noctcore-code-quality/no-vacuous-expect": "error",
12
+ "noctcore-code-quality/no-conditional-expect": "error",
13
+ "noctcore-code-quality/fake-timers-must-be-restored": "error",
14
+ "noctcore-code-quality/no-real-network-in-unit-tests": "error"
11
15
  };
12
16
 
13
- // src/rules/interface-prefix-i.ts
17
+ // src/rules/fake-timers-must-be-restored.ts
18
+ import fs from "fs";
19
+ import path from "path";
14
20
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
15
21
 
16
22
  // src/createRule.ts
17
23
  import { makeCreateRule } from "@noctcore/eslint-utils";
18
24
  var createRule = makeCreateRule("code-quality");
19
25
 
20
- // src/rules/interface-prefix-i.ts
21
- var RULE_NAME = "interface-prefix-i";
22
- function isAlreadyPrefixed(name) {
23
- return /^I[A-Z]/.test(name);
24
- }
25
- function isInsideAmbientModule(node) {
26
- let current = node.parent;
27
- while (current) {
28
- if (current.type === AST_NODE_TYPES.TSModuleDeclaration) {
29
- return true;
30
- }
31
- current = current.parent;
32
- }
33
- return false;
34
- }
35
- var interfacePrefixIRule = createRule({
36
- name: RULE_NAME,
37
- meta: {
38
- type: "suggestion",
39
- docs: {
40
- description: "Interface names must be prefixed with `I` followed by an uppercase letter. Module/global augmentations are exempt."
41
- },
42
- schema: [],
43
- messages: {
44
- missingPrefix: "Interface `{{name}}` must be prefixed with `I` (e.g. `I{{name}}`). Rename the declaration and its references."
45
- }
46
- },
47
- defaultOptions: [],
48
- create(context) {
49
- return {
50
- TSInterfaceDeclaration(node) {
51
- const name = node.id.name;
52
- if (isAlreadyPrefixed(name) || isInsideAmbientModule(node)) {
53
- return;
54
- }
55
- context.report({ node: node.id, messageId: "missingPrefix", data: { name } });
56
- }
57
- };
58
- }
59
- });
60
-
61
- // src/rules/no-bare-date-now.ts
62
- import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
63
-
64
26
  // src/utils/allowMatch.ts
65
27
  var REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/gu;
66
28
  function escapeLiteral(text) {
@@ -125,10 +87,216 @@ function matchesAny(filename, globs) {
125
87
  return globs.some((glob) => compile(glob).test(normalized));
126
88
  }
127
89
 
90
+ // src/rules/fake-timers-must-be-restored.ts
91
+ var RULE_NAME = "fake-timers-must-be-restored";
92
+ var DEFAULT_FAKE_TIMER_METHODS = ["useFakeTimers"];
93
+ var DEFAULT_RESTORE_TIMER_METHODS = ["useRealTimers"];
94
+ var DEFAULT_FOLLOW_IMPORTED_SUITES = true;
95
+ var DEFAULT_SHARED_SUITE_MODULES = [];
96
+ var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
97
+ var methodList = {
98
+ type: "array",
99
+ items: { type: "string", minLength: 1 },
100
+ uniqueItems: true,
101
+ minItems: 1
102
+ };
103
+ var optionSchema = {
104
+ type: "object",
105
+ additionalProperties: false,
106
+ properties: {
107
+ fakeTimerMethods: methodList,
108
+ restoreTimerMethods: methodList,
109
+ followImportedSuites: { type: "boolean" },
110
+ sharedSuiteModules: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true }
111
+ }
112
+ };
113
+ var sourceCache = /* @__PURE__ */ new Map();
114
+ function readSource(file) {
115
+ try {
116
+ const stat = fs.statSync(file);
117
+ if (!stat.isFile()) {
118
+ return null;
119
+ }
120
+ const cached = sourceCache.get(file);
121
+ if (cached !== void 0 && cached.mtimeMs === stat.mtimeMs) {
122
+ return cached.text;
123
+ }
124
+ const text = fs.readFileSync(file, "utf8");
125
+ sourceCache.set(file, { mtimeMs: stat.mtimeMs, text });
126
+ return text;
127
+ } catch (error) {
128
+ if (error instanceof Error && "code" in error) {
129
+ return null;
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+ function resolveRelative(fromFile, specifier) {
135
+ const base = path.resolve(path.dirname(fromFile), specifier);
136
+ const stem = base.replace(/\.(?:[cm]?js|jsx)$/u, "");
137
+ const candidates = [
138
+ base,
139
+ ...RESOLVE_EXTENSIONS.map((ext) => stem + ext),
140
+ ...RESOLVE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
141
+ ];
142
+ for (const candidate of candidates) {
143
+ if (readSource(candidate) !== null) {
144
+ return candidate;
145
+ }
146
+ }
147
+ return null;
148
+ }
149
+ function escapeRegExp(text) {
150
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
151
+ }
152
+ function importIsCalled(sourceCode, declaration) {
153
+ return sourceCode.getDeclaredVariables(declaration).some(
154
+ (variable) => variable.references.some((reference) => {
155
+ const parent = reference.identifier.parent;
156
+ if (parent.type === AST_NODE_TYPES.CallExpression && parent.callee === reference.identifier) {
157
+ return true;
158
+ }
159
+ return parent.type === AST_NODE_TYPES.MemberExpression && parent.object === reference.identifier && parent.parent.type === AST_NODE_TYPES.CallExpression && parent.parent.callee === parent;
160
+ })
161
+ );
162
+ }
163
+ function calledMethodName(node) {
164
+ const callee = node.callee;
165
+ if (callee.type === AST_NODE_TYPES.Identifier) {
166
+ return callee.name;
167
+ }
168
+ if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES.Identifier) {
169
+ return callee.property.name;
170
+ }
171
+ return null;
172
+ }
173
+ var fakeTimersMustBeRestoredRule = createRule({
174
+ name: RULE_NAME,
175
+ meta: {
176
+ type: "problem",
177
+ docs: {
178
+ description: "A test file that calls `useFakeTimers()` must also call `useRealTimers()`, so fake timers do not leak into later tests."
179
+ },
180
+ schema: [optionSchema],
181
+ messages: {
182
+ 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."
183
+ }
184
+ },
185
+ defaultOptions: [
186
+ {
187
+ fakeTimerMethods: [...DEFAULT_FAKE_TIMER_METHODS],
188
+ restoreTimerMethods: [...DEFAULT_RESTORE_TIMER_METHODS],
189
+ followImportedSuites: DEFAULT_FOLLOW_IMPORTED_SUITES,
190
+ sharedSuiteModules: [...DEFAULT_SHARED_SUITE_MODULES]
191
+ }
192
+ ],
193
+ create(context, [options]) {
194
+ const fakeMethods = new Set(options.fakeTimerMethods ?? DEFAULT_FAKE_TIMER_METHODS);
195
+ const restoreMethodList = options.restoreTimerMethods ?? DEFAULT_RESTORE_TIMER_METHODS;
196
+ const restoreMethods = new Set(restoreMethodList);
197
+ const followImportedSuites = options.followImportedSuites ?? DEFAULT_FOLLOW_IMPORTED_SUITES;
198
+ const sharedSuiteModules = options.sharedSuiteModules ?? DEFAULT_SHARED_SUITE_MODULES;
199
+ const restoreCallPattern = new RegExp(
200
+ `\\b(?:${restoreMethodList.map(escapeRegExp).join("|")})\\s*\\(`,
201
+ "u"
202
+ );
203
+ const fakeCalls = [];
204
+ const imports = [];
205
+ let hasRestore = false;
206
+ function restoredByImportedSuite() {
207
+ return imports.some((declaration) => {
208
+ const specifier = declaration.source.value;
209
+ if (!importIsCalled(context.sourceCode, declaration)) {
210
+ return false;
211
+ }
212
+ if (matchesAny(specifier, sharedSuiteModules)) {
213
+ return true;
214
+ }
215
+ if (!followImportedSuites || !specifier.startsWith(".")) {
216
+ return false;
217
+ }
218
+ const resolved = resolveRelative(context.filename, specifier);
219
+ const text = resolved === null ? null : readSource(resolved);
220
+ return text !== null && restoreCallPattern.test(text);
221
+ });
222
+ }
223
+ return {
224
+ ImportDeclaration(node) {
225
+ if (node.importKind !== "type") {
226
+ imports.push(node);
227
+ }
228
+ },
229
+ CallExpression(node) {
230
+ const method = calledMethodName(node);
231
+ if (method === null) {
232
+ return;
233
+ }
234
+ if (fakeMethods.has(method)) {
235
+ fakeCalls.push({ node, method });
236
+ }
237
+ if (restoreMethods.has(method)) {
238
+ hasRestore = true;
239
+ }
240
+ },
241
+ "Program:exit"() {
242
+ if (fakeCalls.length === 0 || hasRestore || restoredByImportedSuite()) {
243
+ return;
244
+ }
245
+ for (const { node, method } of fakeCalls) {
246
+ context.report({ node, messageId: "timersNotRestored", data: { method } });
247
+ }
248
+ }
249
+ };
250
+ }
251
+ });
252
+
253
+ // src/rules/interface-prefix-i.ts
254
+ import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
255
+ var RULE_NAME2 = "interface-prefix-i";
256
+ function isAlreadyPrefixed(name) {
257
+ return /^I[A-Z]/.test(name);
258
+ }
259
+ function isInsideAmbientModule(node) {
260
+ let current = node.parent;
261
+ while (current) {
262
+ if (current.type === AST_NODE_TYPES2.TSModuleDeclaration) {
263
+ return true;
264
+ }
265
+ current = current.parent;
266
+ }
267
+ return false;
268
+ }
269
+ var interfacePrefixIRule = createRule({
270
+ name: RULE_NAME2,
271
+ meta: {
272
+ type: "suggestion",
273
+ docs: {
274
+ description: "Interface names must be prefixed with `I` followed by an uppercase letter. Module/global augmentations are exempt."
275
+ },
276
+ schema: [],
277
+ messages: {
278
+ missingPrefix: "Interface `{{name}}` must be prefixed with `I` (e.g. `I{{name}}`). Rename the declaration and its references."
279
+ }
280
+ },
281
+ defaultOptions: [],
282
+ create(context) {
283
+ return {
284
+ TSInterfaceDeclaration(node) {
285
+ const name = node.id.name;
286
+ if (isAlreadyPrefixed(name) || isInsideAmbientModule(node)) {
287
+ return;
288
+ }
289
+ context.report({ node: node.id, messageId: "missingPrefix", data: { name } });
290
+ }
291
+ };
292
+ }
293
+ });
294
+
128
295
  // src/rules/no-bare-date-now.ts
129
- var RULE_NAME2 = "no-bare-date-now";
296
+ import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
297
+ var RULE_NAME3 = "no-bare-date-now";
130
298
  var DEFAULT_ALLOW_IN = ["**/clock.ts", "**/clock/**"];
131
- var optionSchema = {
299
+ var optionSchema2 = {
132
300
  type: "object",
133
301
  additionalProperties: false,
134
302
  properties: {
@@ -141,19 +309,19 @@ var optionSchema = {
141
309
  };
142
310
  function isDateNowCall(node) {
143
311
  const callee = node.callee;
144
- return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES2.Identifier && callee.object.name === "Date" && callee.property.type === AST_NODE_TYPES2.Identifier && callee.property.name === "now";
312
+ return callee.type === AST_NODE_TYPES3.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES3.Identifier && callee.object.name === "Date" && callee.property.type === AST_NODE_TYPES3.Identifier && callee.property.name === "now";
145
313
  }
146
314
  function isBareNewDate(node) {
147
- return node.callee.type === AST_NODE_TYPES2.Identifier && node.callee.name === "Date" && node.arguments.length === 0;
315
+ return node.callee.type === AST_NODE_TYPES3.Identifier && node.callee.name === "Date" && node.arguments.length === 0;
148
316
  }
149
317
  var noBareDateNowRule = createRule({
150
- name: RULE_NAME2,
318
+ name: RULE_NAME3,
151
319
  meta: {
152
320
  type: "problem",
153
321
  docs: {
154
322
  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."
155
323
  },
156
- schema: [optionSchema],
324
+ schema: [optionSchema2],
157
325
  messages: {
158
326
  dateNow: "Use `nowMs()` from the shared `clock` util instead of bare `Date.now()`.",
159
327
  newDate: "Use `now()` from the shared `clock` util instead of bare `new Date()`."
@@ -180,20 +348,169 @@ var noBareDateNowRule = createRule({
180
348
  }
181
349
  });
182
350
 
351
+ // src/rules/no-conditional-expect.ts
352
+ import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
353
+ var RULE_NAME4 = "no-conditional-expect";
354
+ var DEFAULT_CHECK_LOOPS = false;
355
+ var BRANCH_TYPES = /* @__PURE__ */ new Set([
356
+ AST_NODE_TYPES4.IfStatement,
357
+ AST_NODE_TYPES4.SwitchCase,
358
+ AST_NODE_TYPES4.ConditionalExpression,
359
+ AST_NODE_TYPES4.LogicalExpression,
360
+ AST_NODE_TYPES4.CatchClause
361
+ ]);
362
+ var LOOP_TYPES = /* @__PURE__ */ new Set([
363
+ AST_NODE_TYPES4.ForStatement,
364
+ AST_NODE_TYPES4.ForInStatement,
365
+ AST_NODE_TYPES4.ForOfStatement,
366
+ AST_NODE_TYPES4.WhileStatement,
367
+ AST_NODE_TYPES4.DoWhileStatement
368
+ ]);
369
+ var RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
370
+ var ASSERTION_COUNT_GUARDS = /* @__PURE__ */ new Set(["assertions", "hasAssertions"]);
371
+ var optionSchema3 = {
372
+ type: "object",
373
+ additionalProperties: false,
374
+ properties: {
375
+ checkLoops: { type: "boolean" }
376
+ }
377
+ };
378
+ function isExpectCall(node) {
379
+ const callee = node.callee;
380
+ if (callee.type === AST_NODE_TYPES4.Identifier) {
381
+ return callee.name === "expect";
382
+ }
383
+ return callee.type === AST_NODE_TYPES4.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES4.Identifier && callee.property.type === AST_NODE_TYPES4.Identifier && callee.property.name === "expect";
384
+ }
385
+ function isAssertionCountGuard(node) {
386
+ const callee = node.callee;
387
+ return callee.type === AST_NODE_TYPES4.MemberExpression && !callee.computed && callee.object.type === AST_NODE_TYPES4.Identifier && callee.object.name === "expect" && callee.property.type === AST_NODE_TYPES4.Identifier && ASSERTION_COUNT_GUARDS.has(callee.property.name);
388
+ }
389
+ function runnerName(callee) {
390
+ let current = callee;
391
+ for (; ; ) {
392
+ if (current.type === AST_NODE_TYPES4.MemberExpression) {
393
+ current = current.object;
394
+ } else if (current.type === AST_NODE_TYPES4.CallExpression) {
395
+ current = current.callee;
396
+ } else if (current.type === AST_NODE_TYPES4.TaggedTemplateExpression) {
397
+ current = current.tag;
398
+ } else {
399
+ break;
400
+ }
401
+ }
402
+ return current.type === AST_NODE_TYPES4.Identifier ? current.name : null;
403
+ }
404
+ function isRunnerCallback(node) {
405
+ const parent = node.parent;
406
+ if (parent.type !== AST_NODE_TYPES4.CallExpression || !parent.arguments.some((argument) => argument === node)) {
407
+ return false;
408
+ }
409
+ const name = runnerName(parent.callee);
410
+ return name !== null && RUNNERS.has(name);
411
+ }
412
+ function isFunctionNode(node) {
413
+ return node.type === AST_NODE_TYPES4.ArrowFunctionExpression || node.type === AST_NODE_TYPES4.FunctionExpression || node.type === AST_NODE_TYPES4.FunctionDeclaration;
414
+ }
415
+ function isUnconditionalPart(node, child) {
416
+ switch (node.type) {
417
+ case AST_NODE_TYPES4.IfStatement:
418
+ case AST_NODE_TYPES4.ConditionalExpression:
419
+ return node.test === child;
420
+ case AST_NODE_TYPES4.LogicalExpression:
421
+ return node.left === child;
422
+ case AST_NODE_TYPES4.SwitchCase:
423
+ return node.test === child;
424
+ case AST_NODE_TYPES4.ForStatement:
425
+ return node.init === child;
426
+ case AST_NODE_TYPES4.ForInStatement:
427
+ case AST_NODE_TYPES4.ForOfStatement:
428
+ return node.right === child;
429
+ default:
430
+ return false;
431
+ }
432
+ }
433
+ var noConditionalExpectRule = createRule({
434
+ name: RULE_NAME4,
435
+ meta: {
436
+ type: "problem",
437
+ docs: {
438
+ description: "Disallow `expect()` inside a branch, `catch` or loop that may not run: a skipped assertion lets a broken test pass."
439
+ },
440
+ schema: [optionSchema3],
441
+ messages: {
442
+ 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."
443
+ }
444
+ },
445
+ defaultOptions: [{ checkLoops: DEFAULT_CHECK_LOOPS }],
446
+ create(context, [options]) {
447
+ const checkLoops = options.checkLoops ?? DEFAULT_CHECK_LOOPS;
448
+ const pending = /* @__PURE__ */ new Map();
449
+ const guarded = /* @__PURE__ */ new Set();
450
+ function isConditional(type) {
451
+ return BRANCH_TYPES.has(type) || checkLoops && LOOP_TYPES.has(type);
452
+ }
453
+ function scan(node) {
454
+ let conditional = false;
455
+ let child = node;
456
+ let current = node.parent;
457
+ while (current !== void 0 && current !== null) {
458
+ if (isFunctionNode(current) && isRunnerCallback(current)) {
459
+ return { boundary: current, conditional };
460
+ }
461
+ if (isConditional(current.type) && !isUnconditionalPart(current, child)) {
462
+ conditional = true;
463
+ }
464
+ child = current;
465
+ current = current.parent;
466
+ }
467
+ return { boundary: null, conditional };
468
+ }
469
+ return {
470
+ CallExpression(node) {
471
+ if (isAssertionCountGuard(node)) {
472
+ guarded.add(scan(node).boundary);
473
+ return;
474
+ }
475
+ if (!isExpectCall(node)) {
476
+ return;
477
+ }
478
+ const { boundary, conditional } = scan(node);
479
+ if (!conditional) {
480
+ return;
481
+ }
482
+ const list = pending.get(boundary) ?? [];
483
+ list.push(node);
484
+ pending.set(boundary, list);
485
+ },
486
+ "Program:exit"() {
487
+ for (const [boundary, calls] of pending) {
488
+ if (guarded.has(boundary)) {
489
+ continue;
490
+ }
491
+ for (const call of calls) {
492
+ context.report({ node: call, messageId: "conditionalExpect" });
493
+ }
494
+ }
495
+ }
496
+ };
497
+ }
498
+ });
499
+
183
500
  // src/rules/no-focused-tests.ts
184
- import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
185
- var RULE_NAME3 = "no-focused-tests";
501
+ import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
502
+ var RULE_NAME5 = "no-focused-tests";
186
503
  var FOCUSABLE_RUNNERS = /* @__PURE__ */ new Set(["it", "describe", "test"]);
187
504
  var FOCUSED_CALL_NAMES = /* @__PURE__ */ new Set(["fdescribe", "fit", "ddescribe"]);
188
505
  function rootRunnerName(node) {
189
506
  let current = node;
190
- while (current.type === AST_NODE_TYPES3.MemberExpression && !current.computed) {
507
+ while (current.type === AST_NODE_TYPES5.MemberExpression && !current.computed) {
191
508
  current = current.object;
192
509
  }
193
- return current.type === AST_NODE_TYPES3.Identifier ? current.name : null;
510
+ return current.type === AST_NODE_TYPES5.Identifier ? current.name : null;
194
511
  }
195
512
  var noFocusedTestsRule = createRule({
196
- name: RULE_NAME3,
513
+ name: RULE_NAME5,
197
514
  meta: {
198
515
  type: "problem",
199
516
  docs: {
@@ -209,7 +526,7 @@ var noFocusedTestsRule = createRule({
209
526
  create(context) {
210
527
  return {
211
528
  MemberExpression(node) {
212
- if (node.property.type !== AST_NODE_TYPES3.Identifier || node.property.name !== "only") {
529
+ if (node.property.type !== AST_NODE_TYPES5.Identifier || node.property.name !== "only") {
213
530
  return;
214
531
  }
215
532
  const runner = rootRunnerName(node.object);
@@ -222,7 +539,7 @@ var noFocusedTestsRule = createRule({
222
539
  }
223
540
  },
224
541
  CallExpression(node) {
225
- if (node.callee.type === AST_NODE_TYPES3.Identifier && FOCUSED_CALL_NAMES.has(node.callee.name)) {
542
+ if (node.callee.type === AST_NODE_TYPES5.Identifier && FOCUSED_CALL_NAMES.has(node.callee.name)) {
226
543
  const name = node.callee.name;
227
544
  context.report({
228
545
  node: node.callee,
@@ -247,7 +564,7 @@ function looksLikeJsDoc(comment) {
247
564
  }
248
565
 
249
566
  // src/rules/no-historical-comments.ts
250
- var RULE_NAME4 = "no-historical-comments";
567
+ var RULE_NAME6 = "no-historical-comments";
251
568
  var HISTORICAL_PATTERNS = [
252
569
  /\bbefore\s+the\s+fix\b/iu,
253
570
  /\bafter\s+the\s+fix\b/iu,
@@ -262,7 +579,7 @@ var HISTORICAL_PATTERNS = [
262
579
  /\bhistorical(?:ly)?\b/iu
263
580
  ];
264
581
  var noHistoricalCommentsRule = createRule({
265
- name: RULE_NAME4,
582
+ name: RULE_NAME6,
266
583
  meta: {
267
584
  type: "suggestion",
268
585
  docs: {
@@ -305,7 +622,7 @@ var noHistoricalCommentsRule = createRule({
305
622
  });
306
623
 
307
624
  // src/rules/no-narration-comments.ts
308
- var RULE_NAME5 = "no-narration-comments";
625
+ var RULE_NAME7 = "no-narration-comments";
309
626
  var NARRATION_PATTERNS = [
310
627
  /^here\s+we\b/iu,
311
628
  /^now\s+we\b/iu,
@@ -317,7 +634,7 @@ var NARRATION_PATTERNS = [
317
634
  /^let\s+me\b/iu
318
635
  ];
319
636
  var noNarrationCommentsRule = createRule({
320
- name: RULE_NAME5,
637
+ name: RULE_NAME7,
321
638
  meta: {
322
639
  type: "suggestion",
323
640
  docs: {
@@ -358,7 +675,7 @@ var noNarrationCommentsRule = createRule({
358
675
  });
359
676
 
360
677
  // src/rules/no-pr-reference-comments.ts
361
- var RULE_NAME6 = "no-pr-reference-comments";
678
+ var RULE_NAME8 = "no-pr-reference-comments";
362
679
  var PR_PATTERNS = [
363
680
  {
364
681
  pattern: /https?:\/\/github\.com\/[^\s)]+\/(?:pull|issues)\/\d+/iu,
@@ -378,7 +695,7 @@ var PR_PATTERNS = [
378
695
  }
379
696
  ];
380
697
  var noPrReferenceCommentsRule = createRule({
381
- name: RULE_NAME6,
698
+ name: RULE_NAME8,
382
699
  meta: {
383
700
  type: "suggestion",
384
701
  docs: {
@@ -422,29 +739,29 @@ var noPrReferenceCommentsRule = createRule({
422
739
  import "@typescript-eslint/utils";
423
740
 
424
741
  // src/utils/ast.ts
425
- import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
742
+ import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
426
743
  function isEmptyStringLiteral(node) {
427
- return node.type === AST_NODE_TYPES4.Literal && node.value === "";
744
+ return node.type === AST_NODE_TYPES6.Literal && node.value === "";
428
745
  }
429
746
  function isStaticMemberAccess(node, objectName, propertyName) {
430
- if (node.type !== AST_NODE_TYPES4.MemberExpression || node.object.type !== AST_NODE_TYPES4.Identifier || node.object.name !== objectName) {
747
+ if (node.type !== AST_NODE_TYPES6.MemberExpression || node.object.type !== AST_NODE_TYPES6.Identifier || node.object.name !== objectName) {
431
748
  return false;
432
749
  }
433
750
  if (node.computed) {
434
- return node.property.type === AST_NODE_TYPES4.Literal && node.property.value === propertyName;
751
+ return node.property.type === AST_NODE_TYPES6.Literal && node.property.value === propertyName;
435
752
  }
436
- return node.property.type === AST_NODE_TYPES4.Identifier && node.property.name === propertyName;
753
+ return node.property.type === AST_NODE_TYPES6.Identifier && node.property.name === propertyName;
437
754
  }
438
755
 
439
756
  // src/rules/no-process-exit.ts
440
- var RULE_NAME7 = "no-process-exit";
757
+ var RULE_NAME9 = "no-process-exit";
441
758
  var DEFAULT_ALLOW_IN2 = [
442
759
  "**/scripts/**",
443
760
  "**/bin/**",
444
761
  "**/cli/**",
445
762
  "**/*.config.{ts,js,mjs,cjs,cts,mts}"
446
763
  ];
447
- var optionSchema2 = {
764
+ var optionSchema4 = {
448
765
  type: "object",
449
766
  additionalProperties: false,
450
767
  properties: {
@@ -459,13 +776,13 @@ function isProcessExit(node) {
459
776
  return isStaticMemberAccess(node.callee, "process", "exit");
460
777
  }
461
778
  var noProcessExitRule = createRule({
462
- name: RULE_NAME7,
779
+ name: RULE_NAME9,
463
780
  meta: {
464
781
  type: "problem",
465
782
  docs: {
466
783
  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."
467
784
  },
468
- schema: [optionSchema2],
785
+ schema: [optionSchema4],
469
786
  messages: {
470
787
  processExit: "`process.exit()` is reserved for bootstrap/shutdown and CLI entrypoints. Throw or reject and let the lifecycle handle teardown."
471
788
  }
@@ -486,14 +803,207 @@ var noProcessExitRule = createRule({
486
803
  }
487
804
  });
488
805
 
806
+ // src/rules/no-real-network-in-unit-tests.ts
807
+ import path2 from "path";
808
+ import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
809
+ var RULE_NAME10 = "no-real-network-in-unit-tests";
810
+ var DEFAULT_TEST_FILE_SUFFIXES = [
811
+ ".test.ts",
812
+ ".test.tsx",
813
+ ".spec.ts",
814
+ ".spec.tsx",
815
+ ".test.js",
816
+ ".test.jsx",
817
+ ".spec.js",
818
+ ".spec.jsx"
819
+ ];
820
+ var DEFAULT_INTEGRATION_MARKERS = [
821
+ ".integration.test.",
822
+ ".integration.spec.",
823
+ ".e2e.test.",
824
+ ".e2e.spec.",
825
+ ".e2e-spec.",
826
+ "/integration/",
827
+ "/e2e/"
828
+ ];
829
+ var DEFAULT_NETWORK_CALLEES = ["fetch"];
830
+ var DEFAULT_HTTP_CLIENTS = ["axios"];
831
+ var HTTP_CLIENT_METHODS = /* @__PURE__ */ new Set([
832
+ "get",
833
+ "post",
834
+ "put",
835
+ "patch",
836
+ "delete",
837
+ "head",
838
+ "options",
839
+ "request"
840
+ ]);
841
+ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set(["globalThis", "window", "self", "global"]);
842
+ var MODULE_MOCKERS = /* @__PURE__ */ new Set(["mock", "doMock", "unstable_mockModule"]);
843
+ var GLOBAL_STUBBERS = /* @__PURE__ */ new Set(["stubGlobal", "spyOn", "replaceProperty"]);
844
+ var stringList = {
845
+ type: "array",
846
+ items: { type: "string", minLength: 1 },
847
+ uniqueItems: true
848
+ };
849
+ var optionSchema5 = {
850
+ type: "object",
851
+ additionalProperties: false,
852
+ properties: {
853
+ testFileSuffixes: stringList,
854
+ integrationMarkers: stringList,
855
+ networkCallees: stringList,
856
+ httpClients: stringList
857
+ }
858
+ };
859
+ function toPosixRelative(filename, cwd) {
860
+ const relative = path2.isAbsolute(filename) ? path2.relative(cwd, filename) : filename;
861
+ return `/${relative.split(path2.sep).join("/")}`;
862
+ }
863
+ function staticPropertyName(member) {
864
+ if (!member.computed && member.property.type === AST_NODE_TYPES7.Identifier) {
865
+ return member.property.name;
866
+ }
867
+ if (member.property.type === AST_NODE_TYPES7.Literal && typeof member.property.value === "string") {
868
+ return member.property.value;
869
+ }
870
+ return null;
871
+ }
872
+ function stringArgument(node, index) {
873
+ const argument = node.arguments[index];
874
+ return argument?.type === AST_NODE_TYPES7.Literal && typeof argument.value === "string" ? argument.value : null;
875
+ }
876
+ var noRealNetworkInUnitTestsRule = createRule({
877
+ name: RULE_NAME10,
878
+ meta: {
879
+ type: "problem",
880
+ docs: {
881
+ description: "Unit tests must not perform real network I/O: mock the HTTP client, or move the test to an integration suite."
882
+ },
883
+ schema: [optionSchema5],
884
+ messages: {
885
+ realNetworkInUnitTest: "Real network call `{{callee}}` in a unit test. Mock it, or move this test to an integration file."
886
+ }
887
+ },
888
+ defaultOptions: [
889
+ {
890
+ testFileSuffixes: [...DEFAULT_TEST_FILE_SUFFIXES],
891
+ integrationMarkers: [...DEFAULT_INTEGRATION_MARKERS],
892
+ networkCallees: [...DEFAULT_NETWORK_CALLEES],
893
+ httpClients: [...DEFAULT_HTTP_CLIENTS]
894
+ }
895
+ ],
896
+ create(context, [options]) {
897
+ const testSuffixes = options.testFileSuffixes ?? DEFAULT_TEST_FILE_SUFFIXES;
898
+ const integrationMarkers = options.integrationMarkers ?? DEFAULT_INTEGRATION_MARKERS;
899
+ const networkCallees = new Set(options.networkCallees ?? DEFAULT_NETWORK_CALLEES);
900
+ const httpClients = new Set(options.httpClients ?? DEFAULT_HTTP_CLIENTS);
901
+ const relPath = toPosixRelative(context.filename, context.cwd);
902
+ if (!testSuffixes.some((suffix) => relPath.endsWith(suffix))) {
903
+ return {};
904
+ }
905
+ if (integrationMarkers.some((marker) => relPath.includes(marker))) {
906
+ return {};
907
+ }
908
+ const findings = [];
909
+ const mocked = /* @__PURE__ */ new Set();
910
+ const importSources = /* @__PURE__ */ new Map();
911
+ function isLocalDouble(node, name) {
912
+ let scope = context.sourceCode.getScope(node);
913
+ while (scope !== null) {
914
+ const variable = scope.set.get(name);
915
+ if (variable !== void 0 && variable.defs.length > 0) {
916
+ return variable.defs.every((def) => def.type !== "ImportBinding");
917
+ }
918
+ scope = scope.upper;
919
+ }
920
+ return false;
921
+ }
922
+ function recordMock(node) {
923
+ const callee = node.callee;
924
+ if (callee.type !== AST_NODE_TYPES7.MemberExpression) {
925
+ return;
926
+ }
927
+ const method = staticPropertyName(callee);
928
+ if (method !== null && MODULE_MOCKERS.has(method)) {
929
+ const moduleName = stringArgument(node, 0);
930
+ if (moduleName !== null) {
931
+ mocked.add(moduleName);
932
+ }
933
+ }
934
+ if (method !== null && GLOBAL_STUBBERS.has(method)) {
935
+ const name = method === "stubGlobal" ? stringArgument(node, 0) : stringArgument(node, 1);
936
+ if (name !== null) {
937
+ mocked.add(name);
938
+ }
939
+ }
940
+ }
941
+ function networkCallee(node) {
942
+ const callee = node.callee;
943
+ if (callee.type === AST_NODE_TYPES7.Identifier) {
944
+ const isNetworkName = networkCallees.has(callee.name) || httpClients.has(callee.name);
945
+ return isNetworkName && !isLocalDouble(callee, callee.name) ? callee.name : null;
946
+ }
947
+ if (callee.type !== AST_NODE_TYPES7.MemberExpression) {
948
+ return null;
949
+ }
950
+ const method = staticPropertyName(callee);
951
+ if (method === null || callee.object.type !== AST_NODE_TYPES7.Identifier) {
952
+ return null;
953
+ }
954
+ const receiver = callee.object.name;
955
+ if (GLOBAL_RECEIVERS.has(receiver) && networkCallees.has(method)) {
956
+ return method;
957
+ }
958
+ if (httpClients.has(receiver) && HTTP_CLIENT_METHODS.has(method) && !isLocalDouble(callee.object, receiver)) {
959
+ return `${receiver}.${method}`;
960
+ }
961
+ return null;
962
+ }
963
+ return {
964
+ ImportDeclaration(node) {
965
+ for (const specifier of node.specifiers) {
966
+ importSources.set(specifier.local.name, node.source.value);
967
+ }
968
+ },
969
+ CallExpression(node) {
970
+ recordMock(node);
971
+ const callee = networkCallee(node);
972
+ if (callee !== null) {
973
+ findings.push({ node, callee });
974
+ }
975
+ },
976
+ AssignmentExpression(node) {
977
+ const target = node.left;
978
+ if (target.type === AST_NODE_TYPES7.MemberExpression && target.object.type === AST_NODE_TYPES7.Identifier && GLOBAL_RECEIVERS.has(target.object.name)) {
979
+ const name = staticPropertyName(target);
980
+ if (name !== null) {
981
+ mocked.add(name);
982
+ }
983
+ }
984
+ },
985
+ "Program:exit"() {
986
+ for (const { node, callee } of findings) {
987
+ const root = callee.split(".")[0] ?? callee;
988
+ const source = importSources.get(root);
989
+ if (mocked.has(root) || source !== void 0 && mocked.has(source)) {
990
+ continue;
991
+ }
992
+ context.report({ node, messageId: "realNetworkInUnitTest", data: { callee } });
993
+ }
994
+ }
995
+ };
996
+ }
997
+ });
998
+
489
999
  // src/rules/no-template-trim-empty-ternary.ts
490
- import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
491
- var RULE_NAME8 = "no-template-trim-empty-ternary";
1000
+ import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
1001
+ var RULE_NAME11 = "no-template-trim-empty-ternary";
492
1002
  function isTrimCallOnTemplate(node) {
493
- return node.type === AST_NODE_TYPES5.CallExpression && node.callee.type === AST_NODE_TYPES5.MemberExpression && node.callee.property.type === AST_NODE_TYPES5.Identifier && node.callee.property.name === "trim" && node.callee.object.type === AST_NODE_TYPES5.TemplateLiteral;
1003
+ return node.type === AST_NODE_TYPES8.CallExpression && node.callee.type === AST_NODE_TYPES8.MemberExpression && node.callee.property.type === AST_NODE_TYPES8.Identifier && node.callee.property.name === "trim" && node.callee.object.type === AST_NODE_TYPES8.TemplateLiteral;
494
1004
  }
495
1005
  function matchesTemplateTrimEmptyTest(test) {
496
- if (test.type !== AST_NODE_TYPES5.BinaryExpression) {
1006
+ if (test.type !== AST_NODE_TYPES8.BinaryExpression) {
497
1007
  return false;
498
1008
  }
499
1009
  if (test.operator !== "===" && test.operator !== "!==") {
@@ -502,7 +1012,7 @@ function matchesTemplateTrimEmptyTest(test) {
502
1012
  return isTrimCallOnTemplate(test.left) && isEmptyStringLiteral(test.right) || isEmptyStringLiteral(test.left) && isTrimCallOnTemplate(test.right);
503
1013
  }
504
1014
  var noTemplateTrimEmptyTernaryRule = createRule({
505
- name: RULE_NAME8,
1015
+ name: RULE_NAME11,
506
1016
  meta: {
507
1017
  type: "suggestion",
508
1018
  docs: {
@@ -526,10 +1036,10 @@ var noTemplateTrimEmptyTernaryRule = createRule({
526
1036
  });
527
1037
 
528
1038
  // src/utils/preferEarlyReturn.ts
529
- import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
1039
+ import { AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
530
1040
  var MIN_CONSEQUENT_STATEMENTS = 2;
531
1041
  function getFunctionBlockBody(node) {
532
- if (node.body.type !== AST_NODE_TYPES6.BlockStatement) {
1042
+ if (node.body.type !== AST_NODE_TYPES9.BlockStatement) {
533
1043
  return null;
534
1044
  }
535
1045
  return node.body;
@@ -539,13 +1049,13 @@ function findWrappedHappyPathIf(body) {
539
1049
  return null;
540
1050
  }
541
1051
  const lastStatement = body.body[body.body.length - 1];
542
- if (lastStatement === void 0 || lastStatement.type !== AST_NODE_TYPES6.IfStatement) {
1052
+ if (lastStatement === void 0 || lastStatement.type !== AST_NODE_TYPES9.IfStatement) {
543
1053
  return null;
544
1054
  }
545
1055
  if (lastStatement.alternate !== null) {
546
1056
  return null;
547
1057
  }
548
- if (lastStatement.consequent.type !== AST_NODE_TYPES6.BlockStatement) {
1058
+ if (lastStatement.consequent.type !== AST_NODE_TYPES9.BlockStatement) {
549
1059
  return null;
550
1060
  }
551
1061
  if (lastStatement.consequent.body.length < MIN_CONSEQUENT_STATEMENTS) {
@@ -555,9 +1065,9 @@ function findWrappedHappyPathIf(body) {
555
1065
  }
556
1066
 
557
1067
  // src/rules/prefer-early-return.ts
558
- var RULE_NAME9 = "prefer-early-return";
1068
+ var RULE_NAME12 = "prefer-early-return";
559
1069
  var preferEarlyReturnRule = createRule({
560
- name: RULE_NAME9,
1070
+ name: RULE_NAME12,
561
1071
  meta: {
562
1072
  type: "problem",
563
1073
  docs: {
@@ -588,8 +1098,189 @@ var preferEarlyReturnRule = createRule({
588
1098
  }
589
1099
  });
590
1100
 
1101
+ // src/rules/no-vacuous-expect.ts
1102
+ import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
1103
+ var RULE_NAME13 = "no-vacuous-expect";
1104
+ var DEFAULT_WEAK_MATCHERS = [
1105
+ "toBeDefined",
1106
+ "toBeTruthy",
1107
+ "toBeFalsy",
1108
+ "not.toBeUndefined"
1109
+ ];
1110
+ var DEFAULT_ASSERTION_CALLEES = ["^assert", "^expect\\w", "\\.expect$"];
1111
+ var TYPEOF_RESULTS = /* @__PURE__ */ new Set([
1112
+ "undefined",
1113
+ "object",
1114
+ "boolean",
1115
+ "number",
1116
+ "bigint",
1117
+ "string",
1118
+ "symbol",
1119
+ "function"
1120
+ ]);
1121
+ var EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1122
+ var TEST_RUNNERS = /* @__PURE__ */ new Set(["it", "test"]);
1123
+ var optionSchema6 = {
1124
+ type: "object",
1125
+ additionalProperties: false,
1126
+ properties: {
1127
+ weakMatchers: { type: "array", items: { type: "string", minLength: 1 }, uniqueItems: true },
1128
+ assertionCallees: {
1129
+ type: "array",
1130
+ items: { type: "string", minLength: 1 },
1131
+ uniqueItems: true
1132
+ }
1133
+ }
1134
+ };
1135
+ function isExpectCall2(node) {
1136
+ return node.type === AST_NODE_TYPES10.CallExpression && node.callee.type === AST_NODE_TYPES10.Identifier && node.callee.name === "expect";
1137
+ }
1138
+ function matcherCall(node) {
1139
+ const callee = node.callee;
1140
+ if (callee.type !== AST_NODE_TYPES10.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES10.Identifier) {
1141
+ return null;
1142
+ }
1143
+ let negated = false;
1144
+ let current = callee.object;
1145
+ while (current.type === AST_NODE_TYPES10.MemberExpression) {
1146
+ if (!current.computed && current.property.type === AST_NODE_TYPES10.Identifier && current.property.name === "not") {
1147
+ negated = !negated;
1148
+ }
1149
+ current = current.object;
1150
+ }
1151
+ if (!isExpectCall2(current)) {
1152
+ return null;
1153
+ }
1154
+ const name = callee.property.name;
1155
+ return { root: current, matcher: negated ? `not.${name}` : name };
1156
+ }
1157
+ function calleePath(callee) {
1158
+ if (callee.type === AST_NODE_TYPES10.Identifier) {
1159
+ return callee.name;
1160
+ }
1161
+ if (callee.type === AST_NODE_TYPES10.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES10.Identifier) {
1162
+ const owner = callee.object.type === AST_NODE_TYPES10.Identifier ? callee.object.name : "";
1163
+ return `${owner}.${callee.property.name}`;
1164
+ }
1165
+ return null;
1166
+ }
1167
+ function runnerName2(callee) {
1168
+ let current = callee;
1169
+ for (; ; ) {
1170
+ if (current.type === AST_NODE_TYPES10.MemberExpression) {
1171
+ current = current.object;
1172
+ } else if (current.type === AST_NODE_TYPES10.CallExpression) {
1173
+ current = current.callee;
1174
+ } else if (current.type === AST_NODE_TYPES10.TaggedTemplateExpression) {
1175
+ current = current.tag;
1176
+ } else {
1177
+ break;
1178
+ }
1179
+ }
1180
+ return current.type === AST_NODE_TYPES10.Identifier ? current.name : null;
1181
+ }
1182
+ function isTestCallback(node) {
1183
+ const parent = node.parent;
1184
+ if (parent.type !== AST_NODE_TYPES10.CallExpression || parent.arguments[1] !== node) {
1185
+ return false;
1186
+ }
1187
+ const name = runnerName2(parent.callee);
1188
+ return name !== null && TEST_RUNNERS.has(name);
1189
+ }
1190
+ function isTypeofExpression(node) {
1191
+ return node?.type === AST_NODE_TYPES10.UnaryExpression && node.operator === "typeof";
1192
+ }
1193
+ function isTypeofResult(node) {
1194
+ return node?.type === AST_NODE_TYPES10.Literal && typeof node.value === "string" && TYPEOF_RESULTS.has(node.value);
1195
+ }
1196
+ function isLiteralTautology(actual, expected) {
1197
+ return actual?.type === AST_NODE_TYPES10.Literal && expected?.type === AST_NODE_TYPES10.Literal && !("regex" in actual) && actual.value === expected.value;
1198
+ }
1199
+ var noVacuousExpectRule = createRule({
1200
+ name: RULE_NAME13,
1201
+ meta: {
1202
+ type: "problem",
1203
+ docs: {
1204
+ description: "Disallow vacuous expects (`typeof` checks, literal tautologies, a sole `toBeDefined`/`toBeTruthy`): a test must assert behaviour that a real regression would break."
1205
+ },
1206
+ schema: [optionSchema6],
1207
+ messages: {
1208
+ typeofExpect: "Do not assert on `typeof`; that only proves a binding exists. Assert a result, a thrown error or an observable effect.",
1209
+ tautologyExpect: "This expect compares a literal with itself and can never fail. Assert something a real regression would break.",
1210
+ soleWeakExpect: "A sole `{{matcher}}` does not pin behaviour. Assert on the value or the outcome, or delete the test."
1211
+ }
1212
+ },
1213
+ defaultOptions: [
1214
+ {
1215
+ weakMatchers: [...DEFAULT_WEAK_MATCHERS],
1216
+ assertionCallees: [...DEFAULT_ASSERTION_CALLEES]
1217
+ }
1218
+ ],
1219
+ create(context, [options]) {
1220
+ const weakMatchers = new Set(options.weakMatchers ?? DEFAULT_WEAK_MATCHERS);
1221
+ const assertionCallees = (options.assertionCallees ?? DEFAULT_ASSERTION_CALLEES).map(
1222
+ (source) => new RegExp(source, "u")
1223
+ );
1224
+ const stack = [];
1225
+ function enter(node) {
1226
+ if (isTestCallback(node)) {
1227
+ stack.push({ node, assertions: 0, weak: null });
1228
+ }
1229
+ }
1230
+ function exit(node) {
1231
+ const frame = stack.at(-1);
1232
+ if (frame?.node !== node) {
1233
+ return;
1234
+ }
1235
+ stack.pop();
1236
+ if (frame.assertions === 1 && frame.weak !== null) {
1237
+ context.report({
1238
+ node: frame.weak.node,
1239
+ messageId: "soleWeakExpect",
1240
+ data: { matcher: frame.weak.matcher }
1241
+ });
1242
+ }
1243
+ }
1244
+ return {
1245
+ ArrowFunctionExpression: enter,
1246
+ FunctionExpression: enter,
1247
+ "ArrowFunctionExpression:exit": exit,
1248
+ "FunctionExpression:exit": exit,
1249
+ CallExpression(node) {
1250
+ const frame = stack.at(-1);
1251
+ const call = matcherCall(node);
1252
+ if (call === null) {
1253
+ const path3 = calleePath(node.callee);
1254
+ if (frame !== void 0 && path3 !== null && assertionCallees.some((re) => re.test(path3))) {
1255
+ frame.assertions += 1;
1256
+ }
1257
+ return;
1258
+ }
1259
+ if (frame !== void 0) {
1260
+ frame.assertions += 1;
1261
+ if (weakMatchers.has(call.matcher)) {
1262
+ frame.weak = { node, matcher: call.matcher };
1263
+ }
1264
+ }
1265
+ const negated = call.matcher.startsWith("not.");
1266
+ const base = negated ? call.matcher.slice("not.".length) : call.matcher;
1267
+ if (!EQUALITY_MATCHERS.has(base)) {
1268
+ return;
1269
+ }
1270
+ const actual = call.root.arguments[0];
1271
+ const expected = node.arguments[0];
1272
+ if (isTypeofExpression(actual) && isTypeofResult(expected)) {
1273
+ context.report({ node, messageId: "typeofExpect" });
1274
+ } else if (!negated && isLiteralTautology(actual, expected)) {
1275
+ context.report({ node, messageId: "tautologyExpect" });
1276
+ }
1277
+ }
1278
+ };
1279
+ }
1280
+ });
1281
+
591
1282
  // src/rules/skipped-tests-need-tracking.ts
592
- var RULE_NAME10 = "skipped-tests-need-tracking";
1283
+ var RULE_NAME14 = "skipped-tests-need-tracking";
593
1284
  var SKIP_PATTERNS = [
594
1285
  { pattern: /\b(?:it|test|describe)\.skip\s*\(/u, label: ".skip(" },
595
1286
  { pattern: /\b(?:it|test|describe)\.fixme\s*\(/u, label: ".fixme(" },
@@ -599,7 +1290,7 @@ var SKIP_PATTERNS = [
599
1290
  ];
600
1291
  var DEFAULT_MARKERS = ["https?://\\S+", "TODO\\(@?\\S+\\)"];
601
1292
  var DEFAULT_LOOKBACK = 30;
602
- var optionSchema3 = {
1293
+ var optionSchema7 = {
603
1294
  type: "object",
604
1295
  additionalProperties: false,
605
1296
  properties: {
@@ -613,13 +1304,13 @@ var optionSchema3 = {
613
1304
  }
614
1305
  };
615
1306
  var skippedTestsNeedTrackingRule = createRule({
616
- name: RULE_NAME10,
1307
+ name: RULE_NAME14,
617
1308
  meta: {
618
1309
  type: "problem",
619
1310
  docs: {
620
1311
  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."
621
1312
  },
622
- schema: [optionSchema3],
1313
+ schema: [optionSchema7],
623
1314
  messages: {
624
1315
  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."
625
1316
  }
@@ -671,6 +1362,10 @@ var rules = {
671
1362
  "no-pr-reference-comments": noPrReferenceCommentsRule,
672
1363
  "no-focused-tests": noFocusedTestsRule,
673
1364
  "skipped-tests-need-tracking": skippedTestsNeedTrackingRule,
1365
+ "no-vacuous-expect": noVacuousExpectRule,
1366
+ "no-conditional-expect": noConditionalExpectRule,
1367
+ "fake-timers-must-be-restored": fakeTimersMustBeRestoredRule,
1368
+ "no-real-network-in-unit-tests": noRealNetworkInUnitTestsRule,
674
1369
  // Available but omitted from `recommended` (opinionated / niche).
675
1370
  "interface-prefix-i": interfacePrefixIRule,
676
1371
  "no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule
@@ -678,7 +1373,7 @@ var rules = {
678
1373
 
679
1374
  // src/index.ts
680
1375
  var NAMESPACE = "noctcore-code-quality";
681
- var VERSION = "0.1.0";
1376
+ var VERSION = "0.2.0";
682
1377
  var plugin = {
683
1378
  meta: { name: "@noctcore/eslint-plugin-code-quality", version: VERSION },
684
1379
  rules,