UNPKG

11.8 kBJavaScriptView Raw
1/**
2 * Rule: no-multiple-resolved
3 * Disallow creating new promises with paths that resolve multiple times
4 */
5
6'use strict'
7
8const getDocsUrl = require('./lib/get-docs-url')
9const {
10 isPromiseConstructorWithInlineExecutor,
11} = require('./lib/is-promise-constructor')
12
13/**
14 * @typedef {import('estree').Node} Node
15 * @typedef {import('estree').Identifier} Identifier
16 * @typedef {import('estree').FunctionExpression} FunctionExpression
17 * @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression
18 * @typedef {import('estree').SimpleCallExpression} CallExpression
19 * @typedef {import('eslint').Rule.CodePath} CodePath
20 * @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment
21 */
22
23/**
24 * Iterate all previous path segments.
25 * @param {CodePathSegment} segment
26 * @returns {Iterable<CodePathSegment[]>}
27 */
28function* iterateAllPrevPathSegments(segment) {
29 yield* iterate(segment, [])
30
31 /**
32 * @param {CodePathSegment} segment
33 * @param {CodePathSegment[]} processed
34 */
35 function* iterate(segment, processed) {
36 if (processed.includes(segment)) {
37 return
38 }
39 const nextProcessed = [segment, ...processed]
40
41 for (const prev of segment.prevSegments) {
42 if (prev.prevSegments.length === 0) {
43 yield [prev]
44 } else {
45 for (const segments of iterate(prev, nextProcessed)) {
46 yield [prev, ...segments]
47 }
48 }
49 }
50 }
51}
52/**
53 * Iterate all next path segments.
54 * @param {CodePathSegment} segment
55 * @returns {Iterable<CodePathSegment[]>}
56 */
57function* iterateAllNextPathSegments(segment) {
58 yield* iterate(segment, [])
59
60 /**
61 * @param {CodePathSegment} segment
62 * @param {CodePathSegment[]} processed
63 */
64 function* iterate(segment, processed) {
65 if (processed.includes(segment)) {
66 return
67 }
68 const nextProcessed = [segment, ...processed]
69
70 for (const next of segment.nextSegments) {
71 if (next.nextSegments.length === 0) {
72 yield [next]
73 } else {
74 for (const segments of iterate(next, nextProcessed)) {
75 yield [next, ...segments]
76 }
77 }
78 }
79 }
80}
81
82/**
83 * Finds the same route path from the given path following previous path segments.
84 * @param {CodePathSegment} segment
85 * @returns {CodePathSegment | null}
86 */
87function findSameRoutePathSegment(segment) {
88 /** @type {Set<CodePathSegment>} */
89 const routeSegments = new Set()
90 for (const route of iterateAllPrevPathSegments(segment)) {
91 if (routeSegments.size === 0) {
92 // First
93 for (const seg of route) {
94 routeSegments.add(seg)
95 }
96 continue
97 }
98 for (const seg of routeSegments) {
99 if (!route.includes(seg)) {
100 routeSegments.delete(seg)
101 }
102 }
103 }
104
105 for (const routeSegment of routeSegments) {
106 let hasUnreached = false
107 for (const segments of iterateAllNextPathSegments(routeSegment)) {
108 if (!segments.includes(segment)) {
109 // It has a route that does not reach the given path.
110 hasUnreached = true
111 break
112 }
113 }
114 if (!hasUnreached) {
115 return routeSegment
116 }
117 }
118 return null
119}
120
121class CodePathInfo {
122 /**
123 * @param {CodePath} path
124 */
125 constructor(path) {
126 this.path = path
127 /** @type {Map<CodePathSegment, CodePathSegmentInfo>} */
128 this.segmentInfos = new Map()
129 this.resolvedCount = 0
130 /** @type {CodePathSegment[]} */
131 this.allSegments = []
132 }
133
134 getCurrentSegmentInfos() {
135 return this.path.currentSegments.map((segment) => {
136 const info = this.segmentInfos.get(segment)
137 if (info) {
138 return info
139 }
140 const newInfo = new CodePathSegmentInfo(this, segment)
141 this.segmentInfos.set(segment, newInfo)
142 return newInfo
143 })
144 }
145 /**
146 * @typedef {object} AlreadyResolvedData
147 * @property {Identifier} resolved
148 * @property {'certain' | 'potential'} kind
149 */
150
151 /**
152 * Check all paths and return paths resolved multiple times.
153 * @returns {Iterable<AlreadyResolvedData & { node: Identifier }>}
154 */
155 *iterateReports() {
156 const targets = [...this.segmentInfos.values()].filter(
157 (info) => info.resolved
158 )
159 for (const segmentInfo of targets) {
160 const result = this._getAlreadyResolvedData(segmentInfo.segment)
161 if (result) {
162 yield {
163 node: segmentInfo.resolved,
164 resolved: result.resolved,
165 kind: result.kind,
166 }
167 }
168 }
169 }
170 /**
171 * Compute the previously resolved path.
172 * @param {CodePathSegment} segment
173 * @returns {AlreadyResolvedData | null}
174 */
175 _getAlreadyResolvedData(segment) {
176 if (segment.prevSegments.length === 0) {
177 return null
178 }
179 const prevSegmentInfos = segment.prevSegments.map((prev) =>
180 this._getProcessedSegmentInfo(prev)
181 )
182 if (prevSegmentInfos.every((info) => info.resolved)) {
183 // If the previous paths are all resolved, the next path is also resolved.
184 return {
185 resolved: prevSegmentInfos[0].resolved,
186 kind: 'certain',
187 }
188 }
189
190 for (const prevSegmentInfo of prevSegmentInfos) {
191 if (prevSegmentInfo.resolved) {
192 // If the previous path is partially resolved,
193 // then the next path is potentially resolved.
194 return {
195 resolved: prevSegmentInfo.resolved,
196 kind: 'potential',
197 }
198 }
199 if (prevSegmentInfo.potentiallyResolved) {
200 let potential = false
201 if (prevSegmentInfo.segment.nextSegments.length === 1) {
202 // If the previous path is potentially resolved and there is one next path,
203 // then the next path is potentially resolved.
204 potential = true
205 } else {
206 // This is necessary, for example, if `resolve()` in the finally section.
207 const segmentInfo = this.segmentInfos.get(segment)
208 if (segmentInfo && segmentInfo.resolved) {
209 if (
210 prevSegmentInfo.segment.nextSegments.every((next) => {
211 const nextSegmentInfo = this.segmentInfos.get(next)
212 return (
213 nextSegmentInfo &&
214 nextSegmentInfo.resolved === segmentInfo.resolved
215 )
216 })
217 ) {
218 // If the previous path is potentially resolved and
219 // the next paths all point to the same resolved node,
220 // then the next path is potentially resolved.
221 potential = true
222 }
223 }
224 }
225
226 if (potential) {
227 return {
228 resolved: prevSegmentInfo.potentiallyResolved,
229 kind: 'potential',
230 }
231 }
232 }
233 }
234
235 const sameRoute = findSameRoutePathSegment(segment)
236 if (sameRoute) {
237 const sameRouteSegmentInfo = this._getProcessedSegmentInfo(sameRoute)
238 if (sameRouteSegmentInfo.potentiallyResolved) {
239 return {
240 resolved: sameRouteSegmentInfo.potentiallyResolved,
241 kind: 'potential',
242 }
243 }
244 }
245 return null
246 }
247 /**
248 * @param {CodePathSegment} segment
249 */
250 _getProcessedSegmentInfo(segment) {
251 const segmentInfo = this.segmentInfos.get(segment)
252 if (segmentInfo) {
253 return segmentInfo
254 }
255 const newInfo = new CodePathSegmentInfo(this, segment)
256 this.segmentInfos.set(segment, newInfo)
257
258 const alreadyResolvedData = this._getAlreadyResolvedData(segment)
259 if (alreadyResolvedData) {
260 if (alreadyResolvedData.kind === 'certain') {
261 newInfo.resolved = alreadyResolvedData.resolved
262 } else {
263 newInfo.potentiallyResolved = alreadyResolvedData.resolved
264 }
265 }
266 return newInfo
267 }
268}
269
270class CodePathSegmentInfo {
271 /**
272 * @param {CodePathInfo} pathInfo
273 * @param {CodePathSegment} segment
274 */
275 constructor(pathInfo, segment) {
276 this.pathInfo = pathInfo
277 this.segment = segment
278 /** @type {Identifier | null} */
279 this._resolved = null
280 /** @type {Identifier | null} */
281 this.potentiallyResolved = null
282 }
283
284 get resolved() {
285 return this._resolved
286 }
287 /** @type {Identifier} */
288 set resolved(identifier) {
289 this._resolved = identifier
290 this.pathInfo.resolvedCount++
291 }
292}
293
294module.exports = {
295 meta: {
296 type: 'problem',
297 docs: {
298 url: getDocsUrl('no-multiple-resolved'),
299 },
300 messages: {
301 alreadyResolved:
302 'Promise should not be resolved multiple times. Promise is already resolved on line {{line}}.',
303 potentiallyAlreadyResolved:
304 'Promise should not be resolved multiple times. Promise is potentially resolved on line {{line}}.',
305 },
306 schema: [],
307 },
308 /** @param {import('eslint').Rule.RuleContext} context */
309 create(context) {
310 const reported = new Set()
311 /**
312 * @param {Identifier} node
313 * @param {Identifier} resolved
314 * @param {'certain' | 'potential'} kind
315 */
316 function report(node, resolved, kind) {
317 if (reported.has(node)) {
318 return
319 }
320 reported.add(node)
321 context.report({
322 node: node.parent,
323 messageId:
324 kind === 'certain' ? 'alreadyResolved' : 'potentiallyAlreadyResolved',
325 data: {
326 line: resolved.loc.start.line,
327 },
328 })
329 }
330 /** @param {CodePathInfo} codePathInfo */
331 function verifyMultipleResolvedPath(codePathInfo) {
332 for (const { node, resolved, kind } of codePathInfo.iterateReports()) {
333 report(node, resolved, kind)
334 }
335 }
336
337 /** @type {CodePathInfo[]} */
338 const codePathInfoStack = []
339 /** @type {Set<Identifier>[]} */
340 const resolverReferencesStack = [new Set()]
341 return {
342 /** @param {FunctionExpression | ArrowFunctionExpression} node */
343 'FunctionExpression, ArrowFunctionExpression'(node) {
344 if (!isPromiseConstructorWithInlineExecutor(node.parent)) {
345 return
346 }
347 // Collect and stack `resolve` and `reject` references.
348 /** @type {Set<Identifier>} */
349 const resolverReferences = new Set()
350 const resolvers = node.params.filter(
351 /** @returns {node is Identifier} */
352 (node) => node && node.type === 'Identifier'
353 )
354 for (const resolver of resolvers) {
355 const variable = context.getScope().set.get(resolver.name)
356 // istanbul ignore next -- Usually always present.
357 if (!variable) continue
358 for (const reference of variable.references) {
359 resolverReferences.add(reference.identifier)
360 }
361 }
362
363 resolverReferencesStack.unshift(resolverReferences)
364 },
365 /** @param {FunctionExpression | ArrowFunctionExpression} node */
366 'FunctionExpression, ArrowFunctionExpression:exit'(node) {
367 if (!isPromiseConstructorWithInlineExecutor(node.parent)) {
368 return
369 }
370 resolverReferencesStack.shift()
371 },
372 /** @param {CodePath} path */
373 onCodePathStart(path) {
374 codePathInfoStack.unshift(new CodePathInfo(path))
375 },
376 onCodePathEnd() {
377 const codePathInfo = codePathInfoStack.shift()
378 if (codePathInfo.resolvedCount > 1) {
379 verifyMultipleResolvedPath(codePathInfo)
380 }
381 },
382 /** @type {Identifier} */
383 'CallExpression > Identifier.callee'(node) {
384 const codePathInfo = codePathInfoStack[0]
385 const resolverReferences = resolverReferencesStack[0]
386 if (!resolverReferences.has(node)) {
387 return
388 }
389 for (const segmentInfo of codePathInfo.getCurrentSegmentInfos()) {
390 // If a resolving path is found, report if the path is already resolved.
391 // Store the information if it is not already resolved.
392 if (segmentInfo.resolved) {
393 report(node, segmentInfo.resolved, 'certain')
394 continue
395 }
396 segmentInfo.resolved = node
397 }
398 },
399 }
400 },
401}