eslint-plugin-no-mistakes 0.84.2 → 0.86.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.84.2",
3
+ "version": "0.86.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ const rules = {
20
20
  "playwright-consistent-attribute": require("./rules/playwright-consistent-attribute"),
21
21
  "playwright-defaults": require("./rules/playwright-defaults"),
22
22
  "playwright-assertion-timeout-cap": require("./rules/playwright-assertion-timeout-cap"),
23
+ "playwright-test-timeout-cap": require("./rules/playwright-test-timeout-cap"),
23
24
  "playwright-literals": require("./rules/playwright-literals"),
24
25
  "playwright-naming-convention": require("./rules/playwright-naming-convention"),
25
26
  "playwright-no-empty": require("./rules/playwright-no-empty"),
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ const { unwrapExpression, isFunction } = require("./async-ast");
3
+ const {
4
+ resolveVariable,
5
+ isReassigned,
6
+ recordObjectPatternBindings,
7
+ } = require("./async-target-bindings");
8
+ const { literalString } = require("./module-mock-helpers");
9
+ const HOOKS = new Set(["beforeAll", "beforeEach", "afterAll", "afterEach"]);
10
+ const CHAINS = new Set(["only", "skip", "fixme", "fail", "describe", "parallel", "serial"]);
11
+
12
+ function createBindings(context, modules, exportRoles = {}, calls = []) {
13
+ const active = new Set();
14
+ function callback(node, seen = new Set(), allowPartial = false) {
15
+ node = unwrapExpression(node);
16
+ if (isFunction(node)) return node;
17
+ if (
18
+ node?.type === "CallExpression" &&
19
+ node.callee.type === "MemberExpression" &&
20
+ member(node.callee) === "bind"
21
+ ) {
22
+ const partial =
23
+ node.arguments.length > 1 || node.arguments.some((arg) => arg.type === "SpreadElement");
24
+ return partial && !allowPartial ? null : callback(node.callee.object, seen, allowPartial);
25
+ }
26
+ if (node?.type !== "Identifier") return null;
27
+ const variable = resolveVariable(node, context),
28
+ def = variable?.defs[0];
29
+ if (seen.has(variable)) return null;
30
+ if (def?.type === "FunctionName")
31
+ return variable.references.some((ref) => ref.isWrite() && !ref.init) ? null : def.node;
32
+ if (isReassigned(node, context)) return null;
33
+ if (def?.type !== "Variable" || def.parent.kind !== "const") return null;
34
+ return callback(def.node.init, new Set([...seen, variable]), allowPartial);
35
+ }
36
+ function registrationFor(fn) {
37
+ return calls.filter(
38
+ (call) =>
39
+ call.arguments.some((arg) => callback(arg) === fn) && registration(resolve(call.callee)),
40
+ );
41
+ }
42
+ function name(node, seen = new Set()) {
43
+ node = unwrapExpression(node);
44
+ if (!node) return null;
45
+ const literal = literalString(node);
46
+ if (literal !== null) return literal;
47
+ if (node.type === "BinaryExpression" && node.operator === "+") {
48
+ const left = name(node.left, seen),
49
+ right = name(node.right, seen);
50
+ return left === null || right === null ? null : left + right;
51
+ }
52
+ if (node.type !== "Identifier") return null;
53
+ const variable = resolveVariable(node, context),
54
+ def = variable?.defs[0];
55
+ if (
56
+ seen.has(variable) ||
57
+ def?.type !== "Variable" ||
58
+ def.parent.kind !== "const" ||
59
+ isReassigned(node, context)
60
+ )
61
+ return null;
62
+ return name(def.node.init, new Set([...seen, variable]));
63
+ }
64
+ const member = (node) => (node.computed ? name(node.property) : node.property.name);
65
+ function property(owner, key) {
66
+ if (!owner) return null;
67
+ if (owner.kind === "opaque") return owner;
68
+ if (owner.kind === "namespace") {
69
+ const role =
70
+ owner.source === "@playwright/test" || ["test", "defineConfig", "mergeTests"].includes(key)
71
+ ? undefined
72
+ : exportRoles[owner.source]?.[key];
73
+ if (role === "assertion" || role === "ordinary") return null;
74
+ if (role === "opaque") return { kind: "opaque", root: owner.root };
75
+ if (key === "test" || (key === "default" && owner.source === "@playwright/test"))
76
+ return { kind: "test", root: owner.root, path: [] };
77
+ if (key === "defineConfig" || key === "mergeTests") return { kind: key, root: owner.root };
78
+ return owner.source !== "@playwright/test" ? { kind: "opaque", root: owner.root } : null;
79
+ }
80
+ if (owner.kind === "test") return { kind: "method", owner, key };
81
+ if (owner.kind === "method" && CHAINS.has(owner.key))
82
+ return property(
83
+ { kind: "test", root: owner.owner.root, path: [...owner.owner.path, owner.key] },
84
+ key,
85
+ );
86
+ if (owner.kind === "info" && (key === "setTimeout" || key === "slow" || key === null))
87
+ return { kind: "method", owner, key };
88
+ return null;
89
+ }
90
+ function registration(api) {
91
+ return (
92
+ api?.kind === "test" ||
93
+ (api?.kind === "method" && (CHAINS.has(api.key) || HOOKS.has(api.key)))
94
+ );
95
+ }
96
+ function resolve(node) {
97
+ node = unwrapExpression(node);
98
+ if (!node) return null;
99
+ if (node.type === "MemberExpression") return property(resolve(node.object), member(node));
100
+ if (node.type === "CallExpression") {
101
+ const callee = unwrapExpression(node.callee);
102
+ if (callee.type === "MemberExpression" && member(callee) === "bind") {
103
+ const target = resolve(callee.object),
104
+ receiver = resolve(node.arguments[0]);
105
+ if (target?.kind !== "method") return null;
106
+ if (
107
+ receiver?.kind !== target.owner.kind ||
108
+ receiver.root !== target.owner.root ||
109
+ JSON.stringify(receiver.path) !== JSON.stringify(target.owner.path)
110
+ )
111
+ return {
112
+ kind: "partial",
113
+ target,
114
+ receiverUnresolved: true,
115
+ boundArguments: node.arguments.slice(1),
116
+ };
117
+ // Pre-bound arguments precede later call arguments. Never check the later value as effective.
118
+ return node.arguments.length > 1
119
+ ? { kind: "partial", target, boundArguments: node.arguments.slice(1) }
120
+ : target;
121
+ }
122
+ const api = resolve(callee);
123
+ if (api?.kind === "method" && api.owner.kind === "test") {
124
+ if (api.key === "info") return { kind: "info", root: api.owner.root };
125
+ if (api.key === "extend") return { kind: "test", root: node, path: [] };
126
+ }
127
+ if (api?.kind === "opaque") return api;
128
+ if (api?.kind === "mergeTests")
129
+ return node.arguments.length && node.arguments.every((arg) => resolve(arg)?.kind === "test")
130
+ ? { kind: "test", root: node, path: [] }
131
+ : { kind: "opaque", root: node };
132
+ return null;
133
+ }
134
+ if (node.type !== "Identifier") return null;
135
+ const variable = resolveVariable(node, context);
136
+ if (!variable || active.has(variable)) return null;
137
+ active.add(variable);
138
+ try {
139
+ const def = variable.defs[0];
140
+ if (def?.type === "ImportBinding" && modules.has(def.parent.source.value)) {
141
+ if (def.node.type === "ImportNamespaceSpecifier")
142
+ return { kind: "namespace", root: variable, source: def.parent.source.value };
143
+ const imported = def.node.imported?.name ?? def.node.imported?.value;
144
+ const key =
145
+ def.parent.source.value === "@playwright/test" &&
146
+ (def.node.type === "ImportDefaultSpecifier" || imported === "default")
147
+ ? "test"
148
+ : imported;
149
+ const role =
150
+ def.parent.source.value === "@playwright/test" ||
151
+ ["test", "defineConfig", "mergeTests"].includes(key)
152
+ ? undefined
153
+ : exportRoles[def.parent.source.value]?.[key];
154
+ if (role === "assertion" || role === "ordinary") return null;
155
+ if (role === "opaque") return { kind: "opaque", root: variable };
156
+ if (key === "test") return { kind: "test", root: variable, path: [] };
157
+ if (key === "defineConfig" || key === "mergeTests") return { kind: key, root: variable };
158
+ if (def.parent.source.value !== "@playwright/test")
159
+ return { kind: "opaque", root: variable };
160
+ }
161
+ if (def?.type === "Parameter") {
162
+ const fn = def.node;
163
+ if (!isFunction(fn)) return null;
164
+ const registrations = registrationFor(fn);
165
+ if (!registrations.length) return null;
166
+ const param = fn.params[1];
167
+ if (
168
+ registrations.some((call) => {
169
+ const api = resolve(call.callee);
170
+ return (
171
+ api?.kind === "method" &&
172
+ (api.key === "describe" || api.owner.path?.includes("describe"))
173
+ );
174
+ })
175
+ )
176
+ return null;
177
+ const info = { kind: "info", root: fn };
178
+ if (param?.type === "Identifier" && param.name === node.name) return info;
179
+ let result = null;
180
+ if (param?.type === "ObjectPattern")
181
+ recordObjectPatternBindings(param, info, (id, source, key) => {
182
+ if (id?.name === node.name) result = property(source, key);
183
+ });
184
+ return result;
185
+ }
186
+ if (def?.type !== "Variable" || def.parent.kind !== "const" || isReassigned(node, context))
187
+ return null;
188
+ if (def.node.id.type === "Identifier") return resolve(def.node.init);
189
+ let result = null;
190
+ if (def.node.id.type === "ObjectPattern")
191
+ recordObjectPatternBindings(def.node.id, resolve(def.node.init), (id, source, key) => {
192
+ if (id?.name === node.name) result = property(source, key);
193
+ });
194
+ return result;
195
+ } finally {
196
+ active.delete(variable);
197
+ }
198
+ }
199
+ const opaqueCallback = (node) => !callback(node) && Boolean(callback(node, new Set(), true));
200
+ return { resolve, registration, registrationFor, opaqueCallback, member, hooks: HOOKS };
201
+ }
202
+ module.exports = { createBindings };
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ const { rule, options } = require("../helpers");
3
+ const { ruleSuppression } = require("../rule-suppression");
4
+ const { findContainingFunction, isUnconditionalBeforeReturn } = require("./async-ast");
5
+ const { createEvaluator } = require("./vitest-timeout-values");
6
+ const { createBindings } = require("./playwright-test-timeout-bindings");
7
+ const { createCarriers } = require("./playwright-timeout-carriers");
8
+
9
+ module.exports = rule(require("./playwright-timeout-meta"), (context) => {
10
+ const option = options(context),
11
+ max = option.max ?? 30000,
12
+ fixtureMax = option.fixtureMax ?? max;
13
+ for (const [key, value] of [
14
+ ["max", max],
15
+ ["fixtureMax", fixtureMax],
16
+ ])
17
+ if (!Number.isFinite(value) || value <= 0)
18
+ throw new Error(`playwright-test-timeout-cap ${key} must be positive and finite`);
19
+ for (const [source, roles] of Object.entries(option.exportRoles ?? {})) {
20
+ if (source === "@playwright/test" && Object.keys(roles).length)
21
+ throw new Error(
22
+ "playwright-test-timeout-cap exportRoles cannot override builtin SDK exports",
23
+ );
24
+ if (["test", "defineConfig", "mergeTests"].some((key) => Object.hasOwn(roles, key)))
25
+ throw new Error(
26
+ "playwright-test-timeout-cap exportRoles cannot override recognized registrar exports",
27
+ );
28
+ }
29
+ const suppressed = ruleSuppression(context, "playwright-test-timeout-cap");
30
+ const calls = [],
31
+ exports = [],
32
+ states = new Map();
33
+ const bindings = createBindings(
34
+ context,
35
+ new Set(["@playwright/test", ...(option.registrationPackages ?? [])]),
36
+ option.exportRoles,
37
+ calls,
38
+ );
39
+ const evaluate = createEvaluator(context, new Set());
40
+ function report(node, messageId, name, cap = max) {
41
+ if (!suppressed(node)) context.report({ node, messageId, data: { name, max: cap } });
42
+ }
43
+ function check(value, node, name, cap = max) {
44
+ if (value.kind !== "number" || value.runtimeFunction) {
45
+ if (option.unknownValues === "finding") report(node, "unknown", name, cap);
46
+ } else if (!Number.isFinite(value.value) || value.value <= 0)
47
+ report(node, "invalid", name, cap);
48
+ else if (value.value > cap) report(node, "timeout", name, cap);
49
+ }
50
+ const { field, configRoots, fixtures, slowCondition } = createCarriers(
51
+ context,
52
+ evaluate,
53
+ check,
54
+ max,
55
+ fixtureMax,
56
+ );
57
+ function branchFree(node, fn) {
58
+ return fn?.body.type === "BlockStatement" && isUnconditionalBeforeReturn(node, fn.body);
59
+ }
60
+ return {
61
+ CallExpression(node) {
62
+ calls.push(node);
63
+ },
64
+ ExportDefaultDeclaration(node) {
65
+ exports.push(node.declaration);
66
+ },
67
+ "Program:exit"() {
68
+ configRoots(option, exports, calls, bindings);
69
+ const priorSlowMayExist = calls.some((node) => {
70
+ const api = bindings.resolve(node.callee);
71
+ if (api?.kind !== "method") return false;
72
+ if (bindings.hooks.has(api.key) || api.key === "use" || api.key === null) return true;
73
+ const fn = findContainingFunction(node),
74
+ parentApi =
75
+ fn?.parent?.type === "CallExpression" ? bindings.resolve(fn.parent.callee) : null;
76
+ return (
77
+ api.key === "slow" &&
78
+ (!fn || parentApi?.key === "describe" || parentApi?.owner?.path?.includes("describe"))
79
+ );
80
+ });
81
+ function initialState(fn) {
82
+ const registrations = fn ? bindings.registrationFor(fn) : [];
83
+ const api = registrations.length === 1 ? bindings.resolve(registrations[0].callee) : null;
84
+ const root = api?.kind === "test" ? api.root : api?.owner?.root;
85
+ const directSdk = root?.defs?.some(
86
+ (def) => def.type === "ImportBinding" && def.parent.source.value === "@playwright/test",
87
+ );
88
+ return {
89
+ timeout: { kind: "unknown" },
90
+ slow: directSdk && !priorSlowMayExist ? false : null,
91
+ };
92
+ }
93
+ for (const node of calls.sort((a, b) => a.range[0] - b.range[0])) {
94
+ const api = bindings.resolve(node.callee);
95
+ if (bindings.registration(api) && option.unknownValues === "finding")
96
+ report(node, "effectiveSlot", "inherited effective deadline");
97
+ if (bindings.registration(api))
98
+ for (const argument of node.arguments)
99
+ if (bindings.opaqueCallback(argument))
100
+ check({ kind: "unknown" }, argument, "bound callback parameter carrier");
101
+ if (api?.kind === "mergeTests") {
102
+ for (const argument of node.arguments)
103
+ if (bindings.resolve(argument)?.kind !== "test")
104
+ check({ kind: "unknown" }, argument, "merged registrar carrier");
105
+ continue;
106
+ }
107
+
108
+ if (api?.kind === "opaque") {
109
+ check({ kind: "unknown" }, node, "admitted registrar carrier");
110
+ continue;
111
+ }
112
+ if (api?.kind === "partial") {
113
+ check({ kind: "unknown" }, node, "partially bound framework deadline");
114
+ const state = states.get(findContainingFunction(node));
115
+ if (state) {
116
+ state.timeout = { kind: "unknown" };
117
+ state.slow = null;
118
+ }
119
+ continue;
120
+ }
121
+ if (api?.kind !== "method") {
122
+ // A helper call may mutate the active slot or slow latch. Do not invent effects.
123
+ const state = states.get(findContainingFunction(node));
124
+ if (state) {
125
+ state.timeout = { kind: "unknown" };
126
+ state.slow = null;
127
+ }
128
+ continue;
129
+ }
130
+ const owner = api.owner;
131
+ if (api.key === null) {
132
+ check({ kind: "unknown" }, node, "dynamic framework API");
133
+ continue;
134
+ }
135
+ if (owner.kind === "test" && api.key === "extend") {
136
+ fixtures(node.arguments[0]);
137
+ continue;
138
+ }
139
+ if (owner.kind === "test" && api.key === "configure" && owner.path.includes("describe")) {
140
+ field(evaluate(node.arguments[0]), node.arguments[0], "suite timeout");
141
+ continue;
142
+ }
143
+ if (api.key !== "setTimeout" && api.key !== "slow") {
144
+ // SDK info retrieves TestInfo; step titles/locations do not replace the test slot.
145
+ if (
146
+ owner.kind === "test" &&
147
+ !bindings.registration(api) &&
148
+ !["info", "step"].includes(api.key)
149
+ )
150
+ check({ kind: "unknown" }, node, "admitted framework API carrier");
151
+ continue;
152
+ }
153
+ const fn = findContainingFunction(node);
154
+ const admitted = fn && bindings.registrationFor(fn).length === 1;
155
+ const state = states.get(fn) ?? initialState(fn);
156
+ const ordered = admitted && branchFree(node, fn);
157
+ if (api.key === "setTimeout") {
158
+ const value = evaluate(node.arguments[0]);
159
+ check(value, node.arguments[0] ?? node, "runtime timeout");
160
+ if (ordered && !value.runtimeFunction) state.timeout = value;
161
+ else state.timeout = { kind: "unknown" };
162
+ } else {
163
+ const condition = slowCondition(node);
164
+ if (condition === false) continue;
165
+ if (condition === null || !ordered) {
166
+ check({ kind: "unknown" }, node, "slow effective timeout");
167
+ state.timeout = { kind: "unknown" };
168
+ state.slow = null;
169
+ } else if (state.slow === null) {
170
+ // Unknown inherited latch: multiplication is a safe upper bound, not proof it happened.
171
+ const upper = state.timeout.kind === "number" ? state.timeout.value * 3 : null;
172
+ if (!(upper > 0 && Number.isFinite(upper) && upper <= max))
173
+ check({ kind: "unknown" }, node, "slow effective timeout");
174
+ state.timeout =
175
+ upper > 0 && Number.isFinite(upper) && upper <= max
176
+ ? { kind: "number", value: upper }
177
+ : { kind: "unknown" };
178
+ state.slow = true;
179
+ } else if (!state.slow) {
180
+ state.timeout =
181
+ state.timeout.kind === "number"
182
+ ? { kind: "number", value: state.timeout.value * 3 }
183
+ : { kind: "unknown" };
184
+ check(state.timeout, node, "slow effective timeout");
185
+ state.slow = true;
186
+ }
187
+ }
188
+ states.set(fn, state);
189
+ }
190
+ },
191
+ };
192
+ });
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ const { unwrapExpression, isFunction } = require("./async-ast");
3
+ const { resolveVariable, isReassigned } = require("./async-target-bindings");
4
+ function createCarriers(context, evaluate, check, max, fixtureMax) {
5
+ function field(value, node, name = "timeout", cap = max) {
6
+ if (value.kind !== "object") {
7
+ check({ kind: "unknown" }, node, name, cap);
8
+ return undefined;
9
+ }
10
+ const entry = value.properties.get("timeout");
11
+ if (entry) check(entry.value, entry.origin, name, cap);
12
+ else if (value.unknown) check({ kind: "unknown" }, node, name, cap);
13
+ return entry?.value;
14
+ }
15
+ function config(value, node) {
16
+ if (value.kind !== "object") {
17
+ check({ kind: "unknown" }, node, "config carrier");
18
+ return;
19
+ }
20
+ field(value, node, "project timeout");
21
+ const projects = value.properties.get("projects");
22
+ if (projects) {
23
+ if (projects.value.kind !== "array")
24
+ check({ kind: "unknown" }, projects.origin, "projects carrier");
25
+ else for (const entry of projects.value.entries) config(entry.value, entry.origin);
26
+ } else if (value.unknown) check({ kind: "unknown" }, node, "projects carrier");
27
+ }
28
+ function fixtures(node) {
29
+ const value = evaluate(node);
30
+ if (value.kind !== "object") {
31
+ check({ kind: "unknown" }, node, "fixture carrier", fixtureMax);
32
+ return;
33
+ }
34
+ if (value.unknown) check({ kind: "unknown" }, node, "fixture carrier", fixtureMax);
35
+ for (const entry of value.properties.values()) {
36
+ // Only SDK fixture tuples carry a separate deadline. Ordinary fixture data is not options.
37
+ if (entry.value.kind === "array" && entry.value.entries.length === 2)
38
+ field(
39
+ entry.value.entries[1].value,
40
+ entry.value.entries[1].origin,
41
+ "fixture timeout",
42
+ fixtureMax,
43
+ );
44
+ else if (entry.value.kind === "unknown" && !isFunction(unwrapExpression(entry.origin.value)))
45
+ check({ kind: "unknown" }, entry.origin, "fixture entry carrier", fixtureMax);
46
+ }
47
+ }
48
+ function booleanValue(node, seen = new Set()) {
49
+ node = unwrapExpression(node);
50
+ if (!node) return null;
51
+ if (node.type === "Literal" && typeof node.value === "boolean") return node.value;
52
+ if (node.type === "UnaryExpression" && node.operator === "!") {
53
+ const value = booleanValue(node.argument, seen);
54
+ return value === null ? null : !value;
55
+ }
56
+ if (node.type !== "Identifier") return null;
57
+ const variable = resolveVariable(node, context),
58
+ def = variable?.defs[0];
59
+ if (
60
+ seen.has(variable) ||
61
+ def?.type !== "Variable" ||
62
+ def.parent.kind !== "const" ||
63
+ isReassigned(node, context)
64
+ )
65
+ return null;
66
+ return booleanValue(def.node.init, new Set([...seen, variable]));
67
+ }
68
+ function slowCondition(node) {
69
+ if (!node.arguments.length) return true;
70
+
71
+ // A fixture callback is not evaluated as a config callback: its runtime condition is unknown.
72
+ return booleanValue(node.arguments[0]);
73
+ }
74
+ function configRoots(option, exports, calls, bindings) {
75
+ const checkedConfigs = new Set();
76
+ for (const node of exports) {
77
+ const api = node.type === "CallExpression" ? bindings.resolve(node.callee) : null;
78
+ if (api?.kind === "defineConfig") {
79
+ checkedConfigs.add(node);
80
+ if (node.arguments.length === 1) config(evaluate(node.arguments[0]), node.arguments[0]);
81
+ else check({ kind: "unknown" }, node, "merged config carrier");
82
+ } else if (
83
+ (option.configFiles ?? []).some(
84
+ (file) =>
85
+ context.filename.replace(/\\/g, "/").endsWith("/" + file) || context.filename === file,
86
+ )
87
+ )
88
+ config(evaluate(node), node);
89
+ }
90
+ for (const node of calls) {
91
+ if (checkedConfigs.has(node) || bindings.resolve(node.callee)?.kind !== "defineConfig")
92
+ continue;
93
+ if (node.arguments.length === 1) config(evaluate(node.arguments[0]), node.arguments[0]);
94
+ else check({ kind: "unknown" }, node, "merged config carrier");
95
+ }
96
+ }
97
+ return { field, configRoots, fixtures, slowCondition };
98
+ }
99
+ module.exports = { createCarriers };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ module.exports = {
3
+ type: "problem",
4
+ docs: { description: "require bounded positive Playwright test deadlines", recommended: false },
5
+ schema: [
6
+ {
7
+ type: "object",
8
+ properties: {
9
+ max: { type: "number", minimum: 0, exclusiveMinimum: true },
10
+ fixtureMax: { type: "number", minimum: 0, exclusiveMinimum: true },
11
+ unknownValues: { enum: ["ignore", "finding"] },
12
+ registrationPackages: { type: "array", items: { type: "string" }, uniqueItems: true },
13
+ exportRoles: {
14
+ type: "object",
15
+ additionalProperties: {
16
+ type: "object",
17
+ additionalProperties: { enum: ["assertion", "ordinary", "opaque"] },
18
+ },
19
+ },
20
+ configFiles: { type: "array", items: { type: "string" }, uniqueItems: true },
21
+ },
22
+ additionalProperties: false,
23
+ },
24
+ ],
25
+ messages: {
26
+ effectiveSlot:
27
+ "Playwright registration inherits an unresolved effective deadline/latch; prove its configured owner and helper slots against {{max}} ms.",
28
+ invalid: "Playwright {{name}} must be positive and finite; zero disables the deadline.",
29
+ timeout:
30
+ "Playwright {{name}} exceeds {{max}} ms. Fix the stalled work rather than increasing the deadline.",
31
+ unknown:
32
+ "Playwright {{name}} cannot be resolved against {{max}} ms; keep admitted timeout carriers explicit.",
33
+ },
34
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ const { resolveVariable, isReassigned } = require("./async-target-bindings");
3
+ const { unwrapExpression, isFunction } = require("./async-ast");
4
+ const { literalString } = require("./module-mock-helpers");
5
+ function createCallbackBindings(context) {
6
+ function callback(node, seen = new Set(), allowPartial = false) {
7
+ node = unwrapExpression(node);
8
+ if (isFunction(node)) return node;
9
+ if (
10
+ node?.type === "CallExpression" &&
11
+ node.callee.type === "MemberExpression" &&
12
+ member(node.callee) === "bind"
13
+ ) {
14
+ const partial =
15
+ node.arguments.length > 1 || node.arguments.some((arg) => arg.type === "SpreadElement");
16
+ return partial && !allowPartial ? null : callback(node.callee.object, seen, allowPartial);
17
+ }
18
+ if (node?.type !== "Identifier") return null;
19
+ const variable = resolveVariable(node, context),
20
+ def = variable?.defs[0];
21
+ if (seen.has(variable)) return null;
22
+ if (def?.type === "FunctionName")
23
+ return variable.references.some((ref) => ref.isWrite() && !ref.init) ? null : def.node;
24
+ if (isReassigned(node, context)) return null;
25
+ if (def?.type !== "Variable" || def.parent.kind !== "const") return null;
26
+ return callback(def.node.init, new Set([...seen, variable]), allowPartial);
27
+ }
28
+ function staticName(node, seen = new Set()) {
29
+ node = unwrapExpression(node);
30
+ const literal = literalString(node);
31
+ if (literal !== null) return literal;
32
+ if (node?.type === "BinaryExpression" && node.operator === "+") {
33
+ const left = staticName(node.left, seen),
34
+ right = staticName(node.right, seen);
35
+ return left === null || right === null ? null : left + right;
36
+ }
37
+ if (node?.type !== "Identifier") return null;
38
+ const variable = resolveVariable(node, context),
39
+ def = variable?.defs[0];
40
+ if (
41
+ seen.has(variable) ||
42
+ def?.type !== "Variable" ||
43
+ def.parent.kind !== "const" ||
44
+ isReassigned(node, context)
45
+ )
46
+ return null;
47
+ return staticName(def.node.init, new Set([...seen, variable]));
48
+ }
49
+ const member = (node) => (node.computed ? staticName(node.property) : node.property.name);
50
+ return { callback, staticName, member };
51
+ }
52
+ module.exports = { createCallbackBindings };
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ const { unwrapExpression, unwrapTransparentParent } = require("./async-ast");
3
+ const { resolveVariable } = require("./async-target-bindings");
4
+
5
+ function initializer(variable) {
6
+ const def = variable?.defs[0];
7
+ return variable?.defs.length === 1 &&
8
+ def.type === "Variable" &&
9
+ def.parent.kind === "const" &&
10
+ def.node.id.type === "Identifier" &&
11
+ !["ExportNamedDeclaration", "ExportDefaultDeclaration"].includes(def.parent.parent?.type)
12
+ ? def.node.init
13
+ : null;
14
+ }
15
+ function immutableArray(variable, context, active = new Set()) {
16
+ if (!initializer(variable) || active.has(variable)) return false;
17
+ active.add(variable);
18
+ const result = variable.references.every((reference) => {
19
+ if (reference.init) return true;
20
+ if (reference.isWrite()) return false;
21
+ const node = unwrapTransparentParent(reference.identifier),
22
+ parent = node.parent;
23
+ // Spreads consume array elements, without exposing the array receiver.
24
+ if (parent?.type === "SpreadElement")
25
+ return ["ArrayExpression", "ObjectExpression"].includes(parent.parent.type);
26
+ return (
27
+ parent?.type === "VariableDeclarator" &&
28
+ parent.init === node &&
29
+ parent.id.type === "Identifier" &&
30
+ immutableArray(resolveVariable(parent.id, context), context, active)
31
+ );
32
+ });
33
+ active.delete(variable);
34
+ return result;
35
+ }
36
+ function arrayLength(expression, context, active = new Set()) {
37
+ const node = unwrapExpression(expression);
38
+ if (node?.type === "Identifier") {
39
+ const variable = resolveVariable(node, context);
40
+ if (!immutableArray(variable, context) || active.has(variable)) return null;
41
+ active.add(variable);
42
+ const length = arrayLength(initializer(variable), context, active);
43
+ active.delete(variable);
44
+ return length;
45
+ }
46
+ if (node?.type !== "ArrayExpression") return null;
47
+ let length = 0;
48
+ for (const element of node.elements) {
49
+ const added =
50
+ element?.type === "SpreadElement" ? arrayLength(element.argument, context, active) : 1;
51
+ if (added === null) return null;
52
+ length += added;
53
+ }
54
+ return length;
55
+ }
56
+ module.exports = { arrayLength };