eslint-plugin-no-mistakes 0.92.0 → 0.93.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.92.0",
3
+ "version": "0.93.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,12 +2,6 @@
2
2
 
3
3
  const { matches, expressionInfo } = require("./forbidden-member-chains-match");
4
4
 
5
- const PARAM_PARENTS = new Set([
6
- "FunctionDeclaration",
7
- "FunctionExpression",
8
- "ArrowFunctionExpression",
9
- ]);
10
-
11
5
  function patternKey(prop) {
12
6
  const key = prop.key;
13
7
  if (key.type === "Identifier") return key.name;
@@ -24,8 +18,19 @@ function objectPattern(node) {
24
18
  }
25
19
 
26
20
  function destructureVisitors({ compiled, context, suppressed, info }) {
21
+ // The outer pattern and a default can reach the same property and chain.
22
+ // `const { panel: { open } = other.panel } = other` reports that pair once.
23
+ const reported = new WeakMap();
24
+
27
25
  function report(node, chain) {
28
26
  if (suppressed(node)) return;
27
+ let seen = reported.get(node);
28
+ if (seen?.has(chain)) return;
29
+ if (!seen) {
30
+ seen = new Set();
31
+ reported.set(node, seen);
32
+ }
33
+ seen.add(chain);
29
34
  context.report({ node, messageId: "forbiddenChain", data: { message: chain.message } });
30
35
  }
31
36
 
@@ -63,8 +68,11 @@ function destructureVisitors({ compiled, context, suppressed, info }) {
63
68
  const pattern = objectPattern(node.left);
64
69
  if (pattern) check(pattern, node.right);
65
70
  },
71
+ // A default is a receiver at any depth, not only on a function parameter.
72
+ // The parser only allows one in a parameter, property, or array element.
73
+ // `const { x: { open } = other.panel } = {}` reports `*.panel.open`.
74
+ // An array element is too: `const [{ open } = other.panel] = []`.
66
75
  AssignmentPattern(node) {
67
- if (!PARAM_PARENTS.has(node.parent?.type)) return;
68
76
  const pattern = objectPattern(node.left);
69
77
  if (pattern) check(pattern, node.right);
70
78
  },
@@ -17,29 +17,19 @@ function nonStar() {
17
17
  return text(128, "^(?!\\*$)[\\s\\S]+$");
18
18
  }
19
19
 
20
- function pathString(pattern) {
21
- return text(256, pattern);
22
- }
23
-
24
- function segments(maxItems) {
25
- return { type: "array", minItems: 1, maxItems, items: nonStar() };
26
- }
27
-
28
- function starThen(minItems, maxItems) {
29
- return {
30
- type: "array",
31
- minItems,
32
- maxItems,
33
- items: [{ enum: ["*"] }],
34
- additionalItems: nonStar(),
35
- };
36
- }
37
-
38
- function pathSchema(pattern, starMin, plainMax, starMax) {
39
- return {
40
- anyOf: [pathString(pattern), segments(plainMax), starThen(starMin, starMax)],
41
- };
42
- }
20
+ // Segment rules run in assertPath so a bad path names the segment. A JSON Schema
21
+ // anyOf only says the value matched none of the alternatives.
22
+ const pathValue = {
23
+ anyOf: [
24
+ { type: "string", minLength: 1, maxLength: 256 },
25
+ {
26
+ type: "array",
27
+ minItems: 1,
28
+ maxItems: 32,
29
+ items: { type: "string", minLength: 1, maxLength: 128 },
30
+ },
31
+ ],
32
+ };
43
33
 
44
34
  function chain(properties, required) {
45
35
  return {
@@ -60,20 +50,17 @@ const schema = [
60
50
  maxItems: 64,
61
51
  items: {
62
52
  anyOf: [
63
- chain({ path: pathSchema(OPEN_PATH, 2, MAX_SEGMENTS, MAX_SEGMENTS) }, [
64
- "path",
65
- "message",
66
- ]),
53
+ chain({ path: pathValue }, ["path", "message"]),
67
54
  chain(
68
55
  {
69
- path: pathSchema(OPEN_PATH, 2, MAX_SEGMENTS, MAX_SEGMENTS),
56
+ path: pathValue,
70
57
  receiver: { type: "string", enum: ["*"] },
71
58
  },
72
59
  ["path", "message", "receiver"],
73
60
  ),
74
61
  chain(
75
62
  {
76
- path: pathSchema(NAMED_PATH, 1, MAX_SEGMENTS - 1, MAX_SEGMENTS),
63
+ path: pathValue,
77
64
  receiver: nonStar(),
78
65
  },
79
66
  ["path", "message", "receiver"],
@@ -85,7 +72,48 @@ const schema = [
85
72
  },
86
73
  ];
87
74
 
75
+ function shownPath(path) {
76
+ return Array.isArray(path) ? path.join(".") : String(path);
77
+ }
78
+
79
+ function assertPath(path, named) {
80
+ const parts = Array.isArray(path) ? path : String(path).split(".");
81
+ const shown = shownPath(path);
82
+ const empty = parts.findIndex((part) => part === "");
83
+ if (empty !== -1) throw new Error(`empty segment at index ${empty} in '${shown}'`);
84
+ const star = parts.findIndex((part, index) => part === "*" && index !== 0);
85
+ if (star !== -1) {
86
+ throw new Error(`'*' segment at index ${star} in '${shown}'; '*' is only a leading segment`);
87
+ }
88
+ if (typeof path === "string") {
89
+ const pattern = new RegExp(named ? NAMED_PATH : OPEN_PATH);
90
+ if (pattern.test(path)) return;
91
+ const limit = named && parts[0] !== "*" ? MAX_SEGMENTS - 1 : MAX_SEGMENTS;
92
+ if (parts.length > limit) {
93
+ throw new Error(`path '${shown}' has ${parts.length} segments; the maximum is ${limit}`);
94
+ }
95
+ throw new Error(`malformed path '${shown}'`);
96
+ }
97
+ const leadingStar = parts[0] === "*";
98
+ const limit = leadingStar ? MAX_SEGMENTS : named ? MAX_SEGMENTS - 1 : MAX_SEGMENTS;
99
+ const min = leadingStar ? (named ? 1 : 2) : 1;
100
+ if (parts.length < min) {
101
+ throw new Error(`path '${shown}' has ${parts.length} segments; at least ${min} are required`);
102
+ }
103
+ if (parts.length > limit) {
104
+ throw new Error(`path '${shown}' has ${parts.length} segments; the maximum is ${limit}`);
105
+ }
106
+ }
107
+
108
+ function assertChains(entries) {
109
+ if (!Array.isArray(entries)) return;
110
+ for (const entry of entries) {
111
+ assertPath(entry.path, typeof entry.receiver === "string" && entry.receiver !== "*");
112
+ }
113
+ }
114
+
88
115
  function compile(entry) {
116
+ assertPath(entry.path, typeof entry.receiver === "string" && entry.receiver !== "*");
89
117
  const parts = Array.isArray(entry.path) ? entry.path.slice() : entry.path.split(".");
90
118
  let wildcard = false;
91
119
  let body = parts;
@@ -101,4 +129,4 @@ function compile(entry) {
101
129
  return { wildcard, parts: body, message: entry.message };
102
130
  }
103
131
 
104
- module.exports = { schema, compile };
132
+ module.exports = { schema, compile, assertChains };
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const { ruleSuppression } = require("../rule-suppression");
4
- const { schema, compile } = require("./forbidden-member-chains-schema");
4
+ const { schema, compile, assertChains } = require("./forbidden-member-chains-schema");
5
5
  const {
6
6
  matches,
7
7
  computeChain,
@@ -20,6 +20,7 @@ module.exports = {
20
20
  create(context) {
21
21
  const compiled = [];
22
22
  const entries = context.options[0]?.chains;
23
+ assertChains(entries);
23
24
  if (Array.isArray(entries)) {
24
25
  for (const entry of entries) compiled.push(compile(entry));
25
26
  }