eslint-plugin-no-mistakes 0.90.0 → 0.91.1

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.1",
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 };
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  const { createBindingResolver } = require("./vitest-timeout-binding-resolver");
3
- // One resolver per rule/context; scopes and in-progress cycles never cross requests.
3
+ // Hosts can reuse a rule context across files. Each create starts a fresh program.
4
4
  const resolvers = new WeakMap();
5
+ function resetBindingResolver(context) {
6
+ resolvers.delete(context);
7
+ }
5
8
  function resolver(context) {
6
9
  if (!resolvers.has(context)) resolvers.set(context, createBindingResolver(context));
7
10
  return resolvers.get(context);
@@ -40,6 +43,7 @@ function opaqueCallback(node, context) {
40
43
  return resolver(context).opaqueCallback(node);
41
44
  }
42
45
  module.exports = {
46
+ resetBindingResolver,
43
47
  configCall,
44
48
  overrideCall,
45
49
  staticPropertyName,
@@ -4,6 +4,7 @@ const { isFunction, unwrapExpression } = require("./async-ast");
4
4
  const { resolveVariable, isReassigned } = require("./async-target-bindings");
5
5
  const { ruleSuppression } = require("../rule-suppression");
6
6
  const {
7
+ resetBindingResolver,
7
8
  configCall,
8
9
  overrideCall,
9
10
  recordCall,
@@ -27,6 +28,7 @@ module.exports = rule(
27
28
  defaultMax: { type: "number", minimum: 0, exclusiveMinimum: true },
28
29
  overrideMax: { type: "number", minimum: 0, exclusiveMinimum: true },
29
30
  unknownValues: { enum: ["ignore", "finding"] },
31
+ configUnknownValues: { enum: ["inherit", "defer-to-runner"] },
30
32
  configRoot: { type: "boolean" },
31
33
  },
32
34
  additionalProperties: false,
@@ -42,6 +44,7 @@ module.exports = rule(
42
44
  },
43
45
  },
44
46
  (context) => {
47
+ resetBindingResolver(context);
45
48
  const option = options(context);
46
49
  for (const name of ["defaultMax", "overrideMax"]) {
47
50
  if (option[name] !== undefined && (!Number.isFinite(option[name]) || option[name] <= 0))
@@ -109,10 +112,11 @@ module.exports = rule(
109
112
  for (const original of overrides) {
110
113
  const kind = overrideCall(original, context);
111
114
  if (!kind) continue;
112
- const node = { ...original, arguments: invocationArguments(original, context) };
115
+ const node = original;
116
+ const callArguments = invocationArguments(original, context);
113
117
  const max = option.overrideMax ?? 30000;
114
118
  if (kind === "test" || kind === "hook")
115
- for (const argument of node.arguments)
119
+ for (const argument of callArguments)
116
120
  if (opaqueCallback(argument, context))
117
121
  check(
118
122
  { value: { kind: "unknown" }, origin: argument },
@@ -126,14 +130,14 @@ module.exports = rule(
126
130
  }
127
131
  if (kind === "runtime") {
128
132
  properties(
129
- evaluate(node.arguments[0]),
133
+ evaluate(callArguments[0]),
130
134
  ["testTimeout", "hookTimeout"],
131
135
  max,
132
- node.arguments[0] || node,
136
+ callArguments[0] || node,
133
137
  );
134
138
  continue;
135
139
  }
136
- const args = kind === "hook" ? node.arguments.slice(1) : node.arguments.slice(1, 3);
140
+ const args = kind === "hook" ? callArguments.slice(1) : callArguments.slice(1, 3);
137
141
  for (const argument of args) {
138
142
  if (callback(argument)) continue;
139
143
  const value = evaluate(argument);
@@ -148,17 +152,17 @@ module.exports = rule(
148
152
  );
149
153
  else if (
150
154
  kind === "test" &&
151
- node.arguments.length >= 3 &&
152
- argument !== node.arguments.at(-1) &&
155
+ callArguments.length >= 3 &&
156
+ argument !== callArguments.at(-1) &&
153
157
  value.kind === "unknown"
154
158
  )
155
159
  properties(value, ["timeout"], max, argument);
156
160
  }
157
- const last = node.arguments.at(-1);
161
+ const last = callArguments.at(-1);
158
162
  const minimum = kind === "hook" ? 2 : 3;
159
- if (!last || node.arguments.length < minimum || callback(last)) continue;
163
+ if (!last || callArguments.length < minimum || callback(last)) continue;
160
164
  // An options object followed by a callback is not a trailing timeout.
161
- if (evaluate(node.arguments.at(-2)).kind === "object") continue;
165
+ if (evaluate(callArguments.at(-2)).kind === "object") continue;
162
166
  const value = evaluate(last);
163
167
  if (value.kind !== "object") check({ value, origin: last }, "timeout", max);
164
168
  }
@@ -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