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.
- package/index.js +26 -18
- package/package.json +32 -30
- package/readme.md +183 -147
- package/rules/ast/call-or-new-expression.js +3 -5
- package/rules/ast/literal.js +1 -6
- package/rules/better-dom-traversing.js +291 -0
- package/rules/catch-error-name.js +3 -3
- package/rules/consistent-compound-words.js +452 -0
- package/rules/consistent-destructuring.js +234 -22
- package/rules/consistent-function-scoping.js +35 -11
- package/rules/consistent-json-file-read.js +245 -0
- package/rules/consistent-template-literal-escape.js +52 -0
- package/rules/custom-error-definition.js +176 -71
- package/rules/dom-node-dataset.js +431 -0
- package/rules/escape-case.js +2 -1
- package/rules/expiring-todo-comments.js +56 -102
- package/rules/explicit-length-check.js +145 -33
- package/rules/filename-case.js +97 -13
- package/rules/fix/append-argument.js +1 -1
- package/rules/fix/remove-expression-statement.js +1 -0
- package/rules/import-style.js +30 -10
- package/rules/index.js +37 -3
- package/rules/isolated-functions.js +11 -5
- package/rules/no-accessor-recursion.js +1 -2
- package/rules/no-anonymous-default-export.js +9 -11
- package/rules/no-array-callback-reference.js +148 -22
- package/rules/no-array-fill-with-reference-type.js +127 -0
- package/rules/no-array-for-each.js +18 -3
- package/rules/no-array-from-fill.js +76 -0
- package/rules/no-array-method-this-argument.js +1 -1
- package/rules/no-array-reduce.js +509 -2
- package/rules/no-blob-to-file.js +299 -0
- package/rules/no-canvas-to-image.js +152 -0
- package/rules/no-confusing-array-splice.js +164 -0
- package/rules/no-duplicate-set-values.js +57 -0
- package/rules/no-empty-file.js +31 -2
- package/rules/no-exports-in-scripts.js +34 -0
- package/rules/no-for-loop.js +1 -5
- package/rules/no-hex-escape.js +1 -1
- package/rules/no-incorrect-query-selector.js +347 -0
- package/rules/no-instanceof-builtins.js +4 -6
- package/rules/no-invalid-fetch-options.js +1 -1
- package/rules/no-invalid-file-input-accept.js +475 -0
- package/rules/no-keyword-prefix.js +23 -5
- package/rules/no-late-current-target-access.js +166 -0
- package/rules/no-lonely-if.js +152 -52
- package/rules/no-manually-wrapped-comments.js +151 -0
- package/rules/no-null.js +16 -5
- package/rules/no-process-exit.js +1 -1
- package/rules/no-this-outside-of-class.js +75 -0
- package/rules/no-typeof-undefined.js +7 -4
- package/rules/no-unnecessary-nested-ternary.js +132 -0
- package/rules/no-unnecessary-polyfills.js +253 -24
- package/rules/no-unreadable-iife.js +33 -3
- package/rules/no-unused-array-method-return.js +301 -0
- package/rules/no-unused-properties.js +13 -19
- package/rules/no-useless-iterator-to-array.js +249 -0
- package/rules/no-useless-length-check.js +5 -11
- package/rules/no-useless-promise-resolve-reject.js +5 -5
- package/rules/no-useless-spread.js +53 -4
- package/rules/no-useless-undefined.js +62 -46
- package/rules/numeric-separators-style.js +17 -6
- package/rules/prefer-add-event-listener.js +1 -0
- package/rules/prefer-array-find.js +1 -0
- package/rules/prefer-array-flat.js +76 -8
- package/rules/prefer-array-last-methods.js +93 -0
- package/rules/prefer-array-some.js +74 -8
- package/rules/prefer-at.js +118 -57
- package/rules/prefer-class-fields.js +2 -4
- package/rules/prefer-default-parameters.js +1 -2
- package/rules/prefer-dom-node-append.js +2 -2
- package/rules/prefer-dom-node-remove.js +24 -18
- package/rules/prefer-export-from.js +8 -7
- package/rules/prefer-get-or-insert-computed.js +159 -0
- package/rules/prefer-global-this.js +33 -29
- package/rules/prefer-https.js +83 -0
- package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
- package/rules/prefer-iterator-concat.js +174 -0
- package/rules/prefer-iterator-to-array-at-end.js +123 -0
- package/rules/prefer-keyboard-event-key.js +202 -76
- package/rules/prefer-logical-operator-over-ternary.js +1 -1
- package/rules/prefer-math-abs.js +397 -0
- package/rules/prefer-math-min-max.js +54 -26
- package/rules/prefer-module.js +5 -7
- package/rules/prefer-native-coercion-functions.js +8 -1
- package/rules/prefer-node-protocol.js +7 -4
- package/rules/prefer-number-properties.js +2 -0
- package/rules/prefer-object-from-entries.js +12 -7
- package/rules/prefer-prototype-methods.js +1 -3
- package/rules/prefer-query-selector.js +119 -7
- package/rules/prefer-queue-microtask.js +204 -0
- package/rules/prefer-regexp-test.js +166 -20
- package/rules/prefer-set-has.js +474 -47
- package/rules/prefer-simple-condition-first.js +282 -0
- package/rules/prefer-single-call.js +129 -38
- package/rules/prefer-split-limit.js +107 -0
- package/rules/prefer-spread.js +150 -19
- package/rules/prefer-string-match-all.js +352 -0
- package/rules/prefer-string-pad-start-end.js +380 -0
- package/rules/prefer-string-raw.js +90 -19
- package/rules/prefer-string-repeat.js +173 -0
- package/rules/prefer-string-replace-all.js +180 -12
- package/rules/prefer-string-slice.js +55 -4
- package/rules/prefer-string-starts-ends-with.js +2 -2
- package/rules/prefer-structured-clone.js +1 -0
- package/rules/prefer-switch.js +2 -0
- package/rules/prefer-ternary.js +3 -2
- package/rules/prefer-top-level-await.js +168 -6
- package/rules/prevent-abbreviations.js +144 -158
- package/rules/relative-url-style.js +1 -0
- package/rules/require-css-escape.js +141 -0
- package/rules/require-module-attributes.js +16 -18
- package/rules/require-module-specifiers.js +62 -0
- package/rules/require-passive-events.js +292 -0
- package/rules/rule/to-eslint-rule.js +1 -1
- package/rules/rule/to-eslint-rules.js +4 -6
- package/rules/shared/abbreviations.js +1 -0
- package/rules/shared/dom-events.js +7 -7
- package/rules/shared/identifier-checks.js +205 -0
- package/rules/shared/package-json.js +1 -1
- package/rules/string-content.js +35 -5
- package/rules/switch-case-braces.js +2 -3
- package/rules/switch-case-break-position.js +138 -0
- package/rules/template-indent.js +59 -3
- package/rules/text-encoding-identifier-case.js +23 -5
- package/rules/throw-new-error.js +4 -0
- package/rules/try-complexity.js +184 -0
- package/rules/utils/assert-token.js +3 -5
- package/rules/utils/boolean.js +37 -28
- package/rules/utils/create-deprecated-rules.js +25 -27
- package/rules/utils/get-call-expression-arguments-text.js +3 -5
- package/rules/utils/get-duplicate-array-elements.js +77 -0
- package/rules/utils/index.js +4 -1
- package/rules/utils/is-node-contains-lexical-this.js +65 -0
- package/rules/utils/is-number.js +2 -2
- package/rules/utils/is-same-reference.js +25 -17
- package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
- package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
- package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -5
- package/rules/utils/should-add-parentheses-to-expression-statement-expression.js +1 -1
- package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
- package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -1
- package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
- package/rules/better-regex.js +0 -145
- package/rules/prefer-dom-node-dataset.js +0 -135
- 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;
|
package/rules/escape-case.js
CHANGED
|
@@ -48,7 +48,7 @@ const create = context => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
context.on('TemplateElement', node => {
|
|
51
|
-
if (isTaggedTemplateLiteral(node.parent
|
|
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
|
|