no-multiple-resolved.js 14 KB

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