eslint-plugin-no-mistakes 0.90.0 → 0.91.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.90.0",
3
+ "version": "0.91.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": {
@@ -2,28 +2,38 @@
2
2
  const { unwrapExpression, isFunction } = require("./async-ast");
3
3
  const { resolveVariable, isReassigned } = require("./async-target-bindings");
4
4
  function createCarriers(context, evaluate, check, max, fixtureMax) {
5
- function field(value, node, name = "timeout", cap = max) {
5
+ const option = context.options[0] ?? {};
6
+ function configCheck(value, ...args) {
7
+ const unresolved = value.kind !== "number" || value.runtimeFunction;
8
+ if (unresolved && option.configUnknownValues === "defer-to-runner") return;
9
+ check(value, ...args);
10
+ }
11
+ function field(value, node, name = "timeout", cap = max, inspect = check) {
6
12
  if (value.kind !== "object") {
7
- check({ kind: "unknown" }, node, name, cap);
13
+ inspect({ kind: "unknown" }, node, name, cap);
8
14
  return undefined;
9
15
  }
10
16
  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);
17
+ if (entry) inspect(entry.value, entry.origin, name, cap);
18
+ else if (value.unknown) inspect({ kind: "unknown" }, node, name, cap);
13
19
  return entry?.value;
14
20
  }
15
21
  function config(value, node) {
16
22
  if (value.kind !== "object") {
17
- check({ kind: "unknown" }, node, "config carrier");
23
+ configCheck({ kind: "unknown" }, node, "config carrier");
18
24
  return;
19
25
  }
20
- field(value, node, "project timeout");
26
+ field(value, node, "project timeout", max, configCheck);
21
27
  const projects = value.properties.get("projects");
22
28
  if (projects) {
23
29
  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");
30
+ configCheck({ kind: "unknown" }, projects.origin, "projects carrier");
31
+ else {
32
+ if (!projects.value.lengthKnown)
33
+ configCheck({ kind: "unknown" }, projects.origin, "projects carrier");
34
+ for (const entry of projects.value.entries) config(entry.value, entry.origin);
35
+ }
36
+ } else if (value.unknown) configCheck({ kind: "unknown" }, node, "projects carrier");
27
37
  }
28
38
  function fixtures(node) {
29
39
  const value = evaluate(node);
@@ -33,15 +43,26 @@ function createCarriers(context, evaluate, check, max, fixtureMax) {
33
43
  }
34
44
  if (value.unknown) check({ kind: "unknown" }, node, "fixture carrier", fixtureMax);
35
45
  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)))
46
+ // Playwright inspects runtime index 1, irrespective of extra tail elements.
47
+ if (entry.value.kind === "array" && !entry.value.runtimeFunction) {
48
+ const tuple = entry.value;
49
+ if (tuple.knownPrefixLength < 2) {
50
+ if (tuple.entries.length >= 2 || !tuple.lengthKnown)
51
+ check({ kind: "unknown" }, entry.origin, "fixture tuple position", fixtureMax);
52
+ continue;
53
+ }
54
+ const options = tuple.entries[1];
55
+ if (options.value.runtimeFunction) continue;
56
+ if (
57
+ options.value.kind === "number" ||
58
+ ["string", "boolean", "bigint", "undefined"].includes(options.value.runtimeType)
59
+ )
60
+ continue;
61
+ field(options.value, options.origin, "fixture timeout", fixtureMax);
62
+ } else if (
63
+ entry.value.kind === "unknown" &&
64
+ !isFunction(unwrapExpression(entry.origin.value))
65
+ )
45
66
  check({ kind: "unknown" }, entry.origin, "fixture entry carrier", fixtureMax);
46
67
  }
47
68
  }
@@ -78,7 +99,7 @@ function createCarriers(context, evaluate, check, max, fixtureMax) {
78
99
  if (api?.kind === "defineConfig") {
79
100
  checkedConfigs.add(node);
80
101
  if (node.arguments.length === 1) config(evaluate(node.arguments[0]), node.arguments[0]);
81
- else check({ kind: "unknown" }, node, "merged config carrier");
102
+ else configCheck({ kind: "unknown" }, node, "merged config carrier");
82
103
  } else if (
83
104
  (option.configFiles ?? []).some(
84
105
  (file) =>
@@ -91,7 +112,7 @@ function createCarriers(context, evaluate, check, max, fixtureMax) {
91
112
  if (checkedConfigs.has(node) || bindings.resolve(node.callee)?.kind !== "defineConfig")
92
113
  continue;
93
114
  if (node.arguments.length === 1) config(evaluate(node.arguments[0]), node.arguments[0]);
94
- else check({ kind: "unknown" }, node, "merged config carrier");
115
+ else configCheck({ kind: "unknown" }, node, "merged config carrier");
95
116
  }
96
117
  }
97
118
  return { field, configRoots, fixtures, slowCondition };
@@ -9,6 +9,7 @@ module.exports = {
9
9
  max: { type: "number", minimum: 0, exclusiveMinimum: true },
10
10
  fixtureMax: { type: "number", minimum: 0, exclusiveMinimum: true },
11
11
  unknownValues: { enum: ["ignore", "finding"] },
12
+ configUnknownValues: { enum: ["inherit", "defer-to-runner"] },
12
13
  registrationPackages: { type: "array", items: { type: "string" }, uniqueItems: true },
13
14
  exportRoles: {
14
15
  type: "object",
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ function arrayValue(node, evaluate) {
4
+ const entries = [];
5
+ let knownPrefixLength = 0;
6
+ let positionsKnown = true;
7
+ for (const element of node.elements) {
8
+ if (!element) {
9
+ entries.push({ value: { kind: "unknown", runtimeType: "undefined" }, origin: node });
10
+ if (positionsKnown) knownPrefixLength++;
11
+ } else if (element.type === "SpreadElement") {
12
+ const spread = evaluate(element.argument);
13
+ if (spread.kind === "array" && !spread.runtimeFunction) {
14
+ entries.push(...spread.entries.map((entry) => ({ ...entry, origin: element })));
15
+ if (positionsKnown) {
16
+ knownPrefixLength += spread.knownPrefixLength;
17
+ positionsKnown = spread.lengthKnown;
18
+ }
19
+ } else {
20
+ entries.push({ value: { kind: "unknown" }, origin: element });
21
+ positionsKnown = false;
22
+ }
23
+ } else {
24
+ entries.push({ value: evaluate(element), origin: element });
25
+ if (positionsKnown) knownPrefixLength++;
26
+ }
27
+ }
28
+ return { kind: "array", entries, knownPrefixLength, lengthKnown: positionsKnown };
29
+ }
30
+
31
+ function concatenateArrays(left, right) {
32
+ return {
33
+ kind: "array",
34
+ entries: [...left.entries, ...right.entries],
35
+ knownPrefixLength: left.lengthKnown
36
+ ? left.entries.length + right.knownPrefixLength
37
+ : left.knownPrefixLength,
38
+ lengthKnown: left.lengthKnown && right.lengthKnown,
39
+ };
40
+ }
41
+
42
+ module.exports = { arrayValue, concatenateArrays };
@@ -27,6 +27,7 @@ module.exports = rule(
27
27
  defaultMax: { type: "number", minimum: 0, exclusiveMinimum: true },
28
28
  overrideMax: { type: "number", minimum: 0, exclusiveMinimum: true },
29
29
  unknownValues: { enum: ["ignore", "finding"] },
30
+ configUnknownValues: { enum: ["inherit", "defer-to-runner"] },
30
31
  configRoot: { type: "boolean" },
31
32
  },
32
33
  additionalProperties: false,
@@ -109,10 +110,11 @@ module.exports = rule(
109
110
  for (const original of overrides) {
110
111
  const kind = overrideCall(original, context);
111
112
  if (!kind) continue;
112
- const node = { ...original, arguments: invocationArguments(original, context) };
113
+ const node = original;
114
+ const callArguments = invocationArguments(original, context);
113
115
  const max = option.overrideMax ?? 30000;
114
116
  if (kind === "test" || kind === "hook")
115
- for (const argument of node.arguments)
117
+ for (const argument of callArguments)
116
118
  if (opaqueCallback(argument, context))
117
119
  check(
118
120
  { value: { kind: "unknown" }, origin: argument },
@@ -126,14 +128,14 @@ module.exports = rule(
126
128
  }
127
129
  if (kind === "runtime") {
128
130
  properties(
129
- evaluate(node.arguments[0]),
131
+ evaluate(callArguments[0]),
130
132
  ["testTimeout", "hookTimeout"],
131
133
  max,
132
- node.arguments[0] || node,
134
+ callArguments[0] || node,
133
135
  );
134
136
  continue;
135
137
  }
136
- const args = kind === "hook" ? node.arguments.slice(1) : node.arguments.slice(1, 3);
138
+ const args = kind === "hook" ? callArguments.slice(1) : callArguments.slice(1, 3);
137
139
  for (const argument of args) {
138
140
  if (callback(argument)) continue;
139
141
  const value = evaluate(argument);
@@ -148,17 +150,17 @@ module.exports = rule(
148
150
  );
149
151
  else if (
150
152
  kind === "test" &&
151
- node.arguments.length >= 3 &&
152
- argument !== node.arguments.at(-1) &&
153
+ callArguments.length >= 3 &&
154
+ argument !== callArguments.at(-1) &&
153
155
  value.kind === "unknown"
154
156
  )
155
157
  properties(value, ["timeout"], max, argument);
156
158
  }
157
- const last = node.arguments.at(-1);
159
+ const last = callArguments.at(-1);
158
160
  const minimum = kind === "hook" ? 2 : 3;
159
- if (!last || node.arguments.length < minimum || callback(last)) continue;
161
+ if (!last || callArguments.length < minimum || callback(last)) continue;
160
162
  // An options object followed by a callback is not a trailing timeout.
161
- if (evaluate(node.arguments.at(-2)).kind === "object") continue;
163
+ if (evaluate(callArguments.at(-2)).kind === "object") continue;
162
164
  const value = evaluate(last);
163
165
  if (value.kind !== "object") check({ value, origin: last }, "timeout", max);
164
166
  }
@@ -1,5 +1,10 @@
1
1
  "use strict";
2
2
  function createCarriers(option, check) {
3
+ function configCheck(entry, ...args) {
4
+ const unresolved = entry.value.kind !== "number" || entry.value.runtimeFunction;
5
+ if (unresolved && option.configUnknownValues === "defer-to-runner") return;
6
+ check(entry, ...args);
7
+ }
3
8
  function properties(
4
9
  value,
5
10
  names,
@@ -7,30 +12,31 @@ function createCarriers(option, check) {
7
12
  origin,
8
13
  carrier = "timeout options",
9
14
  carrierOrigin = origin,
15
+ inspect = check,
10
16
  ) {
11
17
  if (value.kind !== "object") {
12
- check({ value: { kind: "unknown" }, origin }, carrier, max, origin);
18
+ inspect({ value: { kind: "unknown" }, origin }, carrier, max, origin);
13
19
  return;
14
20
  }
15
21
  let unresolvedKeys = false;
16
22
  for (const name of names) {
17
23
  const entry = value.properties.get(name);
18
- if (entry) check(entry, name, max, origin);
24
+ if (entry) inspect(entry, name, max, origin);
19
25
  else if (value.unknown) unresolvedKeys = true;
20
26
  }
21
27
  if (unresolvedKeys)
22
- check({ value: { kind: "unknown" }, origin: carrierOrigin }, carrier, max, carrierOrigin);
28
+ inspect({ value: { kind: "unknown" }, origin: carrierOrigin }, carrier, max, carrierOrigin);
23
29
  }
24
30
  function defaults(value, project, origin = project) {
25
31
  const max = option.defaultMax ?? 5000;
26
32
  if (value.kind !== "object") {
27
- check({ value: { kind: "unknown" }, origin }, "config carrier", max, origin);
33
+ configCheck({ value: { kind: "unknown" }, origin }, "config carrier", max, origin);
28
34
  return;
29
35
  }
30
36
  const testEntry = value.properties.get("test");
31
37
  if (!testEntry) {
32
38
  if (value.unknown)
33
- check({ value: { kind: "unknown" }, origin }, "config carrier", max, origin);
39
+ configCheck({ value: { kind: "unknown" }, origin }, "config carrier", max, origin);
34
40
  return;
35
41
  }
36
42
  const test = testEntry.value;
@@ -42,22 +48,30 @@ function createCarriers(option, check) {
42
48
  test.kind === "object" ? project : testOrigin,
43
49
  "test options carrier",
44
50
  testOrigin,
51
+ configCheck,
45
52
  );
46
53
  if (test.kind !== "object") return;
47
54
  const projects = test.properties.get("projects");
48
55
  if (projects) {
49
- if (projects.value.kind === "array")
56
+ if (projects.value.kind === "array") {
57
+ if (!projects.value.lengthKnown)
58
+ configCheck(
59
+ { value: { kind: "unknown" }, origin: projects.origin },
60
+ "projects carrier",
61
+ max,
62
+ projects.origin,
63
+ );
50
64
  for (const entry of projects.value.entries)
51
65
  defaults(entry.value, entry.origin, entry.origin);
52
- else
53
- check(
66
+ } else
67
+ configCheck(
54
68
  { value: { kind: "unknown" }, origin: projects.origin },
55
69
  "projects carrier",
56
70
  max,
57
71
  projects.origin,
58
72
  );
59
73
  } else if (test.unknown && !project)
60
- check(
74
+ configCheck(
61
75
  { value: { kind: "unknown" }, origin: testOrigin },
62
76
  "projects carrier",
63
77
  max,
@@ -5,6 +5,7 @@ const { createPrimitiveMemberRead } = require("./vitest-timeout-primitive-reads"
5
5
  const { createValueStability } = require("./vitest-timeout-value-stability");
6
6
  const { createScalarCopyWrite } = require("./vitest-timeout-copy-scalars");
7
7
  const { configCall, staticPropertyName } = require("./vitest-timeout-bindings");
8
+ const { arrayValue, concatenateArrays } = require("./vitest-timeout-array-values");
8
9
  const UNKNOWN = { kind: "unknown" };
9
10
  function taint(value) {
10
11
  if (value.kind === "object")
@@ -22,6 +23,8 @@ function taint(value) {
22
23
  return {
23
24
  ...value,
24
25
  entries: value.entries.map((entry) => ({ ...entry, value: taint(entry.value) })),
26
+ knownPrefixLength: 0,
27
+ lengthKnown: false,
25
28
  };
26
29
  return UNKNOWN;
27
30
  }
@@ -38,7 +41,7 @@ function merge(left, right) {
38
41
  if (previous?.value.kind === "object" && value.kind === "object")
39
42
  value = merge(previous.value, value);
40
43
  if (previous?.value.kind === "array" && value.kind === "array")
41
- value = { kind: "array", entries: [...previous.value.entries, ...value.entries] };
44
+ value = concatenateArrays(previous.value, value);
42
45
  properties.set(name, { ...entry, value });
43
46
  }
44
47
  return { kind: "object", properties, unknown: Boolean(left.unknown || right.unknown) };
@@ -64,8 +67,14 @@ function createEvaluator(context, consumed) {
64
67
  }
65
68
  function resolve(node) {
66
69
  if (node.type === "Literal" && node.value === null) return { kind: "null" };
67
- if (node.type === "Literal")
68
- return typeof node.value === "number" ? { kind: "number", value: node.value } : UNKNOWN;
70
+ if (node.type === "Literal") {
71
+ if (typeof node.value === "number") return { kind: "number", value: node.value };
72
+ return ["string", "boolean", "bigint"].includes(typeof node.value)
73
+ ? { kind: "unknown", runtimeType: typeof node.value }
74
+ : UNKNOWN;
75
+ }
76
+ if (node.type === "UnaryExpression" && node.operator === "void")
77
+ return { kind: "unknown", runtimeType: "undefined" };
69
78
  if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) {
70
79
  const value = evaluate(node.argument);
71
80
  return value.kind === "number"
@@ -154,20 +163,7 @@ function createEvaluator(context, consumed) {
154
163
  }
155
164
  return { kind: "object", properties, unknown };
156
165
  }
157
- if (node.type === "ArrayExpression")
158
- return {
159
- kind: "array",
160
- entries: node.elements.flatMap((origin) => {
161
- if (!origin) return [];
162
- if (origin.type === "SpreadElement") {
163
- const value = evaluate(origin.argument);
164
- return value.kind === "array"
165
- ? value.entries.map((entry) => ({ ...entry, origin }))
166
- : [{ value: UNKNOWN, origin }];
167
- }
168
- return [{ value: evaluate(origin), origin }];
169
- }),
170
- };
166
+ if (node.type === "ArrayExpression") return arrayValue(node, evaluate);
171
167
  if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
172
168
  const statements = node.body.type === "BlockStatement" ? node.body.body : null;
173
169
  const value = statements