eslint-plugin-unicorn 64.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 (100) hide show
  1. package/index.js +14 -0
  2. package/package.json +7 -3
  3. package/readme.md +183 -151
  4. package/rules/better-dom-traversing.js +291 -0
  5. package/rules/consistent-compound-words.js +452 -0
  6. package/rules/consistent-destructuring.js +97 -9
  7. package/rules/consistent-function-scoping.js +20 -0
  8. package/rules/consistent-json-file-read.js +245 -0
  9. package/rules/custom-error-definition.js +89 -4
  10. package/rules/dom-node-dataset.js +431 -0
  11. package/rules/escape-case.js +1 -1
  12. package/rules/expiring-todo-comments.js +42 -81
  13. package/rules/explicit-length-check.js +139 -6
  14. package/rules/filename-case.js +95 -21
  15. package/rules/import-style.js +21 -1
  16. package/rules/index.js +33 -3
  17. package/rules/isolated-functions.js +7 -5
  18. package/rules/no-anonymous-default-export.js +6 -6
  19. package/rules/no-array-callback-reference.js +89 -23
  20. package/rules/no-array-fill-with-reference-type.js +127 -0
  21. package/rules/no-array-for-each.js +17 -1
  22. package/rules/no-array-from-fill.js +76 -0
  23. package/rules/no-array-reduce.js +508 -2
  24. package/rules/no-blob-to-file.js +299 -0
  25. package/rules/no-canvas-to-image.js +152 -0
  26. package/rules/no-confusing-array-splice.js +164 -0
  27. package/rules/no-duplicate-set-values.js +57 -0
  28. package/rules/no-empty-file.js +31 -2
  29. package/rules/no-exports-in-scripts.js +34 -0
  30. package/rules/no-hex-escape.js +1 -1
  31. package/rules/no-incorrect-query-selector.js +347 -0
  32. package/rules/no-invalid-file-input-accept.js +475 -0
  33. package/rules/no-keyword-prefix.js +20 -5
  34. package/rules/no-late-current-target-access.js +166 -0
  35. package/rules/no-lonely-if.js +151 -50
  36. package/rules/no-manually-wrapped-comments.js +151 -0
  37. package/rules/no-null.js +14 -3
  38. package/rules/no-this-outside-of-class.js +75 -0
  39. package/rules/no-typeof-undefined.js +6 -4
  40. package/rules/no-unnecessary-nested-ternary.js +132 -0
  41. package/rules/no-unnecessary-polyfills.js +66 -20
  42. package/rules/no-unreadable-iife.js +33 -3
  43. package/rules/no-unused-array-method-return.js +301 -0
  44. package/rules/no-useless-promise-resolve-reject.js +5 -5
  45. package/rules/no-useless-spread.js +53 -4
  46. package/rules/no-useless-undefined.js +52 -45
  47. package/rules/numeric-separators-style.js +13 -4
  48. package/rules/prefer-array-last-methods.js +93 -0
  49. package/rules/prefer-array-some.js +66 -5
  50. package/rules/prefer-at.js +114 -55
  51. package/rules/prefer-dom-node-append.js +2 -2
  52. package/rules/prefer-dom-node-remove.js +24 -18
  53. package/rules/prefer-export-from.js +6 -6
  54. package/rules/prefer-get-or-insert-computed.js +159 -0
  55. package/rules/prefer-global-this.js +7 -46
  56. package/rules/prefer-https.js +83 -0
  57. package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
  58. package/rules/prefer-iterator-concat.js +174 -0
  59. package/rules/prefer-iterator-to-array-at-end.js +123 -0
  60. package/rules/prefer-keyboard-event-key.js +202 -76
  61. package/rules/prefer-math-abs.js +397 -0
  62. package/rules/prefer-math-min-max.js +5 -3
  63. package/rules/prefer-node-protocol.js +6 -4
  64. package/rules/prefer-query-selector.js +119 -7
  65. package/rules/prefer-queue-microtask.js +204 -0
  66. package/rules/prefer-regexp-test.js +164 -18
  67. package/rules/prefer-set-has.js +411 -22
  68. package/rules/prefer-single-call.js +128 -38
  69. package/rules/prefer-split-limit.js +107 -0
  70. package/rules/prefer-spread.js +34 -4
  71. package/rules/prefer-string-match-all.js +352 -0
  72. package/rules/prefer-string-pad-start-end.js +380 -0
  73. package/rules/prefer-string-raw.js +90 -19
  74. package/rules/prefer-string-repeat.js +173 -0
  75. package/rules/prefer-string-replace-all.js +180 -12
  76. package/rules/prefer-string-slice.js +54 -3
  77. package/rules/prefer-top-level-await.js +129 -1
  78. package/rules/prevent-abbreviations.js +126 -134
  79. package/rules/require-css-escape.js +141 -0
  80. package/rules/require-module-specifiers.js +62 -0
  81. package/rules/require-passive-events.js +292 -0
  82. package/rules/shared/dom-events.js +7 -7
  83. package/rules/shared/identifier-checks.js +205 -0
  84. package/rules/shared/package-json.js +1 -1
  85. package/rules/string-content.js +34 -5
  86. package/rules/template-indent.js +52 -3
  87. package/rules/throw-new-error.js +4 -0
  88. package/rules/try-complexity.js +184 -0
  89. package/rules/utils/boolean.js +12 -12
  90. package/rules/utils/get-duplicate-array-elements.js +77 -0
  91. package/rules/utils/index.js +1 -0
  92. package/rules/utils/is-same-reference.js +6 -4
  93. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
  94. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
  95. package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -7
  96. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
  97. package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
  98. package/rules/better-regex.js +0 -155
  99. package/rules/prefer-dom-node-dataset.js +0 -135
  100. package/rules/prefer-json-parse-buffer.js +0 -161
@@ -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;
@@ -3,10 +3,15 @@ import {
3
3
  getParenthesizedText,
4
4
  isNodeMatchesNameOrPath,
5
5
  } from './utils/index.js';
6
+ import {isUndefined} from './ast/index.js';
6
7
 
7
8
  const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
9
+ const MESSAGE_ID_DO_NOT_PASS_MESSAGE_TO_SUPER = 'doNotPassMessageToSuper';
10
+ const MESSAGE_ID_DO_NOT_ASSIGN_MESSAGE_WITHOUT_SETTER = 'doNotAssignMessageWithoutSetter';
8
11
  const messages = {
9
12
  [MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class',
13
+ [MESSAGE_ID_DO_NOT_PASS_MESSAGE_TO_SUPER]: 'Do not pass the error message to `super()` when the class defines a `message` accessor.',
14
+ [MESSAGE_ID_DO_NOT_ASSIGN_MESSAGE_WITHOUT_SETTER]: 'Do not assign to `this.message` when the class defines a `message` getter without a setter.',
10
15
  };
11
16
 
12
17
  const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
@@ -44,6 +49,7 @@ const isSuperExpression = node =>
44
49
  const isAssignmentExpression = (node, name) =>
45
50
  node.type === 'ExpressionStatement'
46
51
  && node.expression.type === 'AssignmentExpression'
52
+ && node.expression.operator === '='
47
53
  && isNodeMatchesNameOrPath(node.expression.left, `this.${name}`);
48
54
 
49
55
  const createInvalidNameError = (node, name) => ({
@@ -62,7 +68,52 @@ const isValidNameProperty = (nameProperty, className) =>
62
68
  nameProperty?.value
63
69
  && nameProperty.value.value === className;
64
70
 
65
- function * checkConstructorBody(context, constructor, name, nameProperty) {
71
+ const isMessageAccessor = (node, kind) =>
72
+ node.type === 'MethodDefinition'
73
+ && node.kind === kind
74
+ && !node.static
75
+ && !node.computed
76
+ && node.key.type === 'Identifier'
77
+ && node.key.name === 'message';
78
+
79
+ const isMissingOrUndefined = node =>
80
+ !node
81
+ || isUndefined(node);
82
+
83
+ const isSameText = (left, right, sourceCode) =>
84
+ sourceCode.getText(left) === sourceCode.getText(right);
85
+
86
+ const hasThisOrSuper = (node, visitorKeys) => {
87
+ if (node.type === 'ThisExpression' || node.type === 'Super') {
88
+ return true;
89
+ }
90
+
91
+ const keys = visitorKeys[node.type] ?? [];
92
+
93
+ for (const key of keys) {
94
+ const value = node[key];
95
+
96
+ if (!value) {
97
+ continue;
98
+ }
99
+
100
+ if (Array.isArray(value)) {
101
+ if (value.some(childNode => childNode && hasThisOrSuper(childNode, visitorKeys))) {
102
+ return true;
103
+ }
104
+
105
+ continue;
106
+ }
107
+
108
+ if (hasThisOrSuper(value, visitorKeys)) {
109
+ return true;
110
+ }
111
+ }
112
+
113
+ return false;
114
+ };
115
+
116
+ function * checkConstructorBody(context, constructor, errorDefinition) {
66
117
  const {sourceCode} = context;
67
118
  const constructorBodyNode = constructor.value.body;
68
119
 
@@ -72,24 +123,49 @@ function * checkConstructorBody(context, constructor, name, nameProperty) {
72
123
  }
73
124
 
74
125
  const constructorBody = constructorBodyNode.body;
126
+ const {name, nameProperty, hasMessageGetter, hasMessageSetter} = errorDefinition;
75
127
 
76
128
  const superExpression = constructorBody.find(bodyNode => isSuperExpression(bodyNode));
129
+ const superExpressionIndex = constructorBody.findIndex(bodyNode => isSuperExpression(bodyNode));
77
130
  const messageExpressionIndex = constructorBody.findIndex(bodyNode => isAssignmentExpression(bodyNode, 'message'));
131
+ const hasMessageAccessor = hasMessageGetter || hasMessageSetter;
78
132
 
79
133
  if (!superExpression) {
80
134
  yield {
81
135
  node: constructorBodyNode,
82
136
  message: 'Missing call to `super()` in constructor.',
83
137
  };
84
- } else if (messageExpressionIndex !== -1) {
138
+ } else if (hasMessageAccessor && !isMissingOrUndefined(superExpression.expression.arguments[0])) {
139
+ yield {
140
+ node: superExpression,
141
+ messageId: MESSAGE_ID_DO_NOT_PASS_MESSAGE_TO_SUPER,
142
+ };
143
+ } else if (
144
+ hasMessageGetter
145
+ && !hasMessageSetter
146
+ && messageExpressionIndex !== -1
147
+ ) {
148
+ yield {
149
+ node: constructorBody[messageExpressionIndex],
150
+ messageId: MESSAGE_ID_DO_NOT_ASSIGN_MESSAGE_WITHOUT_SETTER,
151
+ };
152
+ } else if (!hasMessageSetter && messageExpressionIndex !== -1) {
85
153
  const expression = constructorBody[messageExpressionIndex];
86
154
 
87
155
  yield {
88
156
  node: superExpression,
89
157
  message: 'Pass the error message to `super()` instead of setting `this.message`.',
90
158
  * fix(fixer) {
159
+ const rhs = expression.expression.right;
160
+
91
161
  if (superExpression.expression.arguments.length === 0) {
92
- const rhs = expression.expression.right;
162
+ if (
163
+ messageExpressionIndex !== superExpressionIndex + 1
164
+ || hasThisOrSuper(rhs, sourceCode.visitorKeys)
165
+ ) {
166
+ return;
167
+ }
168
+
93
169
  const [start] = sourceCode.getRange(superExpression);
94
170
  // This part crashes on ESLint 10, but it's still not correct.
95
171
  // There can be spaces, comments after `super`
@@ -97,6 +173,8 @@ function * checkConstructorBody(context, constructor, name, nameProperty) {
97
173
  [start, start + 6],
98
174
  getParenthesizedText(rhs, context),
99
175
  );
176
+ } else if (!isSameText(superExpression.expression.arguments[0], rhs, sourceCode)) {
177
+ return;
100
178
  }
101
179
 
102
180
  const start = messageExpressionIndex === 0
@@ -148,6 +226,8 @@ function * customErrorDefinition(context, node) {
148
226
  const {sourceCode} = context;
149
227
  const constructor = body.find(x => x.kind === 'constructor');
150
228
  const nameProperty = body.find(classNode => isPropertyDefinition(classNode, 'name'));
229
+ const hasMessageGetter = body.some(classNode => isMessageAccessor(classNode, 'get'));
230
+ const hasMessageSetter = body.some(classNode => isMessageAccessor(classNode, 'set'));
151
231
 
152
232
  if (!constructor) {
153
233
  if (isValidNameProperty(nameProperty, name)) {
@@ -176,7 +256,12 @@ function * customErrorDefinition(context, node) {
176
256
  return;
177
257
  }
178
258
 
179
- yield * checkConstructorBody(context, constructor, name, nameProperty);
259
+ yield * checkConstructorBody(context, constructor, {
260
+ name,
261
+ nameProperty,
262
+ hasMessageGetter,
263
+ hasMessageSetter,
264
+ });
180
265
  }
181
266
 
182
267
  const customErrorExport = (context, node) => {