@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/README.md +10 -0
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/dist/index.cjs +796 -91
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +785 -90
- package/docs/rules/fake-timers-must-be-restored.md +108 -0
- package/docs/rules/no-conditional-expect.md +82 -0
- package/docs/rules/no-real-network-in-unit-tests.md +75 -0
- package/docs/rules/no-vacuous-expect.md +89 -0
- package/package.json +4 -3
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/
|
|
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
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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: [
|
|
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
|
|
213
|
-
var
|
|
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 ===
|
|
545
|
+
while (current.type === import_utils5.AST_NODE_TYPES.MemberExpression && !current.computed) {
|
|
219
546
|
current = current.object;
|
|
220
547
|
}
|
|
221
|
-
return current.type ===
|
|
548
|
+
return current.type === import_utils5.AST_NODE_TYPES.Identifier ? current.name : null;
|
|
222
549
|
}
|
|
223
550
|
var noFocusedTestsRule = createRule({
|
|
224
|
-
name:
|
|
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 !==
|
|
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 ===
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
777
|
+
var import_utils7 = require("@typescript-eslint/utils");
|
|
451
778
|
|
|
452
779
|
// src/utils/ast.ts
|
|
453
|
-
var
|
|
780
|
+
var import_utils6 = require("@typescript-eslint/utils");
|
|
454
781
|
function isEmptyStringLiteral(node) {
|
|
455
|
-
return node.type ===
|
|
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 !==
|
|
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 ===
|
|
789
|
+
return node.property.type === import_utils6.AST_NODE_TYPES.Literal && node.property.value === propertyName;
|
|
463
790
|
}
|
|
464
|
-
return node.property.type ===
|
|
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
|
|
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
|
|
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:
|
|
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: [
|
|
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
|
|
519
|
-
var
|
|
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 ===
|
|
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 !==
|
|
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:
|
|
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
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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
|
|
1106
|
+
var RULE_NAME12 = "prefer-early-return";
|
|
587
1107
|
var preferEarlyReturnRule = createRule({
|
|
588
|
-
name:
|
|
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
|
|
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
|
|
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:
|
|
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: [
|
|
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.
|
|
1414
|
+
var VERSION = "0.2.0";
|
|
710
1415
|
var plugin = {
|
|
711
1416
|
meta: { name: "@noctcore/eslint-plugin-code-quality", version: VERSION },
|
|
712
1417
|
rules,
|