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
@@ -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
- variables: sourceCode.getDeclaredVariables(node),
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 destructurings = objectPattern.properties.filter(property =>
86
- property.type === 'Property'
87
- && property.key.type === 'Identifier'
88
- && property.value.type === 'Identifier',
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 hasRest = lastProperty && lastProperty.type === 'RestElement';
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 = destructurings.find(property =>
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 isArrowFunctionWithThis = scope =>
98
- scope.type === 'function'
99
- && scope.block?.type === 'ArrowFunctionExpression'
100
- && (scope.thisFound || scope.childScopes.some(scope => isArrowFunctionWithThis(scope)));
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 (!scope || isArrowFunctionWithThis(scope)) {
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;