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.
- package/package.json +3 -1
- package/readme.md +12 -0
- package/rules/ast/is-empty-object-expression.js +2 -2
- package/rules/comment-content.js +136 -18
- package/rules/consistent-boolean-name.js +31 -4
- package/rules/consistent-conditional-object-spread.js +9 -3
- package/rules/consistent-tuple-labels.js +49 -0
- package/rules/custom-error-definition.js +26 -7
- package/rules/index.js +12 -0
- package/rules/no-chained-comparison.js +1 -1
- package/rules/no-computed-property-existence-check.js +6 -0
- package/rules/no-incorrect-template-string-interpolation.js +82 -30
- package/rules/no-instanceof-builtins.js +59 -13
- package/rules/no-invalid-well-known-symbol-methods.js +205 -0
- package/rules/no-late-current-target-access.js +9 -71
- package/rules/no-late-event-control.js +127 -0
- package/rules/no-nonstandard-builtin-properties.js +2 -0
- package/rules/no-object-methods-with-collections.js +4 -289
- package/rules/no-return-array-push.js +20 -5
- package/rules/no-top-level-side-effects.js +8 -1
- package/rules/no-unnecessary-global-this.js +36 -1
- package/rules/no-unreadable-object-destructuring.js +15 -0
- package/rules/no-unsafe-string-replacement.js +31 -0
- package/rules/no-unused-array-method-return.js +11 -2
- package/rules/no-useless-boolean-cast.js +58 -1
- package/rules/no-useless-concat.js +27 -3
- package/rules/no-useless-else.js +16 -2
- package/rules/no-useless-iterator-to-array.js +165 -115
- package/rules/prefer-abort-signal-timeout.js +395 -0
- package/rules/prefer-aggregate-error.js +478 -0
- package/rules/prefer-at.js +5 -6
- package/rules/prefer-code-point.js +58 -8
- package/rules/prefer-continue.js +16 -0
- package/rules/prefer-dom-node-replace-children.js +353 -0
- package/rules/prefer-error-is-error.js +181 -0
- package/rules/prefer-has-check.js +1 -4
- package/rules/prefer-hoisting-branch-code.js +9 -7
- package/rules/prefer-iterator-concat.js +21 -1
- package/rules/prefer-minimal-ternary.js +15 -11
- package/rules/prefer-modern-dom-apis.js +1 -45
- package/rules/prefer-observer-apis.js +498 -0
- package/rules/prefer-promise-try.js +238 -0
- package/rules/prefer-set-methods.js +337 -0
- package/rules/prefer-toggle-attribute.js +293 -0
- package/rules/prefer-url-search-parameters.js +411 -0
- package/rules/shared/late-event-handler.js +82 -0
- package/rules/shared/name-replacements.js +6 -0
- package/rules/utils/builtin-collection-type.js +267 -0
- package/rules/utils/index.js +6 -0
- package/rules/utils/is-boolean.js +3 -1
- package/rules/utils/is-event.js +127 -0
- package/rules/utils/should-report-replace-children-receiver.js +210 -0
- 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
|
-
|| !
|
|
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 =
|
|
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
|
|
81
|
-
const node = getCallExpressionResultNode(
|
|
82
|
-
|
|
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
|
-
||
|
|
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
|
-
|
|
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 {
|
|
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
|
-
||
|
|
306
|
+
|| shouldSkipReceiver(node.callee.object, method, context)
|
|
298
307
|
) {
|
|
299
308
|
return;
|
|
300
309
|
}
|