eslint-plugin-unicorn 63.0.0 → 65.0.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.
Files changed (146) hide show
  1. package/index.js +26 -18
  2. package/package.json +32 -30
  3. package/readme.md +183 -147
  4. package/rules/ast/call-or-new-expression.js +3 -5
  5. package/rules/ast/literal.js +1 -6
  6. package/rules/better-dom-traversing.js +291 -0
  7. package/rules/catch-error-name.js +3 -3
  8. package/rules/consistent-compound-words.js +452 -0
  9. package/rules/consistent-destructuring.js +234 -22
  10. package/rules/consistent-function-scoping.js +35 -11
  11. package/rules/consistent-json-file-read.js +245 -0
  12. package/rules/consistent-template-literal-escape.js +52 -0
  13. package/rules/custom-error-definition.js +176 -71
  14. package/rules/dom-node-dataset.js +431 -0
  15. package/rules/escape-case.js +2 -1
  16. package/rules/expiring-todo-comments.js +56 -102
  17. package/rules/explicit-length-check.js +145 -33
  18. package/rules/filename-case.js +97 -13
  19. package/rules/fix/append-argument.js +1 -1
  20. package/rules/fix/remove-expression-statement.js +1 -0
  21. package/rules/import-style.js +30 -10
  22. package/rules/index.js +37 -3
  23. package/rules/isolated-functions.js +11 -5
  24. package/rules/no-accessor-recursion.js +1 -2
  25. package/rules/no-anonymous-default-export.js +9 -11
  26. package/rules/no-array-callback-reference.js +148 -22
  27. package/rules/no-array-fill-with-reference-type.js +127 -0
  28. package/rules/no-array-for-each.js +18 -3
  29. package/rules/no-array-from-fill.js +76 -0
  30. package/rules/no-array-method-this-argument.js +1 -1
  31. package/rules/no-array-reduce.js +509 -2
  32. package/rules/no-blob-to-file.js +299 -0
  33. package/rules/no-canvas-to-image.js +152 -0
  34. package/rules/no-confusing-array-splice.js +164 -0
  35. package/rules/no-duplicate-set-values.js +57 -0
  36. package/rules/no-empty-file.js +31 -2
  37. package/rules/no-exports-in-scripts.js +34 -0
  38. package/rules/no-for-loop.js +1 -5
  39. package/rules/no-hex-escape.js +1 -1
  40. package/rules/no-incorrect-query-selector.js +347 -0
  41. package/rules/no-instanceof-builtins.js +4 -6
  42. package/rules/no-invalid-fetch-options.js +1 -1
  43. package/rules/no-invalid-file-input-accept.js +475 -0
  44. package/rules/no-keyword-prefix.js +23 -5
  45. package/rules/no-late-current-target-access.js +166 -0
  46. package/rules/no-lonely-if.js +152 -52
  47. package/rules/no-manually-wrapped-comments.js +151 -0
  48. package/rules/no-null.js +16 -5
  49. package/rules/no-process-exit.js +1 -1
  50. package/rules/no-this-outside-of-class.js +75 -0
  51. package/rules/no-typeof-undefined.js +7 -4
  52. package/rules/no-unnecessary-nested-ternary.js +132 -0
  53. package/rules/no-unnecessary-polyfills.js +253 -24
  54. package/rules/no-unreadable-iife.js +33 -3
  55. package/rules/no-unused-array-method-return.js +301 -0
  56. package/rules/no-unused-properties.js +13 -19
  57. package/rules/no-useless-iterator-to-array.js +249 -0
  58. package/rules/no-useless-length-check.js +5 -11
  59. package/rules/no-useless-promise-resolve-reject.js +5 -5
  60. package/rules/no-useless-spread.js +53 -4
  61. package/rules/no-useless-undefined.js +62 -46
  62. package/rules/numeric-separators-style.js +17 -6
  63. package/rules/prefer-add-event-listener.js +1 -0
  64. package/rules/prefer-array-find.js +1 -0
  65. package/rules/prefer-array-flat.js +76 -8
  66. package/rules/prefer-array-last-methods.js +93 -0
  67. package/rules/prefer-array-some.js +74 -8
  68. package/rules/prefer-at.js +118 -57
  69. package/rules/prefer-class-fields.js +2 -4
  70. package/rules/prefer-default-parameters.js +1 -2
  71. package/rules/prefer-dom-node-append.js +2 -2
  72. package/rules/prefer-dom-node-remove.js +24 -18
  73. package/rules/prefer-export-from.js +8 -7
  74. package/rules/prefer-get-or-insert-computed.js +159 -0
  75. package/rules/prefer-global-this.js +33 -29
  76. package/rules/prefer-https.js +83 -0
  77. package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
  78. package/rules/prefer-iterator-concat.js +174 -0
  79. package/rules/prefer-iterator-to-array-at-end.js +123 -0
  80. package/rules/prefer-keyboard-event-key.js +202 -76
  81. package/rules/prefer-logical-operator-over-ternary.js +1 -1
  82. package/rules/prefer-math-abs.js +397 -0
  83. package/rules/prefer-math-min-max.js +54 -26
  84. package/rules/prefer-module.js +5 -7
  85. package/rules/prefer-native-coercion-functions.js +8 -1
  86. package/rules/prefer-node-protocol.js +7 -4
  87. package/rules/prefer-number-properties.js +2 -0
  88. package/rules/prefer-object-from-entries.js +12 -7
  89. package/rules/prefer-prototype-methods.js +1 -3
  90. package/rules/prefer-query-selector.js +119 -7
  91. package/rules/prefer-queue-microtask.js +204 -0
  92. package/rules/prefer-regexp-test.js +166 -20
  93. package/rules/prefer-set-has.js +474 -47
  94. package/rules/prefer-simple-condition-first.js +282 -0
  95. package/rules/prefer-single-call.js +129 -38
  96. package/rules/prefer-split-limit.js +107 -0
  97. package/rules/prefer-spread.js +150 -19
  98. package/rules/prefer-string-match-all.js +352 -0
  99. package/rules/prefer-string-pad-start-end.js +380 -0
  100. package/rules/prefer-string-raw.js +90 -19
  101. package/rules/prefer-string-repeat.js +173 -0
  102. package/rules/prefer-string-replace-all.js +180 -12
  103. package/rules/prefer-string-slice.js +55 -4
  104. package/rules/prefer-string-starts-ends-with.js +2 -2
  105. package/rules/prefer-structured-clone.js +1 -0
  106. package/rules/prefer-switch.js +2 -0
  107. package/rules/prefer-ternary.js +3 -2
  108. package/rules/prefer-top-level-await.js +168 -6
  109. package/rules/prevent-abbreviations.js +144 -158
  110. package/rules/relative-url-style.js +1 -0
  111. package/rules/require-css-escape.js +141 -0
  112. package/rules/require-module-attributes.js +16 -18
  113. package/rules/require-module-specifiers.js +62 -0
  114. package/rules/require-passive-events.js +292 -0
  115. package/rules/rule/to-eslint-rule.js +1 -1
  116. package/rules/rule/to-eslint-rules.js +4 -6
  117. package/rules/shared/abbreviations.js +1 -0
  118. package/rules/shared/dom-events.js +7 -7
  119. package/rules/shared/identifier-checks.js +205 -0
  120. package/rules/shared/package-json.js +1 -1
  121. package/rules/string-content.js +35 -5
  122. package/rules/switch-case-braces.js +2 -3
  123. package/rules/switch-case-break-position.js +138 -0
  124. package/rules/template-indent.js +59 -3
  125. package/rules/text-encoding-identifier-case.js +23 -5
  126. package/rules/throw-new-error.js +4 -0
  127. package/rules/try-complexity.js +184 -0
  128. package/rules/utils/assert-token.js +3 -5
  129. package/rules/utils/boolean.js +37 -28
  130. package/rules/utils/create-deprecated-rules.js +25 -27
  131. package/rules/utils/get-call-expression-arguments-text.js +3 -5
  132. package/rules/utils/get-duplicate-array-elements.js +77 -0
  133. package/rules/utils/index.js +4 -1
  134. package/rules/utils/is-node-contains-lexical-this.js +65 -0
  135. package/rules/utils/is-number.js +2 -2
  136. package/rules/utils/is-same-reference.js +25 -17
  137. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
  138. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
  139. package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -5
  140. package/rules/utils/should-add-parentheses-to-expression-statement-expression.js +1 -1
  141. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
  142. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -1
  143. package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
  144. package/rules/better-regex.js +0 -145
  145. package/rules/prefer-dom-node-dataset.js +0 -135
  146. package/rules/prefer-json-parse-buffer.js +0 -161
@@ -0,0 +1,431 @@
1
+ import helperValidatorIdentifier from '@babel/helper-validator-identifier';
2
+ import {
3
+ escapeString,
4
+ getIndentString,
5
+ getParenthesizedText,
6
+ hasOptionalChainElement,
7
+ isParenthesized,
8
+ isValueNotUsable,
9
+ needsSemicolon,
10
+ shouldAddParenthesesToMemberExpressionObject,
11
+ } from './utils/index.js';
12
+ import {
13
+ isMemberExpression, isMethodCall, isStringLiteral, isExpressionStatement,
14
+ } from './ast/index.js';
15
+
16
+ const {isIdentifierName} = helperValidatorIdentifier;
17
+ const MESSAGE_ID = 'prefer-dataset';
18
+ const INVERSE_MESSAGE_ID = 'prefer-attributes';
19
+ const messages = {
20
+ [MESSAGE_ID]: 'Prefer `.dataset` over `{{method}}(…)`.',
21
+ [INVERSE_MESSAGE_ID]: 'Prefer `.{{method}}(…)` over `.dataset`.',
22
+ };
23
+
24
+ const dashToCamelCase = string => string.replaceAll(/-[a-z]/g, s => s[1].toUpperCase());
25
+ const camelCaseToDash = string => `data-${string.replaceAll(/[A-Z]/g, char => `-${char.toLowerCase()}`)}`;
26
+ const isDatasetAccess = node => isMemberExpression(node, {property: 'dataset'});
27
+
28
+ // Names inherited from `Object.prototype` — never a real `data-*` attribute.
29
+ const DATASET_INHERITED_MEMBERS = new Set([
30
+ 'constructor',
31
+ 'hasOwnProperty',
32
+ 'isPrototypeOf',
33
+ 'propertyIsEnumerable',
34
+ 'toLocaleString',
35
+ 'toString',
36
+ 'valueOf',
37
+ '__proto__',
38
+ '__defineGetter__',
39
+ '__defineSetter__',
40
+ '__lookupGetter__',
41
+ '__lookupSetter__',
42
+ ]);
43
+
44
+ // HTML attribute names cannot contain whitespace, ", ', <, >, /, =, or control chars.
45
+ const INVALID_ATTRIBUTE_NAME_CHARS = /[\s\u0000-\u001F"'/<=>]/; // eslint-disable-line no-control-regex
46
+
47
+ // A dataset key only safely maps to a `data-*` attribute when it is not
48
+ // inherited from `Object.prototype` (the inverse `dataset.toString` is the
49
+ // inherited method, not an attribute), doesn't contain a dash (`dataset.fooBar`
50
+ // and `dataset['foo-bar']` collapse to the same `data-foo-bar`), and doesn't
51
+ // include chars forbidden in HTML attribute names (`setAttribute` would throw).
52
+ const isUnsafeDatasetKey = key =>
53
+ DATASET_INHERITED_MEMBERS.has(key)
54
+ || key.includes('-')
55
+ || INVALID_ATTRIBUTE_NAME_CHARS.test(key);
56
+
57
+ // Get text for a node that becomes the receiver of a method call, preserving
58
+ // any required parentheses so e.g. `(a + b).dataset.foo` rewrites to
59
+ // `(a + b).getAttribute('data-foo')` rather than `a + b.getAttribute(...)`.
60
+ function getReceiverText(node, context) {
61
+ const text = getParenthesizedText(node, context);
62
+ if (
63
+ !isParenthesized(node, context.sourceCode)
64
+ && shouldAddParenthesesToMemberExpressionObject(node, context)
65
+ ) {
66
+ return `(${text})`;
67
+ }
68
+
69
+ return text;
70
+ }
71
+
72
+ function getFix(callExpression, context) {
73
+ const method = callExpression.callee.property.name;
74
+
75
+ // `foo?.bar = ''` is invalid
76
+ // TODO: Remove this restriction if https://github.com/nicolo-ribaudo/ecma262/pull/4 get merged
77
+ if (method === 'setAttribute' && hasOptionalChainElement(callExpression.callee)) {
78
+ return;
79
+ }
80
+
81
+ // `element.setAttribute(…)` returns `undefined`, but `AssignmentExpression` returns value of RHS
82
+ if (method === 'setAttribute' && !isValueNotUsable(callExpression)) {
83
+ return;
84
+ }
85
+
86
+ if (method === 'removeAttribute' && !isExpressionStatement(callExpression.parent)) {
87
+ return;
88
+ }
89
+
90
+ return fixer => {
91
+ const [nameNode] = callExpression.arguments;
92
+ const name = dashToCamelCase(nameNode.value.toLowerCase().slice(5));
93
+ let text = '';
94
+ const datasetText = `${getReceiverText(callExpression.callee.object, context)}${callExpression.callee.optional ? '?' : ''}.dataset`;
95
+ switch (method) {
96
+ case 'setAttribute':
97
+ case 'getAttribute':
98
+ case 'removeAttribute': {
99
+ text = isIdentifierName(name) ? `.${name}` : `[${escapeString(name, nameNode.raw.charAt(0))}]`;
100
+ text = `${datasetText}${text}`;
101
+ if (method === 'setAttribute') {
102
+ text += ` = ${getParenthesizedText(callExpression.arguments[1], context)}`;
103
+ } else if (method === 'removeAttribute') {
104
+ text = `delete ${text}`;
105
+ }
106
+
107
+ /*
108
+ For non-exists attribute, `element.getAttribute('data-foo')` returns `null`,
109
+ but `element.dataset.foo` returns `undefined`, switch to suggestions if necessary
110
+ */
111
+ break;
112
+ }
113
+
114
+ case 'hasAttribute': {
115
+ text = `Object.hasOwn(${datasetText}, ${escapeString(name, nameNode.raw.charAt(0))})`;
116
+ break;
117
+ }
118
+ // No default
119
+ }
120
+
121
+ return fixer.replaceText(callExpression, text);
122
+ };
123
+ }
124
+
125
+ const schema = [
126
+ {
127
+ type: 'object',
128
+ additionalProperties: false,
129
+ properties: {
130
+ preferAttributes: {
131
+ type: 'boolean',
132
+ description: 'Prefer attribute methods over named `.dataset` access.',
133
+ },
134
+ },
135
+ },
136
+ ];
137
+
138
+ /** @param {import('eslint').Rule.RuleContext} context */
139
+ const create = context => {
140
+ const {preferAttributes} = context.options[0];
141
+
142
+ if (preferAttributes) {
143
+ const {sourceCode} = context;
144
+
145
+ const getHasAttributeReport = (reportNode, keyNode, datasetNode) => {
146
+ if (isUnsafeDatasetKey(keyNode.value)) {
147
+ return;
148
+ }
149
+
150
+ const objectText = getReceiverText(datasetNode.object, context);
151
+ const chain = datasetNode.optional ? '?.' : '.';
152
+ const attributeName = escapeString(camelCaseToDash(keyNode.value), keyNode.raw.charAt(0));
153
+
154
+ return {
155
+ node: reportNode,
156
+ messageId: INVERSE_MESSAGE_ID,
157
+ data: {method: 'hasAttribute'},
158
+ fix(fixer) {
159
+ let text = `${objectText}${chain}hasAttribute(${attributeName})`;
160
+ if (needsSemicolon(sourceCode.getTokenBefore(reportNode), context, text)) {
161
+ text = `;${text}`;
162
+ }
163
+
164
+ return fixer.replaceText(reportNode, text);
165
+ },
166
+ };
167
+ };
168
+
169
+ context.on('BinaryExpression', binaryExpression => {
170
+ if (!(
171
+ binaryExpression.operator === 'in'
172
+ && isStringLiteral(binaryExpression.left)
173
+ && isDatasetAccess(binaryExpression.right)
174
+ )) {
175
+ return;
176
+ }
177
+
178
+ return getHasAttributeReport(binaryExpression, binaryExpression.left, binaryExpression.right);
179
+ });
180
+
181
+ context.on('CallExpression', callExpression => {
182
+ if (
183
+ isMethodCall(callExpression, {
184
+ object: 'Object',
185
+ method: 'hasOwn',
186
+ argumentsLength: 2,
187
+ optionalCall: false,
188
+ optionalMember: false,
189
+ })
190
+ && isDatasetAccess(callExpression.arguments[0])
191
+ && isStringLiteral(callExpression.arguments[1])
192
+ ) {
193
+ return getHasAttributeReport(callExpression, callExpression.arguments[1], callExpression.arguments[0]);
194
+ }
195
+
196
+ if (
197
+ isMethodCall(callExpression, {
198
+ method: 'hasOwnProperty',
199
+ argumentsLength: 1,
200
+ optionalCall: false,
201
+ optionalMember: false,
202
+ })
203
+ && isDatasetAccess(callExpression.callee.object)
204
+ && isStringLiteral(callExpression.arguments[0])
205
+ ) {
206
+ return getHasAttributeReport(callExpression, callExpression.arguments[0], callExpression.callee.object);
207
+ }
208
+ });
209
+
210
+ context.on('VariableDeclarator', declarator => {
211
+ if (!(
212
+ declarator.init
213
+ && isDatasetAccess(declarator.init)
214
+ && declarator.id.type === 'ObjectPattern'
215
+ )) {
216
+ return;
217
+ }
218
+
219
+ const {properties} = declarator.id;
220
+
221
+ // Skip the whole pattern if any destructured key can't safely map to a
222
+ // `data-*` attribute, or is a runtime computed key we can't analyze
223
+ const hasUnsafeKey = properties.some(property => {
224
+ if (property.type !== 'Property') {
225
+ return false;
226
+ }
227
+
228
+ if (!property.computed && property.key.type === 'Identifier') {
229
+ return isUnsafeDatasetKey(property.key.name);
230
+ }
231
+
232
+ if (isStringLiteral(property.key)) {
233
+ return isUnsafeDatasetKey(property.key.value);
234
+ }
235
+
236
+ return true;
237
+ });
238
+ if (hasUnsafeKey) {
239
+ return;
240
+ }
241
+
242
+ const datasetNode = declarator.init;
243
+ const objectText = getReceiverText(datasetNode.object, context);
244
+ const chain = datasetNode.optional ? '?.' : '.';
245
+
246
+ // Only autofix when all properties are simple (no defaults, rest, computed)
247
+ // and object is a plain identifier (safe to repeat for multi-property)
248
+ const declaration = declarator.parent;
249
+ let fix;
250
+ if (
251
+ properties.length > 0
252
+ && declaration.declarations.length === 1
253
+ && !declaration.parent.type.startsWith('For')
254
+ && declaration.parent.type !== 'ExportNamedDeclaration'
255
+ && properties.every(property =>
256
+ property.type === 'Property'
257
+ && !property.computed
258
+ && property.key.type === 'Identifier'
259
+ && property.value.type === 'Identifier',
260
+ )
261
+ && (properties.length === 1 || datasetNode.object.type === 'Identifier')
262
+ ) {
263
+ const indent = getIndentString(declaration, context);
264
+ const declarations = properties.map(property => {
265
+ const attributeName = escapeString(camelCaseToDash(property.key.name), '\'');
266
+ return `${declaration.kind} ${property.value.name} = ${objectText}${chain}getAttribute(${attributeName})`;
267
+ });
268
+
269
+ fix = fixer => fixer.replaceText(
270
+ declaration,
271
+ `${declarations.join(`;\n${indent}`)};`,
272
+ );
273
+ }
274
+
275
+ return {
276
+ node: datasetNode,
277
+ messageId: INVERSE_MESSAGE_ID,
278
+ data: {method: 'getAttribute'},
279
+ fix,
280
+ };
281
+ });
282
+
283
+ context.on('MemberExpression', memberExpression => {
284
+ const {object} = memberExpression;
285
+ if (!isDatasetAccess(object)) {
286
+ return;
287
+ }
288
+
289
+ const keyName = memberExpression.computed
290
+ ? (isStringLiteral(memberExpression.property) ? memberExpression.property.value : undefined)
291
+ : memberExpression.property.name;
292
+ if (keyName === undefined || isUnsafeDatasetKey(keyName)) {
293
+ return;
294
+ }
295
+
296
+ // `element.dataset?.foo` short-circuits if `dataset` is nullish; the rewrite
297
+ // would lose that — skip (the outer `element?.dataset.foo` form is fine
298
+ // because the optional is on the dataset access, captured via `object.optional`)
299
+ if (memberExpression.optional) {
300
+ return;
301
+ }
302
+
303
+ // `ChainExpression` wraps the outermost optional-chain expression, so for
304
+ // `delete element?.dataset.foo` the surrounding `delete` sits on the chain's
305
+ // parent — unwrap to see the real operator context
306
+ const parent = memberExpression.parent.type === 'ChainExpression'
307
+ ? memberExpression.parent.parent
308
+ : memberExpression.parent;
309
+
310
+ // Method calls and tagged templates on dataset are not attribute reads — skip
311
+ if (
312
+ (parent.type === 'CallExpression' && parent.callee === memberExpression)
313
+ || (parent.type === 'TaggedTemplateExpression' && parent.tag === memberExpression)
314
+ ) {
315
+ return;
316
+ }
317
+
318
+ if (
319
+ parent.type === 'UpdateExpression'
320
+ || (parent.type === 'AssignmentExpression' && parent.left === memberExpression && parent.operator !== '=')
321
+ ) {
322
+ return;
323
+ }
324
+
325
+ const isWrite = parent.type === 'AssignmentExpression' && parent.left === memberExpression;
326
+ const isDelete = parent.type === 'UnaryExpression' && parent.operator === 'delete';
327
+ const method = isWrite ? 'setAttribute' : (isDelete ? 'removeAttribute' : 'getAttribute');
328
+
329
+ const objectText = getReceiverText(object.object, context);
330
+ const chain = object.optional ? '?.' : '.';
331
+ const quote = memberExpression.computed ? memberExpression.property.raw.charAt(0) : undefined;
332
+ const attributeName = escapeString(camelCaseToDash(keyName), quote);
333
+
334
+ let fix;
335
+ if (isWrite && isValueNotUsable(parent)) {
336
+ fix = fixer => fixer.replaceText(parent, `${objectText}${chain}setAttribute(${attributeName}, ${getParenthesizedText(parent.right, context)})`);
337
+ } else if (isDelete && isExpressionStatement(parent.parent)) {
338
+ fix = fixer => {
339
+ let text = `${objectText}${chain}removeAttribute(${attributeName})`;
340
+ if (needsSemicolon(sourceCode.getTokenBefore(parent), context, text)) {
341
+ text = `;${text}`;
342
+ }
343
+
344
+ return fixer.replaceText(parent, text);
345
+ };
346
+ } else if (!isWrite && !isDelete) {
347
+ fix = fixer => fixer.replaceText(memberExpression, `${objectText}${chain}getAttribute(${attributeName})`);
348
+ }
349
+
350
+ return {
351
+ node: memberExpression,
352
+ messageId: INVERSE_MESSAGE_ID,
353
+ data: {method},
354
+ fix,
355
+ };
356
+ });
357
+
358
+ return;
359
+ }
360
+
361
+ context.on('CallExpression', callExpression => {
362
+ if (!(
363
+ (
364
+ isMethodCall(callExpression, {
365
+ method: 'setAttribute',
366
+ argumentsLength: 2,
367
+ optionalCall: false,
368
+ optionalMember: false,
369
+ })
370
+ || isMethodCall(callExpression, {
371
+ methods: ['removeAttribute', 'hasAttribute'],
372
+ argumentsLength: 1,
373
+ optionalCall: false,
374
+ optionalMember: false,
375
+ })
376
+ || isMethodCall(callExpression, {
377
+ method: 'getAttribute',
378
+ argumentsLength: 1,
379
+ optionalCall: false,
380
+ })
381
+ )
382
+ && isStringLiteral(callExpression.arguments[0])
383
+ )) {
384
+ return;
385
+ }
386
+
387
+ const method = callExpression.callee.property.name;
388
+ // Playwright's `Locator#getAttribute()` returns a promise.
389
+ // https://playwright.dev/docs/api/class-locator#locator-get-attribute
390
+ // `ChainExpression` wraps `await locator?.getAttribute(…)`, so unwrap it
391
+ const outerNode = callExpression.parent.type === 'ChainExpression' ? callExpression.parent : callExpression;
392
+ if (
393
+ outerNode.parent.type === 'AwaitExpression'
394
+ && outerNode.parent.argument === outerNode
395
+ && method === 'getAttribute'
396
+ ) {
397
+ return;
398
+ }
399
+
400
+ const attributeName = callExpression.arguments[0].value.toLowerCase();
401
+
402
+ if (!attributeName.startsWith('data-')) {
403
+ return;
404
+ }
405
+
406
+ return {
407
+ node: callExpression,
408
+ messageId: MESSAGE_ID,
409
+ data: {method},
410
+ fix: getFix(callExpression, context),
411
+ };
412
+ });
413
+ };
414
+
415
+ /** @type {import('eslint').Rule.RuleModule} */
416
+ const config = {
417
+ create,
418
+ meta: {
419
+ type: 'suggestion',
420
+ docs: {
421
+ description: 'Enforce consistent style for DOM element dataset access.',
422
+ recommended: 'unopinionated',
423
+ },
424
+ fixable: 'code',
425
+ schema,
426
+ defaultOptions: [{preferAttributes: false}],
427
+ messages,
428
+ },
429
+ };
430
+
431
+ export default config;
@@ -48,7 +48,7 @@ const create = context => {
48
48
  });
49
49
 
50
50
  context.on('TemplateElement', node => {
51
- if (isTaggedTemplateLiteral(node.parent, ['String.raw'])) {
51
+ if (isTaggedTemplateLiteral(node.parent)) {
52
52
  return;
53
53
  }
54
54
 
@@ -64,6 +64,7 @@ const create = context => {
64
64
  const schema = [
65
65
  {
66
66
  enum: ['uppercase', 'lowercase'],
67
+ description: 'The case style for escape sequences.',
67
68
  },
68
69
  ];
69
70