eslint-plugin-unicorn 68.0.0 → 69.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 (53) hide show
  1. package/package.json +3 -1
  2. package/readme.md +12 -0
  3. package/rules/ast/is-empty-object-expression.js +2 -2
  4. package/rules/comment-content.js +136 -18
  5. package/rules/consistent-boolean-name.js +31 -4
  6. package/rules/consistent-conditional-object-spread.js +9 -3
  7. package/rules/consistent-tuple-labels.js +49 -0
  8. package/rules/custom-error-definition.js +26 -7
  9. package/rules/index.js +12 -0
  10. package/rules/no-chained-comparison.js +1 -1
  11. package/rules/no-computed-property-existence-check.js +6 -0
  12. package/rules/no-incorrect-template-string-interpolation.js +82 -30
  13. package/rules/no-instanceof-builtins.js +59 -13
  14. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  15. package/rules/no-late-current-target-access.js +9 -71
  16. package/rules/no-late-event-control.js +127 -0
  17. package/rules/no-nonstandard-builtin-properties.js +2 -0
  18. package/rules/no-object-methods-with-collections.js +4 -289
  19. package/rules/no-return-array-push.js +20 -5
  20. package/rules/no-top-level-side-effects.js +8 -1
  21. package/rules/no-unnecessary-global-this.js +36 -1
  22. package/rules/no-unreadable-object-destructuring.js +15 -0
  23. package/rules/no-unsafe-string-replacement.js +31 -0
  24. package/rules/no-unused-array-method-return.js +11 -2
  25. package/rules/no-useless-boolean-cast.js +58 -1
  26. package/rules/no-useless-concat.js +27 -3
  27. package/rules/no-useless-else.js +16 -2
  28. package/rules/no-useless-iterator-to-array.js +165 -115
  29. package/rules/prefer-abort-signal-timeout.js +395 -0
  30. package/rules/prefer-aggregate-error.js +478 -0
  31. package/rules/prefer-at.js +5 -6
  32. package/rules/prefer-code-point.js +58 -8
  33. package/rules/prefer-continue.js +16 -0
  34. package/rules/prefer-dom-node-replace-children.js +353 -0
  35. package/rules/prefer-error-is-error.js +181 -0
  36. package/rules/prefer-has-check.js +1 -4
  37. package/rules/prefer-hoisting-branch-code.js +9 -7
  38. package/rules/prefer-iterator-concat.js +21 -1
  39. package/rules/prefer-minimal-ternary.js +15 -11
  40. package/rules/prefer-modern-dom-apis.js +1 -45
  41. package/rules/prefer-observer-apis.js +498 -0
  42. package/rules/prefer-promise-try.js +238 -0
  43. package/rules/prefer-set-methods.js +337 -0
  44. package/rules/prefer-toggle-attribute.js +293 -0
  45. package/rules/prefer-url-search-parameters.js +411 -0
  46. package/rules/shared/late-event-handler.js +82 -0
  47. package/rules/shared/name-replacements.js +6 -0
  48. package/rules/utils/builtin-collection-type.js +267 -0
  49. package/rules/utils/index.js +6 -0
  50. package/rules/utils/is-boolean.js +3 -1
  51. package/rules/utils/is-event.js +127 -0
  52. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  53. package/rules/utils/type-helpers.js +166 -8
@@ -0,0 +1,127 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {isMethodCall} from './ast/index.js';
3
+ import {
4
+ createLateEventHandlerTracker,
5
+ eventParameterNamePattern,
6
+ getEnclosingFunction,
7
+ } from './shared/late-event-handler.js';
8
+ import {
9
+ isEvent,
10
+ isKnownNonEvent,
11
+ unwrapTypeScriptExpression,
12
+ } from './utils/index.js';
13
+
14
+ const MESSAGE_ID_AFTER_SUSPENSION = 'after-suspension';
15
+ const MESSAGE_ID_IN_GENERATOR = 'in-generator';
16
+ const MESSAGE_ID_IN_NESTED_FUNCTION = 'in-nested-function';
17
+ const eventControlMethodNames = [
18
+ 'preventDefault',
19
+ 'stopImmediatePropagation',
20
+ 'stopPropagation',
21
+ ];
22
+ const messages = {
23
+ [MESSAGE_ID_AFTER_SUSPENSION]: '`{{name}}.{{method}}()` has no effect after the handler suspends. Call it before `await` or `yield` while the event is still being dispatched.',
24
+ [MESSAGE_ID_IN_GENERATOR]: '`{{name}}.{{method}}()` is not called during event dispatch inside a generator function. Use a normal function handler instead.',
25
+ [MESSAGE_ID_IN_NESTED_FUNCTION]: '`{{name}}.{{method}}()` may run after synchronous event dispatch has finished. Call it in the outer handler before deferring work.',
26
+ };
27
+
28
+ const getEventControlCall = node => {
29
+ if (!isMethodCall(node, {
30
+ methods: eventControlMethodNames,
31
+ computed: false,
32
+ })) {
33
+ return;
34
+ }
35
+
36
+ const {callee} = node;
37
+ const event = unwrapTypeScriptExpression(callee.object);
38
+ if (event.type !== 'Identifier') {
39
+ return;
40
+ }
41
+
42
+ return {
43
+ event,
44
+ method: callee.property.name,
45
+ };
46
+ };
47
+
48
+ /** @param {import('eslint').Rule.RuleContext} context */
49
+ const create = context => {
50
+ const {sourceCode} = context;
51
+ const lateEventHandlerTracker = createLateEventHandlerTracker(context);
52
+
53
+ const isEventParameter = node => {
54
+ if (isKnownNonEvent(node, context)) {
55
+ return false;
56
+ }
57
+
58
+ return eventParameterNamePattern.test(node.name)
59
+ || isEvent(node, context);
60
+ };
61
+
62
+ context.onExit('CallExpression', node => {
63
+ const eventControlCall = getEventControlCall(node);
64
+ if (!eventControlCall) {
65
+ return;
66
+ }
67
+
68
+ const {event, method} = eventControlCall;
69
+ const variable = findVariable(sourceCode.getScope(event), event);
70
+ const definition = variable?.defs.find(({type}) => type === 'Parameter');
71
+ if (!definition) {
72
+ return;
73
+ }
74
+
75
+ // For a parameter definition, `definition.node` is the function that declares it.
76
+ const handlerFunction = definition.node;
77
+ if (!isEventParameter(definition.name)) {
78
+ return;
79
+ }
80
+
81
+ const isInNestedFunction = getEnclosingFunction(node) !== handlerFunction;
82
+ const isInGeneratorHandler = handlerFunction.generator;
83
+ if (
84
+ !isInNestedFunction
85
+ && !isInGeneratorHandler
86
+ && !lateEventHandlerTracker.isFunctionSuspended(handlerFunction)
87
+ && !lateEventHandlerTracker.isInsideSuspendingLoop(node, handlerFunction)
88
+ ) {
89
+ return;
90
+ }
91
+
92
+ let messageId = MESSAGE_ID_AFTER_SUSPENSION;
93
+ if (isInNestedFunction) {
94
+ messageId = MESSAGE_ID_IN_NESTED_FUNCTION;
95
+ } else if (isInGeneratorHandler) {
96
+ messageId = MESSAGE_ID_IN_GENERATOR;
97
+ }
98
+
99
+ return {
100
+ node,
101
+ messageId,
102
+ data: {
103
+ name: event.name,
104
+ method,
105
+ },
106
+ };
107
+ });
108
+ };
109
+
110
+ /** @type {import('eslint').Rule.RuleModule} */
111
+ const config = {
112
+ create,
113
+ meta: {
114
+ type: 'problem',
115
+ docs: {
116
+ description: 'Disallow event-control method calls after the synchronous event dispatch has finished.',
117
+ recommended: true,
118
+ },
119
+ schema: [],
120
+ messages,
121
+ languages: [
122
+ 'js/js',
123
+ ],
124
+ },
125
+ };
126
+
127
+ export default config;
@@ -284,6 +284,8 @@ const errorPrototype = createPropertyInfo({
284
284
  properties: [
285
285
  'message',
286
286
  'name',
287
+ // Deliberate exception to the published-spec-only boundary below: `stack` is a stage-3 proposal (https://github.com/tc39/proposal-error-stack-accessor) that is universally implemented across engines.
288
+ 'stack',
287
289
  ],
288
290
  methods: [
289
291
  'toString',
@@ -1,30 +1,13 @@
1
- import {findVariable} from '@eslint-community/eslint-utils';
2
- import {isNewExpression} from './ast/index.js';
3
1
  import {
2
+ getBuiltinCollectionType,
3
+ isGlobalIdentifier,
4
4
  getParenthesizedText,
5
- getTypeSymbol,
6
- isUnknownType,
7
5
  shouldAddParenthesesToMemberExpressionObject,
8
6
  } from './utils/index.js';
9
7
 
10
8
  const MESSAGE_ID_ERROR = 'no-object-methods-with-collections/error';
11
9
  const MESSAGE_ID_SUGGESTION = 'no-object-methods-with-collections/suggestion';
12
10
 
13
- const mapTypes = new Set([
14
- 'Map',
15
- 'ReadonlyMap',
16
- ]);
17
-
18
- const setTypes = new Set([
19
- 'Set',
20
- 'ReadonlySet',
21
- ]);
22
-
23
- const collectionTypes = new Set([
24
- ...mapTypes,
25
- ...setTypes,
26
- ]);
27
-
28
11
  const objectMethods = new Set([
29
12
  'entries',
30
13
  'keys',
@@ -36,274 +19,6 @@ const messages = {
36
19
  [MESSAGE_ID_SUGGESTION]: 'Use `Array.from({{replacement}})`.',
37
20
  };
38
21
 
39
- const getTypeSet = type => new Set([type]);
40
-
41
- const getTypeFromTypeReferenceName = name => collectionTypes.has(name) ? name : undefined;
42
-
43
- const typeDeclarationNodeTypes = new Set([
44
- 'ClassDeclaration',
45
- 'TSEnumDeclaration',
46
- 'TSInterfaceDeclaration',
47
- 'TSTypeAliasDeclaration',
48
- ]);
49
-
50
- const getTypeDeclarationName = node => {
51
- if (!node || !typeDeclarationNodeTypes.has(node.type)) {
52
- return;
53
- }
54
-
55
- return node.id?.name;
56
- };
57
-
58
- const getProgramNode = node => {
59
- while (node.parent) {
60
- node = node.parent;
61
- }
62
-
63
- return node.type === 'Program' ? node : undefined;
64
- };
65
-
66
- const hasUserDefinedTypeName = (name, node) => {
67
- const program = getProgramNode(node);
68
- if (!program) {
69
- return false;
70
- }
71
-
72
- for (const node of program.body) {
73
- if (node.type === 'ImportDeclaration') {
74
- if (node.specifiers.some(specifier => specifier.local.name === name)) {
75
- return true;
76
- }
77
-
78
- continue;
79
- }
80
-
81
- if (
82
- node.type === 'ExportNamedDeclaration'
83
- || node.type === 'ExportDefaultDeclaration'
84
- ) {
85
- if (getTypeDeclarationName(node.declaration) === name) {
86
- return true;
87
- }
88
-
89
- continue;
90
- }
91
-
92
- if (getTypeDeclarationName(node) === name) {
93
- return true;
94
- }
95
- }
96
-
97
- return false;
98
- };
99
-
100
- const mergeTypeSets = typeSets => {
101
- const types = new Set();
102
-
103
- for (const typeSet of typeSets) {
104
- if (!typeSet) {
105
- return;
106
- }
107
-
108
- for (const type of typeSet) {
109
- types.add(type);
110
- }
111
- }
112
-
113
- return types;
114
- };
115
-
116
- const isUnshadowedGlobalIdentifier = (node, context) => {
117
- const variable = findVariable(context.sourceCode.getScope(node), node);
118
- return !variable || (variable.scope.type === 'global' && variable.defs.length === 0);
119
- };
120
-
121
- const isDefaultLibraryOnlySymbol = (symbol, program) =>
122
- symbol?.declarations?.length > 0
123
- && symbol.declarations.every(declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()));
124
-
125
- const getTypesFromType = (type, program) => {
126
- if (isUnknownType(type)) {
127
- return;
128
- }
129
-
130
- if (type.isUnion()) {
131
- return mergeTypeSets(type.types.map(type => getTypesFromType(type, program)));
132
- }
133
-
134
- const symbol = getTypeSymbol(type);
135
- if (!isDefaultLibraryOnlySymbol(symbol, program)) {
136
- return;
137
- }
138
-
139
- const typeName = symbol.getName();
140
- const collectionType = typeName && getTypeFromTypeReferenceName(typeName);
141
- return collectionType && getTypeSet(collectionType);
142
- };
143
-
144
- const hasUserDefinedCollectionType = (types, node) => {
145
- for (const type of types) {
146
- if (hasUserDefinedTypeName(type, node)) {
147
- return true;
148
- }
149
- }
150
-
151
- return false;
152
- };
153
-
154
- const getTypesFromTypeInformation = (node, context) => {
155
- const {parserServices} = context.sourceCode;
156
- if (!parserServices?.program) {
157
- return;
158
- }
159
-
160
- try {
161
- const {program} = parserServices;
162
- const types = getTypesFromType(
163
- parserServices.getTypeAtLocation(node),
164
- program,
165
- );
166
-
167
- if (types && !hasUserDefinedCollectionType(types, node)) {
168
- return types;
169
- }
170
- } catch {
171
- // TypeScript can throw while resolving incomplete projects; keep this fallback best-effort.
172
- }
173
- };
174
-
175
- const getTypesFromTypeAnnotation = (node, context) => {
176
- switch (node?.type) {
177
- case 'TSTypeAnnotation':
178
- case 'TSParenthesizedType': {
179
- return getTypesFromTypeAnnotation(node.typeAnnotation, context);
180
- }
181
-
182
- case 'TSTypeOperator': {
183
- return node.operator === 'readonly'
184
- ? getTypesFromTypeAnnotation(node.typeAnnotation, context)
185
- : undefined;
186
- }
187
-
188
- case 'TSTypeReference': {
189
- if (node.typeName.type !== 'Identifier') {
190
- return;
191
- }
192
-
193
- if (hasUserDefinedTypeName(node.typeName.name, node)) {
194
- return;
195
- }
196
-
197
- const type = getTypeFromTypeReferenceName(node.typeName.name);
198
- return type && getTypeSet(type);
199
- }
200
-
201
- case 'TSUnionType': {
202
- return mergeTypeSets(node.types.map(type => getTypesFromTypeAnnotation(type, context)));
203
- }
204
-
205
- default: {
206
- break;
207
- }
208
- }
209
- };
210
-
211
- const getTypesFromVariable = (node, context, visitedVariables) => {
212
- const variable = findVariable(context.sourceCode.getScope(node), node);
213
- if (
214
- !variable
215
- || visitedVariables.has(variable)
216
- || variable.defs.length !== 1
217
- ) {
218
- return;
219
- }
220
-
221
- visitedVariables.add(variable);
222
-
223
- const [definition] = variable.defs;
224
- const annotationTypes = getTypesFromTypeAnnotation(definition.name?.typeAnnotation, context);
225
- if (annotationTypes) {
226
- return annotationTypes;
227
- }
228
-
229
- if (
230
- definition.type !== 'Variable'
231
- || definition.parent.kind !== 'const'
232
- || !definition.node.init
233
- ) {
234
- return;
235
- }
236
-
237
- return getTypes(definition.node.init, context, visitedVariables);
238
- };
239
-
240
- const getTypesFromSyntax = (node, context, visitedVariables) => {
241
- if (
242
- isNewExpression(node, {names: ['Map', 'Set']})
243
- && isUnshadowedGlobalIdentifier(node.callee, context)
244
- ) {
245
- return getTypeSet(node.callee.name);
246
- }
247
-
248
- switch (node.type) {
249
- case 'Identifier': {
250
- return getTypesFromVariable(node, context, visitedVariables);
251
- }
252
-
253
- case 'TSAsExpression':
254
- case 'TSSatisfiesExpression':
255
- case 'TSTypeAssertion': {
256
- return getTypesFromTypeAnnotation(node.typeAnnotation, context) ?? getTypes(node.expression, context, visitedVariables);
257
- }
258
-
259
- case 'TSNonNullExpression': {
260
- return getTypes(node.expression, context, visitedVariables);
261
- }
262
-
263
- case 'ParenthesizedExpression': {
264
- return getTypes(node.expression, context, visitedVariables);
265
- }
266
-
267
- default: {
268
- break;
269
- }
270
- }
271
- };
272
-
273
- const getTypes = (node, context, visitedVariables = new Set()) =>
274
- getTypesFromTypeInformation(node, context)
275
- ?? getTypesFromSyntax(node, context, visitedVariables);
276
-
277
- const getCollectionType = (node, context) => {
278
- const types = getTypes(node, context);
279
- if (!types?.size) {
280
- return;
281
- }
282
-
283
- let hasMapType = false;
284
- let hasSetType = false;
285
-
286
- for (const type of types) {
287
- if (mapTypes.has(type)) {
288
- hasMapType = true;
289
- continue;
290
- }
291
-
292
- if (setTypes.has(type)) {
293
- hasSetType = true;
294
- continue;
295
- }
296
-
297
- return;
298
- }
299
-
300
- if (hasMapType && hasSetType) {
301
- return;
302
- }
303
-
304
- return hasMapType ? 'Map' : 'Set';
305
- };
306
-
307
22
  const getMemberObjectText = (node, context) => {
308
23
  const text = getParenthesizedText(node, context);
309
24
  return shouldAddParenthesesToMemberExpressionObject(node, context) ? `(${text})` : text;
@@ -319,7 +34,7 @@ const getProblem = (node, context) => {
319
34
  || callee.computed
320
35
  || callee.object.type !== 'Identifier'
321
36
  || callee.object.name !== 'Object'
322
- || !isUnshadowedGlobalIdentifier(callee.object, context)
37
+ || !isGlobalIdentifier(callee.object, context)
323
38
  || callee.property.type !== 'Identifier'
324
39
  || !objectMethods.has(callee.property.name)
325
40
  ) {
@@ -327,7 +42,7 @@ const getProblem = (node, context) => {
327
42
  }
328
43
 
329
44
  const [argument] = node.arguments;
330
- const type = getCollectionType(argument, context);
45
+ const type = getBuiltinCollectionType(argument, context);
331
46
  if (!type) {
332
47
  return;
333
48
  }
@@ -1,5 +1,5 @@
1
1
  import {isMethodCall} from './ast/index.js';
2
- import {needsSemicolon} from './utils/index.js';
2
+ import {isKnownNonArray, needsSemicolon} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID_ERROR = 'no-return-array-push/error';
5
5
  const MESSAGE_ID_SUGGESTION = 'no-return-array-push/suggestion';
@@ -77,9 +77,22 @@ function getDirectReturnStatement(callExpression) {
77
77
  }
78
78
  }
79
79
 
80
- function isBareExpressionStatement(expression) {
81
- const node = getCallExpressionResultNode(expression);
82
- return node.parent.type === 'ExpressionStatement';
80
+ function isReturnValueDiscarded(callExpression) {
81
+ const node = getCallExpressionResultNode(callExpression);
82
+ const {parent} = node;
83
+ return (
84
+ parent.type === 'ExpressionStatement'
85
+ // The `void` operator explicitly discards the return value.
86
+ || (parent.type === 'UnaryExpression' && parent.operator === 'void')
87
+ );
88
+ }
89
+
90
+ // Treat chained result member access as a pragmatic signal for custom APIs like router.push().catch(...).
91
+ // Do not flag realistic code only to catch theoretical Number method chains from Array#push().
92
+ function isResultMemberAccessed(callExpression) {
93
+ const node = getCallExpressionResultNode(callExpression);
94
+ const {parent} = node;
95
+ return parent.type === 'MemberExpression' && parent.object === node;
83
96
  }
84
97
 
85
98
  function getSuggestion(callExpression, returnStatement, method, context) {
@@ -121,7 +134,9 @@ const create = context => {
121
134
 
122
135
  if (
123
136
  (method === 'push' && isIgnoredCallee(callExpression.callee))
124
- || isBareExpressionStatement(callExpression)
137
+ || isReturnValueDiscarded(callExpression)
138
+ || isResultMemberAccessed(callExpression)
139
+ || isKnownNonArray(callExpression.callee.object, context)
125
140
  ) {
126
141
  return;
127
142
  }
@@ -12,7 +12,14 @@ const exportDeclarationTypes = new Set([
12
12
  'ExportNamedDeclaration',
13
13
  ]);
14
14
 
15
- const isExportDeclaration = node => exportDeclarationTypes.has(node.type);
15
+ // Type-only exports (`export type`, `export interface`, `export declare`, `export {type Foo}`, …) are erased when TypeScript is compiled to JavaScript, so a file whose only exports are type-only has no runtime exports.
16
+ const isTypeOnlyExport = node =>
17
+ node.exportKind === 'type'
18
+ || node.declaration?.type === 'TSInterfaceDeclaration'
19
+ // `export {type Foo}` keeps `exportKind: 'value'` on the declaration, but every specifier is type-only, so it is fully erased. `export {}` (no specifiers) is a runtime module marker and is not type-only.
20
+ || (node.specifiers?.length > 0 && node.specifiers.every(specifier => specifier.exportKind === 'type'));
21
+
22
+ const isExportDeclaration = node => exportDeclarationTypes.has(node.type) && !isTypeOnlyExport(node);
16
23
 
17
24
  const isAllowedAssignment = node => unwrapTypeScriptExpression(node).type === 'AssignmentExpression';
18
25
 
@@ -6,7 +6,13 @@ import {
6
6
  } from '@babel/helper-validator-identifier';
7
7
  import {findVariable} from '@eslint-community/eslint-utils';
8
8
  import {isStringLiteral} from './ast/index.js';
9
- import {hasOptionalChainElement, isGlobalIdentifier, isLeftHandSide} from './utils/index.js';
9
+ import {
10
+ hasOptionalChainElement,
11
+ isGlobalIdentifier,
12
+ isLeftHandSide,
13
+ isControlFlowTest,
14
+ isBooleanExpression,
15
+ } from './utils/index.js';
10
16
 
11
17
  const MESSAGE_ID = 'no-unnecessary-global-this';
12
18
  const messages = {
@@ -96,6 +102,34 @@ function isTaggedTemplateCallee(node) {
96
102
  && node.parent.tag === node;
97
103
  }
98
104
 
105
+ const equalityOperators = new Set(['==', '!=', '===', '!==']);
106
+
107
+ // `globalThis.foo === undefined`, `globalThis.foo != null`, … compare the global against `null`/`undefined` to detect its presence.
108
+ function isNullishComparison(node) {
109
+ const {parent} = node;
110
+ if (
111
+ parent.type !== 'BinaryExpression'
112
+ || !equalityOperators.has(parent.operator)
113
+ ) {
114
+ return false;
115
+ }
116
+
117
+ const other = unwrapTypeScriptExpression(parent.left === node ? parent.right : parent.left);
118
+
119
+ return (other.type === 'Literal' && other.value === null)
120
+ || (other.type === 'Identifier' && other.name === 'undefined');
121
+ }
122
+
123
+ // `globalThis.foo` in an existence check (`if (globalThis.foo)`, `globalThis.foo ?? x`, `!globalThis.foo`, `globalThis.foo === undefined`, …) safely yields `undefined` when the global is absent, whereas bare `foo` throws a `ReferenceError`. This is deliberate feature detection, so the `globalThis` receiver must be kept.
124
+ function isExistenceCheck(node, context) {
125
+ node = getOuterTypeScriptExpression(node);
126
+
127
+ return node.parent.type === 'LogicalExpression'
128
+ || isNullishComparison(node)
129
+ || isControlFlowTest(node)
130
+ || isBooleanExpression(node, context);
131
+ }
132
+
99
133
  function isOptionalChainUsage(node) {
100
134
  if (hasOptionalChainElement(node)) {
101
135
  return true;
@@ -123,6 +157,7 @@ const create = context => {
123
157
  || !isGlobalIdentifier(object, context)
124
158
  || isWritableTarget(writableTarget)
125
159
  || isOptionalChainUsage(node)
160
+ || isExistenceCheck(node, context)
126
161
  ) {
127
162
  return;
128
163
  }
@@ -1,3 +1,4 @@
1
+ import {getStaticValue} from '@eslint-community/eslint-utils';
1
2
  import {isTypeScriptExpressionWrapper} from './utils/index.js';
2
3
 
3
4
  const MESSAGE_ID_COMPUTED_KEY = 'computed-key';
@@ -70,6 +71,10 @@ function getObjectPatternDepth(node) {
70
71
  return depth;
71
72
  }
72
73
 
74
+ function isStaticComputedKey(node, scope) {
75
+ return getStaticValue(node, scope) !== null;
76
+ }
77
+
73
78
  /** @param {import('eslint').Rule.RuleContext} context */
74
79
  const create = context => {
75
80
  context.on('Property', node => {
@@ -80,6 +85,16 @@ const create = context => {
80
85
  return;
81
86
  }
82
87
 
88
+ // A computed key is the only way to exclude a dynamic property from a rest element,
89
+ // so allow it when the same object pattern collects a rest. A static key is not
90
+ // dynamic, so it stays disallowed.
91
+ if (
92
+ !isStaticComputedKey(node.key, context.sourceCode.getScope(node.key))
93
+ && node.parent.properties.some(property => property.type === 'RestElement')
94
+ ) {
95
+ return;
96
+ }
97
+
83
98
  return {
84
99
  node,
85
100
  messageId: MESSAGE_ID_COMPUTED_KEY,
@@ -5,6 +5,10 @@ import {
5
5
  isStringLiteral,
6
6
  } from './ast/index.js';
7
7
  import {unwrapExpression} from './utils/comparison.js';
8
+ import {
9
+ getConstVariableInitializer,
10
+ isKnownNonString,
11
+ } from './utils/index.js';
8
12
 
9
13
  const MESSAGE_ID = 'no-unsafe-string-replacement';
10
14
  const messages = {
@@ -34,6 +38,25 @@ const isAllowedReplacement = (node, sourceCode) => {
34
38
  || isFunction(node);
35
39
  };
36
40
 
41
+ const objectCoercionPropertyNames = new Set([
42
+ '__proto__',
43
+ 'toString',
44
+ 'valueOf',
45
+ ]);
46
+
47
+ const isPlainObjectReplacement = (node, context) => {
48
+ node = unwrapExpression(node);
49
+ const replacement = unwrapExpression(getConstVariableInitializer(node, context) ?? node);
50
+
51
+ return replacement.type === 'ObjectExpression'
52
+ && replacement.properties.every(property =>
53
+ property.type === 'Property'
54
+ && !property.computed
55
+ && !property.method
56
+ && property.kind === 'init'
57
+ && !objectCoercionPropertyNames.has(property.key.name ?? property.key.value));
58
+ };
59
+
37
60
  /** @param {import('eslint').Rule.RuleContext} context */
38
61
  const create = context => {
39
62
  context.on('CallExpression', node => {
@@ -49,6 +72,14 @@ const create = context => {
49
72
  return;
50
73
  }
51
74
 
75
+ if (isPlainObjectReplacement(replacement, context)) {
76
+ return;
77
+ }
78
+
79
+ if (isKnownNonString(node.callee.object, context)) {
80
+ return;
81
+ }
82
+
52
83
  return {
53
84
  node: replacement,
54
85
  messageId: MESSAGE_ID,
@@ -1,5 +1,5 @@
1
1
  import {findVariable, getPropertyName} from '@eslint-community/eslint-utils';
2
- import {isValueNotUsable} from './utils/index.js';
2
+ import {isArray, isValueNotUsable} from './utils/index.js';
3
3
 
4
4
  const MESSAGE_ID = 'no-unused-array-method-return';
5
5
  const messages = {
@@ -38,6 +38,10 @@ const methods = new Set([
38
38
  'with',
39
39
  ]);
40
40
 
41
+ const methodsRequiringArrayReceiver = new Set([
42
+ 'values',
43
+ ]);
44
+
41
45
  const pascalCaseNamePattern = /^\p{Uppercase_Letter}/v;
42
46
  const uncertainValue = Symbol('uncertainValue');
43
47
  const nonArrayFactoryFunctions = new Set([
@@ -238,6 +242,11 @@ const isObviouslyNonArrayReceiver = (node, context) => {
238
242
  );
239
243
  };
240
244
 
245
+ const shouldSkipReceiver = (node, method, context) =>
246
+ methodsRequiringArrayReceiver.has(method)
247
+ ? !isArray(node, context)
248
+ : isObviouslyNonArrayReceiver(node, context);
249
+
241
250
  const getTrackedMethodName = (node, context) =>
242
251
  node.callee.type === 'MemberExpression'
243
252
  ? getStaticPropertyName(node.callee, context)
@@ -294,7 +303,7 @@ const create = context => {
294
303
  if (
295
304
  !methods.has(method)
296
305
  || !isDiscardedExpression(node)
297
- || isObviouslyNonArrayReceiver(node.callee.object, context)
306
+ || shouldSkipReceiver(node.callee.object, method, context)
298
307
  ) {
299
308
  return;
300
309
  }