Skip to content

Commit dc51b1c

Browse files
authored
fix(no-multiple-resolved): false positives when the last expression in a try block is a call to resolve (#384)
1 parent 70f0012 commit dc51b1c

File tree

2 files changed

+247
-12
lines changed

2 files changed

+247
-12
lines changed

Diff for: __tests__/no-multiple-resolved.js

+163
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,74 @@ ruleTester.run('no-multiple-resolved', rule, {
8888
resolve(value)
8989
})
9090
})`,
91+
`new Promise(async (resolve, reject) => {
92+
try {
93+
await foo();
94+
resolve();
95+
} catch (error) {
96+
reject(error);
97+
}
98+
})`,
99+
`new Promise(async (resolve, reject) => {
100+
try {
101+
const r = await foo();
102+
resolve(r);
103+
} catch (error) {
104+
reject(error);
105+
}
106+
})`,
107+
`new Promise(async (resolve, reject) => {
108+
try {
109+
const r = await foo();
110+
resolve(r());
111+
} catch (error) {
112+
reject(error);
113+
}
114+
})`,
115+
`new Promise(async (resolve, reject) => {
116+
try {
117+
const r = await foo();
118+
resolve(r.foo);
119+
} catch (error) {
120+
reject(error);
121+
}
122+
})`,
123+
`new Promise(async (resolve, reject) => {
124+
try {
125+
const r = await foo();
126+
resolve(new r());
127+
} catch (error) {
128+
reject(error);
129+
}
130+
})`,
131+
`new Promise(async (resolve, reject) => {
132+
try {
133+
const r = await foo();
134+
resolve(import(r));
135+
} catch (error) {
136+
reject(error);
137+
}
138+
})`,
139+
`new Promise((resolve, reject) => {
140+
fn(async function * () {
141+
try {
142+
const r = await foo();
143+
resolve(yield r);
144+
} catch (error) {
145+
reject(error);
146+
}
147+
})
148+
})`,
149+
`new Promise(async (resolve, reject) => {
150+
let a;
151+
try {
152+
const r = await foo();
153+
resolve();
154+
if(r) return;
155+
} catch (error) {
156+
reject(error);
157+
}
158+
})`,
91159
],
92160

93161
invalid: [
@@ -306,5 +374,100 @@ ruleTester.run('no-multiple-resolved', rule, {
306374
},
307375
],
308376
},
377+
{
378+
code: `new Promise(async (resolve, reject) => {
379+
try {
380+
const r = await foo();
381+
resolve();
382+
r();
383+
} catch (error) {
384+
reject(error);
385+
}
386+
})`,
387+
errors: [
388+
{
389+
message:
390+
'Promise should not be resolved multiple times. Promise is potentially resolved on line 4.',
391+
line: 7,
392+
},
393+
],
394+
},
395+
{
396+
code: `new Promise(async (resolve, reject) => {
397+
let a;
398+
try {
399+
const r = await foo();
400+
resolve();
401+
a = r.foo;
402+
} catch (error) {
403+
reject(error);
404+
}
405+
})`,
406+
errors: [
407+
{
408+
message:
409+
'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.',
410+
line: 8,
411+
},
412+
],
413+
},
414+
{
415+
code: `new Promise(async (resolve, reject) => {
416+
let a;
417+
try {
418+
const r = await foo();
419+
resolve();
420+
a = new r();
421+
} catch (error) {
422+
reject(error);
423+
}
424+
})`,
425+
errors: [
426+
{
427+
message:
428+
'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.',
429+
line: 8,
430+
},
431+
],
432+
},
433+
{
434+
code: `new Promise(async (resolve, reject) => {
435+
let a;
436+
try {
437+
const r = await foo();
438+
resolve();
439+
import(r);
440+
} catch (error) {
441+
reject(error);
442+
}
443+
})`,
444+
errors: [
445+
{
446+
message:
447+
'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.',
448+
line: 8,
449+
},
450+
],
451+
},
452+
{
453+
code: `new Promise((resolve, reject) => {
454+
fn(async function * () {
455+
try {
456+
const r = await foo();
457+
resolve();
458+
yield r;
459+
} catch (error) {
460+
reject(error);
461+
}
462+
})
463+
})`,
464+
errors: [
465+
{
466+
message:
467+
'Promise should not be resolved multiple times. Promise is potentially resolved on line 5.',
468+
line: 8,
469+
},
470+
],
471+
},
309472
],
310473
})

Diff for: rules/no-multiple-resolved.js

+84-12
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,25 @@ const {
1212

1313
/**
1414
* @typedef {import('estree').Node} Node
15+
* @typedef {import('estree').Expression} Expression
1516
* @typedef {import('estree').Identifier} Identifier
1617
* @typedef {import('estree').FunctionExpression} FunctionExpression
1718
* @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
1819
* @typedef {import('estree').SimpleCallExpression} CallExpression
20+
* @typedef {import('estree').MemberExpression} MemberExpression
21+
* @typedef {import('estree').NewExpression} NewExpression
22+
* @typedef {import('estree').ImportExpression} ImportExpression
23+
* @typedef {import('estree').YieldExpression} YieldExpression
1924
* @typedef {import('eslint').Rule.CodePath} CodePath
2025
* @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment
2126
*/
2227

28+
/**
29+
* An expression that can throw an error.
30+
* see https://github.com/eslint/eslint/blob/e940be7a83d0caea15b64c1e1c2785a6540e2641/lib/linter/code-path-analysis/code-path-analyzer.js#L639-L643
31+
* @typedef {CallExpression | MemberExpression | NewExpression | ImportExpression | YieldExpression} ThrowableExpression
32+
*/
33+
2334
/**
2435
* Iterate all previous path segments.
2536
* @param {CodePathSegment} segment
@@ -150,14 +161,18 @@ class CodePathInfo {
150161

151162
/**
152163
* Check all paths and return paths resolved multiple times.
164+
* @param {PromiseCodePathContext} promiseCodePathContext
153165
* @returns {Iterable<AlreadyResolvedData & { node: Identifier }>}
154166
*/
155-
*iterateReports() {
167+
*iterateReports(promiseCodePathContext) {
156168
const targets = [...this.segmentInfos.values()].filter(
157169
(info) => info.resolved
158170
)
159171
for (const segmentInfo of targets) {
160-
const result = this._getAlreadyResolvedData(segmentInfo.segment)
172+
const result = this._getAlreadyResolvedData(
173+
segmentInfo.segment,
174+
promiseCodePathContext
175+
)
161176
if (result) {
162177
yield {
163178
node: segmentInfo.resolved,
@@ -170,14 +185,18 @@ class CodePathInfo {
170185
/**
171186
* Compute the previously resolved path.
172187
* @param {CodePathSegment} segment
188+
* @param {PromiseCodePathContext} promiseCodePathContext
173189
* @returns {AlreadyResolvedData | null}
174190
*/
175-
_getAlreadyResolvedData(segment) {
176-
if (segment.prevSegments.length === 0) {
191+
_getAlreadyResolvedData(segment, promiseCodePathContext) {
192+
const prevSegments = segment.prevSegments.filter(
193+
(prev) => !promiseCodePathContext.isResolvedTryBlockCodePathSegment(prev)
194+
)
195+
if (prevSegments.length === 0) {
177196
return null
178197
}
179-
const prevSegmentInfos = segment.prevSegments.map((prev) =>
180-
this._getProcessedSegmentInfo(prev)
198+
const prevSegmentInfos = prevSegments.map((prev) =>
199+
this._getProcessedSegmentInfo(prev, promiseCodePathContext)
181200
)
182201
if (prevSegmentInfos.every((info) => info.resolved)) {
183202
// If the previous paths are all resolved, the next path is also resolved.
@@ -246,16 +265,20 @@ class CodePathInfo {
246265
}
247266
/**
248267
* @param {CodePathSegment} segment
268+
* @param {PromiseCodePathContext} promiseCodePathContext
249269
*/
250-
_getProcessedSegmentInfo(segment) {
270+
_getProcessedSegmentInfo(segment, promiseCodePathContext) {
251271
const segmentInfo = this.segmentInfos.get(segment)
252272
if (segmentInfo) {
253273
return segmentInfo
254274
}
255275
const newInfo = new CodePathSegmentInfo(this, segment)
256276
this.segmentInfos.set(segment, newInfo)
257277

258-
const alreadyResolvedData = this._getAlreadyResolvedData(segment)
278+
const alreadyResolvedData = this._getAlreadyResolvedData(
279+
segment,
280+
promiseCodePathContext
281+
)
259282
if (alreadyResolvedData) {
260283
if (alreadyResolvedData.kind === 'certain') {
261284
newInfo.resolved = alreadyResolvedData.resolved
@@ -291,6 +314,21 @@ class CodePathSegmentInfo {
291314
}
292315
}
293316

317+
class PromiseCodePathContext {
318+
constructor() {
319+
/** @type {Set<string>} */
320+
this.resolvedSegmentIds = new Set()
321+
}
322+
/** @param {CodePathSegment} */
323+
addResolvedTryBlockCodePathSegment(segment) {
324+
this.resolvedSegmentIds.add(segment.id)
325+
}
326+
/** @param {CodePathSegment} */
327+
isResolvedTryBlockCodePathSegment(segment) {
328+
return this.resolvedSegmentIds.has(segment.id)
329+
}
330+
}
331+
294332
module.exports = {
295333
meta: {
296334
type: 'problem',
@@ -308,6 +346,7 @@ module.exports = {
308346
/** @param {import('eslint').Rule.RuleContext} context */
309347
create(context) {
310348
const reported = new Set()
349+
const promiseCodePathContext = new PromiseCodePathContext()
311350
/**
312351
* @param {Identifier} node
313352
* @param {Identifier} resolved
@@ -327,9 +366,14 @@ module.exports = {
327366
},
328367
})
329368
}
330-
/** @param {CodePathInfo} codePathInfo */
331-
function verifyMultipleResolvedPath(codePathInfo) {
332-
for (const { node, resolved, kind } of codePathInfo.iterateReports()) {
369+
/**
370+
* @param {CodePathInfo} codePathInfo
371+
* @param {PromiseCodePathContext} promiseCodePathContext
372+
*/
373+
function verifyMultipleResolvedPath(codePathInfo, promiseCodePathContext) {
374+
for (const { node, resolved, kind } of codePathInfo.iterateReports(
375+
promiseCodePathContext
376+
)) {
333377
report(node, resolved, kind)
334378
}
335379
}
@@ -338,6 +382,8 @@ module.exports = {
338382
const codePathInfoStack = []
339383
/** @type {Set<Identifier>[]} */
340384
const resolverReferencesStack = [new Set()]
385+
/** @type {ThrowableExpression | null} */
386+
let lastThrowableExpression = null
341387
return {
342388
/** @param {FunctionExpression | ArrowFunctionExpression} node */
343389
'FunctionExpression, ArrowFunctionExpression'(node) {
@@ -376,7 +422,33 @@ module.exports = {
376422
onCodePathEnd() {
377423
const codePathInfo = codePathInfoStack.shift()
378424
if (codePathInfo.resolvedCount > 1) {
379-
verifyMultipleResolvedPath(codePathInfo)
425+
verifyMultipleResolvedPath(codePathInfo, promiseCodePathContext)
426+
}
427+
},
428+
/** @param {ThrowableExpression} node */
429+
'CallExpression, MemberExpression, NewExpression, ImportExpression, YieldExpression:exit'(
430+
node
431+
) {
432+
lastThrowableExpression = node
433+
},
434+
/**
435+
* @param {CodePathSegment} segment
436+
* @param {Node} node
437+
*/
438+
onCodePathSegmentEnd(segment, node) {
439+
if (
440+
node.type === 'CatchClause' &&
441+
lastThrowableExpression &&
442+
lastThrowableExpression.type === 'CallExpression' &&
443+
node.parent.type === 'TryStatement' &&
444+
node.parent.range[0] <= lastThrowableExpression.range[0] &&
445+
lastThrowableExpression.range[1] <= node.parent.range[1]
446+
) {
447+
const resolverReferences = resolverReferencesStack[0]
448+
if (resolverReferences.has(lastThrowableExpression.callee)) {
449+
// Mark a segment if the last expression in the try block is a call to resolve.
450+
promiseCodePathContext.addResolvedTryBlockCodePathSegment(segment)
451+
}
380452
}
381453
},
382454
/** @type {Identifier} */

0 commit comments

Comments
 (0)