eslint-plugin-playwright 1.0.1 → 1.1.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 CHANGED
@@ -131,6 +131,7 @@ command line option.\
131
131
  | ✔ | | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option |
132
132
  | | | | [no-nth-methods](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md) | Disallow usage of `first()`, `last()`, and `nth()` methods |
133
133
  | ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause()` |
134
+ | ✔ | 🔧 | | [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` |
134
135
  | | 🔧 | | [no-get-by-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-get-by-title.md) | Disallow using `getByTitle()` |
135
136
  | | | | [no-raw-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md) | Disallow using raw locators |
136
137
  | ✔ | 🔧 | | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods |
package/dist/index.d.mts CHANGED
@@ -26,6 +26,7 @@ declare const _default: {
26
26
  'no-raw-locators': eslint.Rule.RuleModule;
27
27
  'no-restricted-matchers': eslint.Rule.RuleModule;
28
28
  'no-skipped-test': eslint.Rule.RuleModule;
29
+ 'no-unsafe-references': eslint.Rule.RuleModule;
29
30
  'no-useless-await': eslint.Rule.RuleModule;
30
31
  'no-useless-not': eslint.Rule.RuleModule;
31
32
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -76,6 +77,7 @@ declare const _default: {
76
77
  'no-raw-locators': eslint.Rule.RuleModule;
77
78
  'no-restricted-matchers': eslint.Rule.RuleModule;
78
79
  'no-skipped-test': eslint.Rule.RuleModule;
80
+ 'no-unsafe-references': eslint.Rule.RuleModule;
79
81
  'no-useless-await': eslint.Rule.RuleModule;
80
82
  'no-useless-not': eslint.Rule.RuleModule;
81
83
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -108,6 +110,7 @@ declare const _default: {
108
110
  'playwright/no-networkidle': string;
109
111
  'playwright/no-page-pause': string;
110
112
  'playwright/no-skipped-test': string;
113
+ 'playwright/no-unsafe-references': string;
111
114
  'playwright/no-useless-await': string;
112
115
  'playwright/no-useless-not': string;
113
116
  'playwright/no-wait-for-selector': string;
@@ -158,6 +161,7 @@ declare const _default: {
158
161
  'playwright/no-networkidle': string;
159
162
  'playwright/no-page-pause': string;
160
163
  'playwright/no-skipped-test': string;
164
+ 'playwright/no-unsafe-references': string;
161
165
  'playwright/no-useless-await': string;
162
166
  'playwright/no-useless-not': string;
163
167
  'playwright/no-wait-for-selector': string;
@@ -186,6 +190,7 @@ declare const _default: {
186
190
  'playwright/no-networkidle': string;
187
191
  'playwright/no-page-pause': string;
188
192
  'playwright/no-skipped-test': string;
193
+ 'playwright/no-unsafe-references': string;
189
194
  'playwright/no-useless-await': string;
190
195
  'playwright/no-useless-not': string;
191
196
  'playwright/no-wait-for-selector': string;
@@ -213,6 +218,7 @@ declare const _default: {
213
218
  'no-raw-locators': eslint.Rule.RuleModule;
214
219
  'no-restricted-matchers': eslint.Rule.RuleModule;
215
220
  'no-skipped-test': eslint.Rule.RuleModule;
221
+ 'no-unsafe-references': eslint.Rule.RuleModule;
216
222
  'no-useless-await': eslint.Rule.RuleModule;
217
223
  'no-useless-not': eslint.Rule.RuleModule;
218
224
  'no-wait-for-selector': eslint.Rule.RuleModule;
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ declare const _default: {
26
26
  'no-raw-locators': eslint.Rule.RuleModule;
27
27
  'no-restricted-matchers': eslint.Rule.RuleModule;
28
28
  'no-skipped-test': eslint.Rule.RuleModule;
29
+ 'no-unsafe-references': eslint.Rule.RuleModule;
29
30
  'no-useless-await': eslint.Rule.RuleModule;
30
31
  'no-useless-not': eslint.Rule.RuleModule;
31
32
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -76,6 +77,7 @@ declare const _default: {
76
77
  'no-raw-locators': eslint.Rule.RuleModule;
77
78
  'no-restricted-matchers': eslint.Rule.RuleModule;
78
79
  'no-skipped-test': eslint.Rule.RuleModule;
80
+ 'no-unsafe-references': eslint.Rule.RuleModule;
79
81
  'no-useless-await': eslint.Rule.RuleModule;
80
82
  'no-useless-not': eslint.Rule.RuleModule;
81
83
  'no-wait-for-selector': eslint.Rule.RuleModule;
@@ -108,6 +110,7 @@ declare const _default: {
108
110
  'playwright/no-networkidle': string;
109
111
  'playwright/no-page-pause': string;
110
112
  'playwright/no-skipped-test': string;
113
+ 'playwright/no-unsafe-references': string;
111
114
  'playwright/no-useless-await': string;
112
115
  'playwright/no-useless-not': string;
113
116
  'playwright/no-wait-for-selector': string;
@@ -158,6 +161,7 @@ declare const _default: {
158
161
  'playwright/no-networkidle': string;
159
162
  'playwright/no-page-pause': string;
160
163
  'playwright/no-skipped-test': string;
164
+ 'playwright/no-unsafe-references': string;
161
165
  'playwright/no-useless-await': string;
162
166
  'playwright/no-useless-not': string;
163
167
  'playwright/no-wait-for-selector': string;
@@ -186,6 +190,7 @@ declare const _default: {
186
190
  'playwright/no-networkidle': string;
187
191
  'playwright/no-page-pause': string;
188
192
  'playwright/no-skipped-test': string;
193
+ 'playwright/no-unsafe-references': string;
189
194
  'playwright/no-useless-await': string;
190
195
  'playwright/no-useless-not': string;
191
196
  'playwright/no-wait-for-selector': string;
@@ -213,6 +218,7 @@ declare const _default: {
213
218
  'no-raw-locators': eslint.Rule.RuleModule;
214
219
  'no-restricted-matchers': eslint.Rule.RuleModule;
215
220
  'no-skipped-test': eslint.Rule.RuleModule;
221
+ 'no-unsafe-references': eslint.Rule.RuleModule;
216
222
  'no-useless-await': eslint.Rule.RuleModule;
217
223
  'no-useless-not': eslint.Rule.RuleModule;
218
224
  'no-wait-for-selector': eslint.Rule.RuleModule;
package/dist/index.js CHANGED
@@ -123,6 +123,19 @@ function dig(node, identifier) {
123
123
  function isPageMethod(node, name) {
124
124
  return node.callee.type === "MemberExpression" && dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) && isPropertyAccessor(node.callee, name);
125
125
  }
126
+ function isFunction(node) {
127
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
128
+ }
129
+
130
+ // src/utils/misc.ts
131
+ var getAmountData = (amount) => ({
132
+ amount: amount.toString(),
133
+ s: amount === 1 ? "" : "s"
134
+ });
135
+ function getSourceCode(context) {
136
+ return context.sourceCode ?? context.getSourceCode();
137
+ }
138
+ var truthy = Boolean;
126
139
 
127
140
  // src/rules/expect-expect.ts
128
141
  function isAssertionCall(context, node, assertFunctionNames) {
@@ -134,7 +147,7 @@ var expect_expect_default = {
134
147
  assertFunctionNames: [],
135
148
  ...context.options?.[0] ?? {}
136
149
  };
137
- const sourceCode = context.sourceCode ?? context.getSourceCode();
150
+ const sourceCode = getSourceCode(context);
138
151
  const unchecked = [];
139
152
  function checkExpressions(nodes) {
140
153
  for (const node of nodes) {
@@ -323,7 +336,7 @@ function getCallType(context, node, awaitableMatchers) {
323
336
  }
324
337
  var missing_playwright_await_default = {
325
338
  create(context) {
326
- const sourceCode = context.sourceCode ?? context.getSourceCode();
339
+ const sourceCode = getSourceCode(context);
327
340
  const options = context.options[0] || {};
328
341
  const awaitableMatchers = /* @__PURE__ */ new Set([
329
342
  ...expectPlaywrightMatchers,
@@ -556,7 +569,7 @@ var no_focused_test_default = {
556
569
 
557
570
  // src/rules/no-force-option.ts
558
571
  function isForceOptionEnabled(node) {
559
- const arg = node.arguments[node.arguments.length - 1];
572
+ const arg = node.arguments.at(-1);
560
573
  return arg?.type === "ObjectExpression" && arg.properties.find(
561
574
  (property) => property.type === "Property" && getStringValue(property.key) === "force" && isBooleanLiteral(property.value, true)
562
575
  );
@@ -901,7 +914,7 @@ var no_restricted_matchers_default = {
901
914
  context.report({
902
915
  data: { message: message ?? "", restriction },
903
916
  loc: {
904
- end: chain[chain.length - 1].loc.end,
917
+ end: chain.at(-1).loc.end,
905
918
  start: chain[0].loc.start
906
919
  },
907
920
  messageId: message ? "restrictedWithMessage" : "restricted"
@@ -993,6 +1006,93 @@ var no_skipped_test_default = {
993
1006
  }
994
1007
  };
995
1008
 
1009
+ // src/rules/no-unsafe-references.ts
1010
+ function collectVariables(scope) {
1011
+ if (!scope)
1012
+ return [];
1013
+ return [
1014
+ ...collectVariables(scope.upper),
1015
+ ...scope.variables.map((ref) => ref.name)
1016
+ ];
1017
+ }
1018
+ function addArgument(fixer, node, refs) {
1019
+ if (!node.arguments.length)
1020
+ return;
1021
+ if (node.arguments.length === 1) {
1022
+ return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
1023
+ }
1024
+ const arr = node.arguments.at(-1);
1025
+ if (!arr || arr.type !== "ArrayExpression")
1026
+ return;
1027
+ const lastItem = arr.elements.at(-1);
1028
+ return lastItem ? fixer.insertTextAfter(lastItem, `, ${refs}`) : fixer.replaceText(arr, `[${refs}]`);
1029
+ }
1030
+ function getParen(sourceCode, node) {
1031
+ let token = sourceCode.getFirstToken(node);
1032
+ while (token && token.value !== "(") {
1033
+ token = sourceCode.getTokenAfter(token);
1034
+ }
1035
+ return token;
1036
+ }
1037
+ function addParam(sourceCode, fixer, node, refs) {
1038
+ const lastParam = node.params.at(-1);
1039
+ if (lastParam) {
1040
+ return fixer.insertTextAfter(lastParam, `, ${refs}`);
1041
+ }
1042
+ const token = getParen(sourceCode, node);
1043
+ return token ? fixer.insertTextAfter(token, `[${refs}]`) : null;
1044
+ }
1045
+ var no_unsafe_references_default = {
1046
+ create(context) {
1047
+ return {
1048
+ CallExpression(node) {
1049
+ if (!isPageMethod(node, "evaluate"))
1050
+ return;
1051
+ const [fn] = node.arguments;
1052
+ if (!fn || !isFunction(fn))
1053
+ return;
1054
+ const sourceCode = getSourceCode(context);
1055
+ const { through, upper } = sourceCode.getScope(fn.body);
1056
+ const allRefs = new Set(collectVariables(upper));
1057
+ through.filter((ref) => allRefs.has(ref.identifier.name)).forEach((ref, i, arr) => {
1058
+ const descriptor = {
1059
+ data: { variable: ref.identifier.name },
1060
+ messageId: "noUnsafeReference",
1061
+ node: ref.identifier
1062
+ };
1063
+ if (i !== 0) {
1064
+ context.report(descriptor);
1065
+ return;
1066
+ }
1067
+ context.report({
1068
+ ...descriptor,
1069
+ fix(fixer) {
1070
+ const refs = arr.map((ref2) => ref2.identifier.name).join(", ");
1071
+ return [
1072
+ addArgument(fixer, node, refs),
1073
+ addParam(sourceCode, fixer, fn, refs)
1074
+ ].filter(truthy);
1075
+ }
1076
+ });
1077
+ });
1078
+ }
1079
+ };
1080
+ },
1081
+ meta: {
1082
+ docs: {
1083
+ category: "Possible Errors",
1084
+ description: "Prevent unsafe variable references in page.evaluate()",
1085
+ recommended: true,
1086
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md"
1087
+ },
1088
+ fixable: "code",
1089
+ messages: {
1090
+ noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.evaluate()'
1091
+ },
1092
+ type: "problem"
1093
+ }
1094
+ };
1095
+
996
1096
  // src/rules/no-useless-await.ts
997
1097
  var locatorMethods = /* @__PURE__ */ new Set([
998
1098
  "and",
@@ -1465,7 +1565,7 @@ var prefer_to_contain_default = {
1465
1565
  );
1466
1566
  context.report({
1467
1567
  fix(fixer) {
1468
- const sourceCode = context.sourceCode ?? context.getSourceCode();
1568
+ const sourceCode = getSourceCode(context);
1469
1569
  const addNotModifier = matcherArg.type === "Literal" && matcherArg.value === !!notModifier;
1470
1570
  const fixes = [
1471
1571
  // remove the "includes" call entirely
@@ -1661,6 +1761,19 @@ var supportedMatchers = /* @__PURE__ */ new Set([
1661
1761
  "toBeTruthy",
1662
1762
  "toBeFalsy"
1663
1763
  ]);
1764
+ function dereference(context, node) {
1765
+ if (node.type !== "Identifier") {
1766
+ return node;
1767
+ }
1768
+ const sourceCode = getSourceCode(context);
1769
+ const scope = sourceCode.getScope(node);
1770
+ for (const ref of scope.references) {
1771
+ const refParent = ref.identifier.parent;
1772
+ if (refParent.type === "VariableDeclarator") {
1773
+ return refParent.init;
1774
+ }
1775
+ }
1776
+ }
1664
1777
  var prefer_web_first_assertions_default = {
1665
1778
  create(context) {
1666
1779
  return {
@@ -1668,8 +1781,8 @@ var prefer_web_first_assertions_default = {
1668
1781
  const expectCall = parseExpectCall(context, node);
1669
1782
  if (!expectCall)
1670
1783
  return;
1671
- const [arg] = node.arguments;
1672
- if (arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1784
+ const arg = dereference(context, node.arguments[0]);
1785
+ if (!arg || arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1673
1786
  return;
1674
1787
  }
1675
1788
  if (!supportedMatchers.has(expectCall.matcherName))
@@ -1693,7 +1806,7 @@ var prefer_web_first_assertions_default = {
1693
1806
  },
1694
1807
  fix: (fixer) => {
1695
1808
  const methodArgs = arg.argument.type === "CallExpression" ? arg.argument.arguments : [];
1696
- const methodEnd = methodArgs.length ? methodArgs[methodArgs.length - 1].range[1] + 1 : callee.property.range[1] + 2;
1809
+ const methodEnd = methodArgs.length ? methodArgs.at(-1).range[1] + 1 : callee.property.range[1] + 2;
1697
1810
  const fixes = [
1698
1811
  // Add await to the expect call
1699
1812
  fixer.insertTextBefore(node, "await "),
@@ -1791,12 +1904,6 @@ var require_soft_assertions_default = {
1791
1904
  }
1792
1905
  };
1793
1906
 
1794
- // src/utils/misc.ts
1795
- var getAmountData = (amount) => ({
1796
- amount: amount.toString(),
1797
- s: amount === 1 ? "" : "s"
1798
- });
1799
-
1800
1907
  // src/rules/require-top-level-describe.ts
1801
1908
  var require_top_level_describe_default = {
1802
1909
  create(context) {
@@ -2180,6 +2287,7 @@ var index = {
2180
2287
  "no-raw-locators": no_raw_locators_default,
2181
2288
  "no-restricted-matchers": no_restricted_matchers_default,
2182
2289
  "no-skipped-test": no_skipped_test_default,
2290
+ "no-unsafe-references": no_unsafe_references_default,
2183
2291
  "no-useless-await": no_useless_await_default,
2184
2292
  "no-useless-not": no_useless_not_default,
2185
2293
  "no-wait-for-selector": no_wait_for_selector_default,
@@ -2212,6 +2320,7 @@ var sharedConfig = {
2212
2320
  "playwright/no-networkidle": "error",
2213
2321
  "playwright/no-page-pause": "warn",
2214
2322
  "playwright/no-skipped-test": "warn",
2323
+ "playwright/no-unsafe-references": "error",
2215
2324
  "playwright/no-useless-await": "warn",
2216
2325
  "playwright/no-useless-not": "warn",
2217
2326
  "playwright/no-wait-for-selector": "warn",
package/dist/index.mjs CHANGED
@@ -93,6 +93,9 @@ function dig(node, identifier) {
93
93
  function isPageMethod(node, name) {
94
94
  return node.callee.type === "MemberExpression" && dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) && isPropertyAccessor(node.callee, name);
95
95
  }
96
+ function isFunction(node) {
97
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
98
+ }
96
99
  var isTemplateLiteral, describeProperties, testHooks, expectSubCommands;
97
100
  var init_ast = __esm({
98
101
  "src/utils/ast.ts"() {
@@ -111,6 +114,22 @@ var init_ast = __esm({
111
114
  }
112
115
  });
113
116
 
117
+ // src/utils/misc.ts
118
+ function getSourceCode(context) {
119
+ return context.sourceCode ?? context.getSourceCode();
120
+ }
121
+ var getAmountData, truthy;
122
+ var init_misc = __esm({
123
+ "src/utils/misc.ts"() {
124
+ "use strict";
125
+ getAmountData = (amount) => ({
126
+ amount: amount.toString(),
127
+ s: amount === 1 ? "" : "s"
128
+ });
129
+ truthy = Boolean;
130
+ }
131
+ });
132
+
114
133
  // src/rules/expect-expect.ts
115
134
  function isAssertionCall(context, node, assertFunctionNames) {
116
135
  return isExpectCall(context, node) || assertFunctionNames.find((name) => dig(node.callee, name));
@@ -120,13 +139,14 @@ var init_expect_expect = __esm({
120
139
  "src/rules/expect-expect.ts"() {
121
140
  "use strict";
122
141
  init_ast();
142
+ init_misc();
123
143
  expect_expect_default = {
124
144
  create(context) {
125
145
  const options = {
126
146
  assertFunctionNames: [],
127
147
  ...context.options?.[0] ?? {}
128
148
  };
129
- const sourceCode = context.sourceCode ?? context.getSourceCode();
149
+ const sourceCode = getSourceCode(context);
130
150
  const unchecked = [];
131
151
  function checkExpressions(nodes) {
132
152
  for (const node of nodes) {
@@ -275,6 +295,7 @@ var init_missing_playwright_await = __esm({
275
295
  "src/rules/missing-playwright-await.ts"() {
276
296
  "use strict";
277
297
  init_ast();
298
+ init_misc();
278
299
  validTypes = /* @__PURE__ */ new Set([
279
300
  "AwaitExpression",
280
301
  "ReturnStatement",
@@ -329,7 +350,7 @@ var init_missing_playwright_await = __esm({
329
350
  ];
330
351
  missing_playwright_await_default = {
331
352
  create(context) {
332
- const sourceCode = context.sourceCode ?? context.getSourceCode();
353
+ const sourceCode = getSourceCode(context);
333
354
  const options = context.options[0] || {};
334
355
  const awaitableMatchers = /* @__PURE__ */ new Set([
335
356
  ...expectPlaywrightMatchers,
@@ -592,7 +613,7 @@ var init_no_focused_test = __esm({
592
613
 
593
614
  // src/rules/no-force-option.ts
594
615
  function isForceOptionEnabled(node) {
595
- const arg = node.arguments[node.arguments.length - 1];
616
+ const arg = node.arguments.at(-1);
596
617
  return arg?.type === "ObjectExpression" && arg.properties.find(
597
618
  (property) => property.type === "Property" && getStringValue(property.key) === "force" && isBooleanLiteral(property.value, true)
598
619
  );
@@ -999,7 +1020,7 @@ var init_no_restricted_matchers = __esm({
999
1020
  context.report({
1000
1021
  data: { message: message ?? "", restriction },
1001
1022
  loc: {
1002
- end: chain[chain.length - 1].loc.end,
1023
+ end: chain.at(-1).loc.end,
1003
1024
  start: chain[0].loc.start
1004
1025
  },
1005
1026
  messageId: message ? "restrictedWithMessage" : "restricted"
@@ -1100,6 +1121,101 @@ var init_no_skipped_test = __esm({
1100
1121
  }
1101
1122
  });
1102
1123
 
1124
+ // src/rules/no-unsafe-references.ts
1125
+ function collectVariables(scope) {
1126
+ if (!scope)
1127
+ return [];
1128
+ return [
1129
+ ...collectVariables(scope.upper),
1130
+ ...scope.variables.map((ref) => ref.name)
1131
+ ];
1132
+ }
1133
+ function addArgument(fixer, node, refs) {
1134
+ if (!node.arguments.length)
1135
+ return;
1136
+ if (node.arguments.length === 1) {
1137
+ return fixer.insertTextAfter(node.arguments[0], `, [${refs}]`);
1138
+ }
1139
+ const arr = node.arguments.at(-1);
1140
+ if (!arr || arr.type !== "ArrayExpression")
1141
+ return;
1142
+ const lastItem = arr.elements.at(-1);
1143
+ return lastItem ? fixer.insertTextAfter(lastItem, `, ${refs}`) : fixer.replaceText(arr, `[${refs}]`);
1144
+ }
1145
+ function getParen(sourceCode, node) {
1146
+ let token = sourceCode.getFirstToken(node);
1147
+ while (token && token.value !== "(") {
1148
+ token = sourceCode.getTokenAfter(token);
1149
+ }
1150
+ return token;
1151
+ }
1152
+ function addParam(sourceCode, fixer, node, refs) {
1153
+ const lastParam = node.params.at(-1);
1154
+ if (lastParam) {
1155
+ return fixer.insertTextAfter(lastParam, `, ${refs}`);
1156
+ }
1157
+ const token = getParen(sourceCode, node);
1158
+ return token ? fixer.insertTextAfter(token, `[${refs}]`) : null;
1159
+ }
1160
+ var no_unsafe_references_default;
1161
+ var init_no_unsafe_references = __esm({
1162
+ "src/rules/no-unsafe-references.ts"() {
1163
+ "use strict";
1164
+ init_ast();
1165
+ init_misc();
1166
+ no_unsafe_references_default = {
1167
+ create(context) {
1168
+ return {
1169
+ CallExpression(node) {
1170
+ if (!isPageMethod(node, "evaluate"))
1171
+ return;
1172
+ const [fn] = node.arguments;
1173
+ if (!fn || !isFunction(fn))
1174
+ return;
1175
+ const sourceCode = getSourceCode(context);
1176
+ const { through, upper } = sourceCode.getScope(fn.body);
1177
+ const allRefs = new Set(collectVariables(upper));
1178
+ through.filter((ref) => allRefs.has(ref.identifier.name)).forEach((ref, i, arr) => {
1179
+ const descriptor = {
1180
+ data: { variable: ref.identifier.name },
1181
+ messageId: "noUnsafeReference",
1182
+ node: ref.identifier
1183
+ };
1184
+ if (i !== 0) {
1185
+ context.report(descriptor);
1186
+ return;
1187
+ }
1188
+ context.report({
1189
+ ...descriptor,
1190
+ fix(fixer) {
1191
+ const refs = arr.map((ref2) => ref2.identifier.name).join(", ");
1192
+ return [
1193
+ addArgument(fixer, node, refs),
1194
+ addParam(sourceCode, fixer, fn, refs)
1195
+ ].filter(truthy);
1196
+ }
1197
+ });
1198
+ });
1199
+ }
1200
+ };
1201
+ },
1202
+ meta: {
1203
+ docs: {
1204
+ category: "Possible Errors",
1205
+ description: "Prevent unsafe variable references in page.evaluate()",
1206
+ recommended: true,
1207
+ url: "https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md"
1208
+ },
1209
+ fixable: "code",
1210
+ messages: {
1211
+ noUnsafeReference: 'Unsafe reference to variable "{{ variable }}" in page.evaluate()'
1212
+ },
1213
+ type: "problem"
1214
+ }
1215
+ };
1216
+ }
1217
+ });
1218
+
1103
1219
  // src/rules/no-useless-await.ts
1104
1220
  function isSupportedMethod(node) {
1105
1221
  if (node.callee.type !== "MemberExpression")
@@ -1617,6 +1733,7 @@ var init_prefer_to_contain = __esm({
1617
1733
  "src/rules/prefer-to-contain.ts"() {
1618
1734
  "use strict";
1619
1735
  init_ast();
1736
+ init_misc();
1620
1737
  init_parseExpectCall();
1621
1738
  matchers = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
1622
1739
  isFixableIncludesCallExpression = (node) => node.type === "CallExpression" && node.callee.type === "MemberExpression" && isPropertyAccessor(node.callee, "includes") && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement";
@@ -1638,7 +1755,7 @@ var init_prefer_to_contain = __esm({
1638
1755
  );
1639
1756
  context.report({
1640
1757
  fix(fixer) {
1641
- const sourceCode = context.sourceCode ?? context.getSourceCode();
1758
+ const sourceCode = getSourceCode(context);
1642
1759
  const addNotModifier = matcherArg.type === "Literal" && matcherArg.value === !!notModifier;
1643
1760
  const fixes = [
1644
1761
  // remove the "includes" call entirely
@@ -1813,11 +1930,25 @@ var init_prefer_to_have_length = __esm({
1813
1930
  });
1814
1931
 
1815
1932
  // src/rules/prefer-web-first-assertions.ts
1933
+ function dereference(context, node) {
1934
+ if (node.type !== "Identifier") {
1935
+ return node;
1936
+ }
1937
+ const sourceCode = getSourceCode(context);
1938
+ const scope = sourceCode.getScope(node);
1939
+ for (const ref of scope.references) {
1940
+ const refParent = ref.identifier.parent;
1941
+ if (refParent.type === "VariableDeclarator") {
1942
+ return refParent.init;
1943
+ }
1944
+ }
1945
+ }
1816
1946
  var methods3, supportedMatchers, prefer_web_first_assertions_default;
1817
1947
  var init_prefer_web_first_assertions = __esm({
1818
1948
  "src/rules/prefer-web-first-assertions.ts"() {
1819
1949
  "use strict";
1820
1950
  init_ast();
1951
+ init_misc();
1821
1952
  init_parseExpectCall();
1822
1953
  methods3 = {
1823
1954
  getAttribute: {
@@ -1867,8 +1998,8 @@ var init_prefer_web_first_assertions = __esm({
1867
1998
  const expectCall = parseExpectCall(context, node);
1868
1999
  if (!expectCall)
1869
2000
  return;
1870
- const [arg] = node.arguments;
1871
- if (arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
2001
+ const arg = dereference(context, node.arguments[0]);
2002
+ if (!arg || arg.type !== "AwaitExpression" || arg.argument.type !== "CallExpression" || arg.argument.callee.type !== "MemberExpression") {
1872
2003
  return;
1873
2004
  }
1874
2005
  if (!supportedMatchers.has(expectCall.matcherName))
@@ -1892,7 +2023,7 @@ var init_prefer_web_first_assertions = __esm({
1892
2023
  },
1893
2024
  fix: (fixer) => {
1894
2025
  const methodArgs = arg.argument.type === "CallExpression" ? arg.argument.arguments : [];
1895
- const methodEnd = methodArgs.length ? methodArgs[methodArgs.length - 1].range[1] + 1 : callee.property.range[1] + 2;
2026
+ const methodEnd = methodArgs.length ? methodArgs.at(-1).range[1] + 1 : callee.property.range[1] + 2;
1896
2027
  const fixes = [
1897
2028
  // Add await to the expect call
1898
2029
  fixer.insertTextBefore(node, "await "),
@@ -1999,18 +2130,6 @@ var init_require_soft_assertions = __esm({
1999
2130
  }
2000
2131
  });
2001
2132
 
2002
- // src/utils/misc.ts
2003
- var getAmountData;
2004
- var init_misc = __esm({
2005
- "src/utils/misc.ts"() {
2006
- "use strict";
2007
- getAmountData = (amount) => ({
2008
- amount: amount.toString(),
2009
- s: amount === 1 ? "" : "s"
2010
- });
2011
- }
2012
- });
2013
-
2014
2133
  // src/rules/require-top-level-describe.ts
2015
2134
  var require_top_level_describe_default;
2016
2135
  var init_require_top_level_describe = __esm({
@@ -2418,6 +2537,7 @@ var require_src = __commonJS({
2418
2537
  init_no_raw_locators();
2419
2538
  init_no_restricted_matchers();
2420
2539
  init_no_skipped_test();
2540
+ init_no_unsafe_references();
2421
2541
  init_no_useless_await();
2422
2542
  init_no_useless_not();
2423
2543
  init_no_wait_for_selector();
@@ -2452,6 +2572,7 @@ var require_src = __commonJS({
2452
2572
  "no-raw-locators": no_raw_locators_default,
2453
2573
  "no-restricted-matchers": no_restricted_matchers_default,
2454
2574
  "no-skipped-test": no_skipped_test_default,
2575
+ "no-unsafe-references": no_unsafe_references_default,
2455
2576
  "no-useless-await": no_useless_await_default,
2456
2577
  "no-useless-not": no_useless_not_default,
2457
2578
  "no-wait-for-selector": no_wait_for_selector_default,
@@ -2484,6 +2605,7 @@ var require_src = __commonJS({
2484
2605
  "playwright/no-networkidle": "error",
2485
2606
  "playwright/no-page-pause": "warn",
2486
2607
  "playwright/no-skipped-test": "warn",
2608
+ "playwright/no-unsafe-references": "error",
2487
2609
  "playwright/no-useless-await": "warn",
2488
2610
  "playwright/no-useless-not": "warn",
2489
2611
  "playwright/no-wait-for-selector": "warn",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-playwright",
3
3
  "description": "ESLint plugin for Playwright testing.",
4
- "version": "1.0.1",
4
+ "version": "1.1.0",
5
5
  "repository": "https://github.com/playwright-community/eslint-plugin-playwright",
6
6
  "author": "Mark Skelton <mark@mskelton.dev>",
7
7
  "packageManager": "pnpm@8.12.0",
@@ -12,6 +12,9 @@
12
12
  "workspaces": [
13
13
  "examples"
14
14
  ],
15
+ "engines": {
16
+ "node": ">=16.6.0"
17
+ },
15
18
  "types": "./dist/index.d.ts",
16
19
  "exports": {
17
20
  "import": {
@@ -37,6 +40,7 @@
37
40
  "devDependencies": {
38
41
  "@jest/globals": "^29.7.0",
39
42
  "@mskelton/eslint-config": "^8.4.0",
43
+ "@mskelton/semantic-release-config": "^1.0.1",
40
44
  "@types/eslint": "^8.44.3",
41
45
  "@types/estree": "^1.0.2",
42
46
  "@types/node": "^20.11.17",
@@ -47,7 +51,8 @@
47
51
  "eslint-plugin-sort": "^2.10.0",
48
52
  "jest": "^29.7.0",
49
53
  "prettier": "^3.0.3",
50
- "semantic-release": "^22.0.5",
54
+ "prettier-plugin-jsdoc": "^1.3.0",
55
+ "semantic-release": "^23.0.2",
51
56
  "ts-jest": "^29.1.1",
52
57
  "tsup": "^8.0.1",
53
58
  "typescript": "^5.2.2"