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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
1
2
|
import {getAvailableVariableName, isLeftHandSide} from './utils/index.js';
|
|
2
|
-
import {isCallOrNewExpression} from './ast/index.js';
|
|
3
|
+
import {isCallOrNewExpression, isStringLiteral} from './ast/index.js';
|
|
3
4
|
|
|
4
5
|
const MESSAGE_ID = 'consistentDestructuring';
|
|
5
6
|
const MESSAGE_ID_SUGGEST = 'consistentDestructuringSuggest';
|
|
@@ -33,6 +34,200 @@ const isChildInParentScope = (child, parent) => {
|
|
|
33
34
|
return false;
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
const getRootIdentifier = expression => {
|
|
38
|
+
while (expression?.type === 'MemberExpression') {
|
|
39
|
+
expression = expression.object;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return expression?.type === 'Identifier' ? expression : undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const hasRestElement = pattern => {
|
|
46
|
+
switch (pattern?.type) {
|
|
47
|
+
case 'RestElement': {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
case 'AssignmentPattern': {
|
|
52
|
+
return hasRestElement(pattern.left);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'ObjectPattern': {
|
|
56
|
+
return pattern.properties.some(property =>
|
|
57
|
+
hasRestElement(property.type === 'Property' ? property.value : property));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case 'ArrayPattern': {
|
|
61
|
+
return pattern.elements.some(element =>
|
|
62
|
+
hasRestElement(element));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
default: {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isIdentifierProperty = property =>
|
|
72
|
+
property.type === 'Property'
|
|
73
|
+
&& property.key.type === 'Identifier';
|
|
74
|
+
|
|
75
|
+
const hasNestedRestElement = pattern => {
|
|
76
|
+
switch (pattern?.type) {
|
|
77
|
+
case 'AssignmentPattern': {
|
|
78
|
+
return hasNestedRestElement(pattern.left);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'ObjectPattern': {
|
|
82
|
+
return pattern.properties.some(property => {
|
|
83
|
+
if (property.type === 'RestElement') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return hasRestElement(property.value);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'ArrayPattern': {
|
|
92
|
+
return pattern.elements.some(element => {
|
|
93
|
+
if (!element || element.type === 'RestElement') {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return hasRestElement(element);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
default: {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isMemberDestructuredInNestedPatternWithRest = (objectPattern, memberName) =>
|
|
108
|
+
objectPattern.properties.some(property =>
|
|
109
|
+
isIdentifierProperty(property)
|
|
110
|
+
&& property.key.name === memberName
|
|
111
|
+
&& property.value.type !== 'Identifier'
|
|
112
|
+
&& hasNestedRestElement(property.value));
|
|
113
|
+
|
|
114
|
+
const shouldIgnoreMemberExpression = node =>
|
|
115
|
+
node.computed
|
|
116
|
+
|| (
|
|
117
|
+
isCallOrNewExpression(node.parent)
|
|
118
|
+
&& node.parent.callee === node
|
|
119
|
+
)
|
|
120
|
+
|| isLeftHandSide(node);
|
|
121
|
+
|
|
122
|
+
const isRootVariableReassigned = (declaration, memberExpressionNode, memberScope, sourceCode) => {
|
|
123
|
+
if (!declaration.rootVariable) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [, declarationEnd] = sourceCode.getRange(declaration.object);
|
|
128
|
+
const [memberStart] = sourceCode.getRange(memberExpressionNode);
|
|
129
|
+
|
|
130
|
+
return declaration.rootVariable.references.some(reference => {
|
|
131
|
+
if (!reference.isWrite()) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const [referenceStart] = sourceCode.getRange(reference.identifier);
|
|
136
|
+
if (referenceStart < declarationEnd) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Be conservative: writes from other variable scopes may run before this read via calls/closures.
|
|
141
|
+
if (reference.from.variableScope !== memberScope.variableScope) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return referenceStart <= memberStart;
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isMatchingInExpression = (node, memberExpression, sourceCode) => {
|
|
150
|
+
if (!(
|
|
151
|
+
node.type === 'BinaryExpression'
|
|
152
|
+
&& node.operator === 'in'
|
|
153
|
+
&& isStringLiteral(node.left)
|
|
154
|
+
&& memberExpression.property.type === 'Identifier'
|
|
155
|
+
&& node.left.value === memberExpression.property.name
|
|
156
|
+
)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return sourceCode.getText(node.right) === sourceCode.getText(memberExpression.object);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const hasMatchingInExpression = (node, memberExpression, sourceCode) => {
|
|
164
|
+
if (isMatchingInExpression(node, memberExpression, sourceCode)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
node.type !== 'LogicalExpression'
|
|
170
|
+
|| node.operator !== '&&'
|
|
171
|
+
) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return hasMatchingInExpression(node.left, memberExpression, sourceCode)
|
|
176
|
+
|| hasMatchingInExpression(node.right, memberExpression, sourceCode);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isInPositiveGuardBranch = (parent, child, memberExpression, sourceCode) => {
|
|
180
|
+
switch (parent.type) {
|
|
181
|
+
case 'ConditionalExpression': {
|
|
182
|
+
return parent.consequent === child
|
|
183
|
+
&& hasMatchingInExpression(parent.test, memberExpression, sourceCode);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'LogicalExpression': {
|
|
187
|
+
return parent.operator === '&&'
|
|
188
|
+
&& parent.right === child
|
|
189
|
+
&& hasMatchingInExpression(parent.left, memberExpression, sourceCode);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'IfStatement': {
|
|
193
|
+
return parent.consequent === child
|
|
194
|
+
&& hasMatchingInExpression(parent.test, memberExpression, sourceCode);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
default: {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const isInTypeGuardBoundary = node =>
|
|
204
|
+
[
|
|
205
|
+
'AccessorProperty',
|
|
206
|
+
'FunctionDeclaration',
|
|
207
|
+
'MethodDefinition',
|
|
208
|
+
'PropertyDefinition',
|
|
209
|
+
].includes(node.type);
|
|
210
|
+
|
|
211
|
+
const isInTypeGuardedBranch = (node, sourceCode) => {
|
|
212
|
+
let child = node;
|
|
213
|
+
let {parent} = node;
|
|
214
|
+
|
|
215
|
+
while (parent) {
|
|
216
|
+
if (isInTypeGuardBoundary(parent)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isInPositiveGuardBranch(parent, child, node, sourceCode)) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
child = parent;
|
|
225
|
+
parent = parent.parent;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return false;
|
|
229
|
+
};
|
|
230
|
+
|
|
36
231
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
37
232
|
const create = context => {
|
|
38
233
|
const {sourceCode} = context;
|
|
@@ -49,22 +244,18 @@ const create = context => {
|
|
|
49
244
|
return;
|
|
50
245
|
}
|
|
51
246
|
|
|
247
|
+
const rootIdentifier = getRootIdentifier(node.init);
|
|
52
248
|
declarations.set(sourceCode.getText(node.init), {
|
|
53
249
|
scope: sourceCode.getScope(node),
|
|
54
|
-
|
|
250
|
+
object: node.init,
|
|
251
|
+
rootIdentifierName: rootIdentifier?.name,
|
|
252
|
+
rootVariable: rootIdentifier && findVariable(sourceCode.getScope(node), rootIdentifier),
|
|
55
253
|
objectPattern: node.id,
|
|
56
254
|
});
|
|
57
255
|
});
|
|
58
256
|
|
|
59
257
|
context.on('MemberExpression', node => {
|
|
60
|
-
if (
|
|
61
|
-
node.computed
|
|
62
|
-
|| (
|
|
63
|
-
isCallOrNewExpression(node.parent)
|
|
64
|
-
&& node.parent.callee === node
|
|
65
|
-
)
|
|
66
|
-
|| isLeftHandSide(node)
|
|
67
|
-
) {
|
|
258
|
+
if (shouldIgnoreMemberExpression(node)) {
|
|
68
259
|
return;
|
|
69
260
|
}
|
|
70
261
|
|
|
@@ -74,32 +265,53 @@ const create = context => {
|
|
|
74
265
|
return;
|
|
75
266
|
}
|
|
76
267
|
|
|
77
|
-
const {scope, objectPattern} = declaration;
|
|
78
268
|
const memberScope = sourceCode.getScope(node);
|
|
269
|
+
const memberRootIdentifier = getRootIdentifier(node.object);
|
|
270
|
+
const memberRootVariable = memberRootIdentifier && findVariable(memberScope, memberRootIdentifier);
|
|
271
|
+
if (
|
|
272
|
+
declaration.rootIdentifierName
|
|
273
|
+
&& memberRootIdentifier?.name === declaration.rootIdentifierName
|
|
274
|
+
&& memberRootVariable !== declaration.rootVariable
|
|
275
|
+
) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isRootVariableReassigned(declaration, node, memberScope, sourceCode)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const {scope, objectPattern} = declaration;
|
|
79
284
|
|
|
80
285
|
// Property is destructured outside the current scope
|
|
81
286
|
if (!isChildInParentScope(memberScope, scope)) {
|
|
82
287
|
return;
|
|
83
288
|
}
|
|
84
289
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const lastProperty = objectPattern.properties.at(-1);
|
|
290
|
+
const member = sourceCode.getText(node.property);
|
|
291
|
+
|
|
292
|
+
if (isInTypeGuardedBranch(node, sourceCode)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
91
295
|
|
|
92
|
-
const
|
|
296
|
+
const memberDestructuredInNestedPattern = isMemberDestructuredInNestedPatternWithRest(objectPattern, member);
|
|
93
297
|
|
|
298
|
+
const destructuredProperties = objectPattern.properties.filter(property =>
|
|
299
|
+
isIdentifierProperty(property)
|
|
300
|
+
&& property.value.type === 'Identifier');
|
|
301
|
+
|
|
302
|
+
const lastProperty = objectPattern.properties.at(-1);
|
|
303
|
+
const hasRest = lastProperty?.type === 'RestElement';
|
|
94
304
|
const expression = sourceCode.getText(node);
|
|
95
|
-
const member = sourceCode.getText(node.property);
|
|
96
305
|
|
|
97
306
|
// Member might already be destructured
|
|
98
|
-
const destructuredMember =
|
|
99
|
-
property.key.name === member
|
|
100
|
-
);
|
|
307
|
+
const destructuredMember = destructuredProperties.find(property =>
|
|
308
|
+
property.key.name === member);
|
|
101
309
|
|
|
102
310
|
if (!destructuredMember) {
|
|
311
|
+
if (memberDestructuredInNestedPattern) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
103
315
|
// Don't destructure additional members when rest is used
|
|
104
316
|
if (hasRest) {
|
|
105
317
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {getFunctionHeadLocation, getFunctionNameWithKind} from '@eslint-community/eslint-utils';
|
|
2
|
-
import {getReferences, isNodeMatches} from './utils/index.js';
|
|
2
|
+
import {getReferences, isNodeContainsLexicalThis, isNodeMatches} from './utils/index.js';
|
|
3
3
|
import {functionTypes} from './ast/index.js';
|
|
4
4
|
|
|
5
5
|
const MESSAGE_ID = 'consistent-function-scoping';
|
|
@@ -61,7 +61,7 @@ function checkReferences(scope, parent, scopeManager) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Look at the scope above the function definition to see if lives
|
|
64
|
+
// Look at the scope above the function definition to see if it lives
|
|
65
65
|
// next to the reference being checked
|
|
66
66
|
return isSameScope(parent, identifierParentScope.upper);
|
|
67
67
|
});
|
|
@@ -72,8 +72,7 @@ function checkReferences(scope, parent, scopeManager) {
|
|
|
72
72
|
.some(variable =>
|
|
73
73
|
hitReference(variable.references)
|
|
74
74
|
|| hitDefinitions(variable.defs)
|
|
75
|
-
|| hitIdentifier(variable.identifiers)
|
|
76
|
-
);
|
|
75
|
+
|| hitIdentifier(variable.identifiers));
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
// https://reactjs.org/docs/hooks-reference.html
|
|
@@ -94,10 +93,30 @@ const isReactHook = scope =>
|
|
|
94
93
|
scope.block?.parent?.callee
|
|
95
94
|
&& isNodeMatches(scope.block.parent.callee, reactHooks);
|
|
96
95
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
const isArrowFunctionNodeWithThis = (node, visitorKeys) =>
|
|
97
|
+
node.type === 'ArrowFunctionExpression'
|
|
98
|
+
// We avoid `scope.thisFound` because parser scope metadata differs; AST lexical checks are consistent.
|
|
99
|
+
// Include both params and body, because parameter defaults can reference lexical `this`.
|
|
100
|
+
&& isNodeContainsLexicalThis(node, visitorKeys);
|
|
101
|
+
|
|
102
|
+
function isInsideJestMockFactory(node) {
|
|
103
|
+
let current = node;
|
|
104
|
+
while (current.parent) {
|
|
105
|
+
const {parent} = current;
|
|
106
|
+
if (
|
|
107
|
+
parent.type === 'CallExpression'
|
|
108
|
+
&& parent.arguments[1] === current
|
|
109
|
+
&& functionTypes.includes(current.type)
|
|
110
|
+
&& isNodeMatches(parent.callee, ['jest.mock'])
|
|
111
|
+
) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
current = parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
101
120
|
|
|
102
121
|
const iifeFunctionTypes = new Set([
|
|
103
122
|
'FunctionExpression',
|
|
@@ -145,10 +164,14 @@ function handleNestedArrowFunctions(parentNode, node) {
|
|
|
145
164
|
return parentNode;
|
|
146
165
|
}
|
|
147
166
|
|
|
148
|
-
function checkNode(node, scopeManager) {
|
|
167
|
+
function checkNode(node, scopeManager, sourceCode) {
|
|
149
168
|
const scope = scopeManager.acquire(node);
|
|
150
169
|
|
|
151
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
!scope
|
|
172
|
+
|| isArrowFunctionNodeWithThis(node, sourceCode.visitorKeys)
|
|
173
|
+
|| isInsideJestMockFactory(node)
|
|
174
|
+
) {
|
|
152
175
|
return true;
|
|
153
176
|
}
|
|
154
177
|
|
|
@@ -220,7 +243,7 @@ const create = context => {
|
|
|
220
243
|
return;
|
|
221
244
|
}
|
|
222
245
|
|
|
223
|
-
if (checkNode(node, scopeManager)) {
|
|
246
|
+
if (checkNode(node, scopeManager, sourceCode)) {
|
|
224
247
|
return;
|
|
225
248
|
}
|
|
226
249
|
|
|
@@ -242,6 +265,7 @@ const schema = [
|
|
|
242
265
|
properties: {
|
|
243
266
|
checkArrowFunctions: {
|
|
244
267
|
type: 'boolean',
|
|
268
|
+
description: 'Whether to check arrow functions.',
|
|
245
269
|
},
|
|
246
270
|
},
|
|
247
271
|
},
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {findVariable, getStaticValue, getPropertyName} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {isMethodCall} from './ast/index.js';
|
|
3
|
+
import {removeArgument} from './fix/index.js';
|
|
4
|
+
|
|
5
|
+
const MESSAGE_ID_STRING = 'consistent-json-file-read/string';
|
|
6
|
+
const MESSAGE_ID_BUFFER = 'consistent-json-file-read/buffer';
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID_STRING]: 'Prefer reading the JSON file as a string.',
|
|
9
|
+
[MESSAGE_ID_BUFFER]: 'Prefer reading the JSON file as a buffer.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const getAwaitExpressionArgument = node => {
|
|
13
|
+
while (node.type === 'AwaitExpression') {
|
|
14
|
+
node = node.argument;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return node;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function getIdentifierDeclaration(node, scope) {
|
|
21
|
+
if (!node) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
node = getAwaitExpressionArgument(node);
|
|
26
|
+
|
|
27
|
+
if (!node || node.type !== 'Identifier') {
|
|
28
|
+
return {node, scope};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const variable = findVariable(scope, node);
|
|
32
|
+
if (!variable) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const {identifiers, references} = variable;
|
|
37
|
+
|
|
38
|
+
if (identifiers.length !== 1 || references.length !== 2) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [identifier] = identifiers;
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
identifier.parent.type !== 'VariableDeclarator'
|
|
46
|
+
|| identifier.parent.id !== identifier
|
|
47
|
+
) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return getIdentifierDeclaration(identifier.parent.init, variable.scope);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isUtf8EncodingStringNode = (node, scope) =>
|
|
55
|
+
isUtf8EncodingString(getStaticValue(node, scope)?.value);
|
|
56
|
+
|
|
57
|
+
const isUtf8EncodingString = value => {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
value = value.toLowerCase();
|
|
63
|
+
|
|
64
|
+
// eslint-disable-next-line unicorn/text-encoding-identifier-case
|
|
65
|
+
return value === 'utf8' || value === 'utf-8';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const isSingleEncodingOptionObject = value =>
|
|
69
|
+
value
|
|
70
|
+
&& typeof value === 'object'
|
|
71
|
+
&& Object.keys(value).length === 1
|
|
72
|
+
&& Object.hasOwn(value, 'encoding');
|
|
73
|
+
|
|
74
|
+
function isUtf8Encoding(node, scope) {
|
|
75
|
+
if (
|
|
76
|
+
node.type === 'ObjectExpression'
|
|
77
|
+
&& node.properties.length === 1
|
|
78
|
+
&& node.properties[0].type === 'Property'
|
|
79
|
+
&& getPropertyName(node.properties[0], scope) === 'encoding'
|
|
80
|
+
&& isUtf8EncodingStringNode(node.properties[0].value, scope)
|
|
81
|
+
) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isUtf8EncodingStringNode(node, scope)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const staticValue = getStaticValue(node, scope);
|
|
90
|
+
if (!staticValue) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const {value} = staticValue;
|
|
95
|
+
if (
|
|
96
|
+
isSingleEncodingOptionObject(value)
|
|
97
|
+
&& isUtf8EncodingString(value.encoding)
|
|
98
|
+
) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isBufferEncoding(node, scope) {
|
|
106
|
+
const staticValue = getStaticValue(node, scope);
|
|
107
|
+
if (!staticValue) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const {value} = staticValue;
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
isSingleEncodingOptionObject(value)
|
|
118
|
+
&& (value.encoding === undefined || value.encoding === null)
|
|
119
|
+
) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isJsonReadFileCall(node, scope) {
|
|
127
|
+
if (
|
|
128
|
+
!(
|
|
129
|
+
node
|
|
130
|
+
&& node.type === 'CallExpression'
|
|
131
|
+
&& !node.optional
|
|
132
|
+
&& (node.arguments.length === 1 || node.arguments.length === 2)
|
|
133
|
+
&& !node.arguments.some(node => node.type === 'SpreadElement')
|
|
134
|
+
&& node.callee.type === 'MemberExpression'
|
|
135
|
+
&& !node.callee.optional
|
|
136
|
+
)
|
|
137
|
+
) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const method = getPropertyName(node.callee, scope);
|
|
142
|
+
return method === 'readFile' || method === 'readFileSync';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function addUtf8Encoding(fixer, callExpression, context) {
|
|
146
|
+
const {sourceCode} = context;
|
|
147
|
+
const [fileNode] = callExpression.arguments;
|
|
148
|
+
const tokenAfterFile = sourceCode.getTokenAfter(fileNode);
|
|
149
|
+
|
|
150
|
+
if (tokenAfterFile.value === ',') {
|
|
151
|
+
return fixer.insertTextAfter(tokenAfterFile, ' \'utf8\'');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return fixer.insertTextAfter(fileNode, ', \'utf8\'');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
158
|
+
const create = context => {
|
|
159
|
+
const [option] = context.options;
|
|
160
|
+
|
|
161
|
+
context.on('CallExpression', callExpression => {
|
|
162
|
+
if (!(isMethodCall(callExpression, {
|
|
163
|
+
object: 'JSON',
|
|
164
|
+
method: 'parse',
|
|
165
|
+
argumentsLength: 1,
|
|
166
|
+
optionalCall: false,
|
|
167
|
+
optionalMember: false,
|
|
168
|
+
}))) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const [argument] = callExpression.arguments;
|
|
173
|
+
const {sourceCode} = context;
|
|
174
|
+
const resolved = getIdentifierDeclaration(argument, sourceCode.getScope(argument));
|
|
175
|
+
if (!resolved) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const {node, scope} = resolved;
|
|
180
|
+
if (!isJsonReadFileCall(node, scope)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (option === 'string') {
|
|
185
|
+
if (node.arguments.length === 1) {
|
|
186
|
+
return {
|
|
187
|
+
node,
|
|
188
|
+
messageId: MESSAGE_ID_STRING,
|
|
189
|
+
fix: fixer => addUtf8Encoding(fixer, node, context),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const [, optionsNode] = node.arguments;
|
|
194
|
+
if (!isBufferEncoding(optionsNode, scope)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
node: optionsNode,
|
|
200
|
+
messageId: MESSAGE_ID_STRING,
|
|
201
|
+
fix: fixer => fixer.replaceText(optionsNode, '\'utf8\''),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (node.arguments.length !== 2) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const [, charsetNode] = node.arguments;
|
|
210
|
+
if (!isUtf8Encoding(charsetNode, scope)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
node: charsetNode,
|
|
216
|
+
messageId: MESSAGE_ID_BUFFER,
|
|
217
|
+
fix: fixer => removeArgument(fixer, charsetNode, context),
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const schema = [
|
|
223
|
+
{
|
|
224
|
+
enum: ['string', 'buffer'],
|
|
225
|
+
description: 'Whether to prefer reading JSON files as strings or buffers before passing them to `JSON.parse()`.',
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
230
|
+
const config = {
|
|
231
|
+
create,
|
|
232
|
+
meta: {
|
|
233
|
+
type: 'suggestion',
|
|
234
|
+
docs: {
|
|
235
|
+
description: 'Enforce consistent JSON file reads before `JSON.parse()`.',
|
|
236
|
+
recommended: true,
|
|
237
|
+
},
|
|
238
|
+
fixable: 'code',
|
|
239
|
+
schema,
|
|
240
|
+
defaultOptions: ['string'],
|
|
241
|
+
messages,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export default config;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {replaceTemplateElement} from './fix/index.js';
|
|
2
|
+
import {isTaggedTemplateLiteral} from './ast/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'consistent-template-literal-escape';
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Use `\\${` instead of `$\\{` to escape in template literals.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
10
|
+
const create = context => {
|
|
11
|
+
context.on('TemplateElement', node => {
|
|
12
|
+
if (isTaggedTemplateLiteral(node.parent)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {raw} = node.value;
|
|
17
|
+
|
|
18
|
+
// Match `$\{` or `\$\{` and replace with `\${`.
|
|
19
|
+
// The `\\?` makes the leading backslash optional to handle both patterns.
|
|
20
|
+
// The lookbehind ensures an even number of preceding backslashes (including zero).
|
|
21
|
+
const fixedRaw = raw.replaceAll(
|
|
22
|
+
/(?<=(?:^|[^\\])(?:\\\\)*)\\?\$\\{/g,
|
|
23
|
+
String.raw`\${`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (raw !== fixedRaw) {
|
|
27
|
+
const problem = {
|
|
28
|
+
node,
|
|
29
|
+
messageId: MESSAGE_ID,
|
|
30
|
+
fix: fixer => replaceTemplateElement(node, fixedRaw, context, fixer),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return problem;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
39
|
+
const config = {
|
|
40
|
+
create,
|
|
41
|
+
meta: {
|
|
42
|
+
type: 'suggestion',
|
|
43
|
+
docs: {
|
|
44
|
+
description: 'Enforce consistent style for escaping `${` in template literals.',
|
|
45
|
+
recommended: true,
|
|
46
|
+
},
|
|
47
|
+
fixable: 'code',
|
|
48
|
+
messages,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default config;
|