@@ -422,25 +422,36 @@ struct PathState<'cfg> {
422422 next_block : BlockId ,
423423 /// Set of all blocks already visited on this path
424424 visited : RoaringBitmap ,
425- /// Current terminating instruction for the path, if one was
426- /// encountered
427- terminator : Option < Option < PathTerminator > > ,
425+ /// Current terminating instruction for the path, if one was encountered.
426+ /// See [`Terminator`] for the semantics of each `Option` level.
427+ terminator : Option < Terminator > ,
428428 exception_handlers : Option < & ' cfg [ ExceptionHandler ] > ,
429+ /// The terminator that was active before entering a `finally` block via a
430+ /// dead implicit jump (i.e. when `break`/`continue`/`return`/`throw`
431+ /// precedes the implicit try-body-to-finally fall-through).
432+ ///
433+ /// `None` means we are not currently inside such a finally block.
434+ /// `Some(t)` stores the `Option<Terminator>` that was in `self.terminator`
435+ /// when the finally block was entered; it is restored by `finally_fallthrough`
436+ /// so that code *after* the `try/finally` construct is still correctly seen
437+ /// as unreachable.
438+ pre_finally_terminator : Option < Option < Terminator > > ,
429439}
430440
431441/// Perform a simple reachability analysis on the control flow graph by
432442/// traversing the function starting at the entry points
433443fn traverse_cfg (
434444 cfg : & JsControlFlowGraph ,
435445 signals : & mut UnreachableRanges ,
436- ) -> FxHashMap < BlockId , Vec < Option < Option < PathTerminator > > > > {
446+ ) -> FxHashMap < BlockId , Vec < Option < Terminator > > > {
437447 let mut queue = VecDeque :: new ( ) ;
438448
439449 queue. push_back ( PathState {
440450 next_block : ROOT_BLOCK_ID ,
441451 visited : RoaringBitmap :: new ( ) ,
442452 terminator : None ,
443453 exception_handlers : None ,
454+ pre_finally_terminator : None ,
444455 } ) ;
445456
446457 // This maps holds a list of "path state", the active terminator
@@ -480,6 +491,7 @@ fn traverse_cfg(
480491 visited : path. visited . clone ( ) ,
481492 terminator : path. terminator ,
482493 exception_handlers : find_catch_handlers ( handlers) ,
494+ pre_finally_terminator : None ,
483495 } ) ;
484496 }
485497 }
@@ -495,10 +507,34 @@ fn traverse_cfg(
495507 InstructionKind :: Statement => { }
496508 InstructionKind :: Jump {
497509 conditional,
498- block,
510+ block : jump_target ,
499511 finally_fallthrough,
500512 } => {
501- handle_jump ( & mut queue, & path, block, finally_fallthrough) ;
513+ // If we are in dead code (has_direct_terminator) but the jump
514+ // target is a cleanup (finally) handler for this block, the finally
515+ // block is still reachable — it always runs before any early exit
516+ // (break/continue/return/throw). Clear the terminator so the finally
517+ // block is not incorrectly reported as unreachable. Store the
518+ // original terminator in `pre_finally_terminator` so it can be
519+ // restored when the finally block exits via `finally_fallthrough`.
520+ let ( effective_terminator, pre_finally_terminator) = if has_direct_terminator
521+ && block
522+ . cleanup_handlers
523+ . iter ( )
524+ . any ( |h| h. target == jump_target)
525+ {
526+ ( None , Some ( path. terminator ) )
527+ } else {
528+ ( path. terminator , None )
529+ } ;
530+ handle_jump (
531+ & mut queue,
532+ & path,
533+ jump_target,
534+ finally_fallthrough,
535+ effective_terminator,
536+ pre_finally_terminator,
537+ ) ;
502538
503539 // Jump is a terminator instruction if it's unconditional
504540 if path. terminator . is_none ( ) && !conditional {
@@ -565,12 +601,20 @@ fn find_catch_handlers(handlers: &[ExceptionHandler]) -> Option<&[ExceptionHandl
565601 }
566602}
567603
568- /// Create an additional visitor path from a jump instruction and push it to the queue
604+ /// Create an additional visitor path from a jump instruction and push it to the queue.
605+ /// `effective_terminator` is the terminator to propagate to the target block; callers may
606+ /// pass `None` when the target is known to be reachable regardless of any prior terminator
607+ /// (e.g. an implicit jump from the end of a try body to its finally block).
608+ /// `pre_finally_terminator` should be set when `effective_terminator` was cleared to allow
609+ /// a finally block to appear reachable; it stores the original terminator so it can be
610+ /// restored when the finally block exits via `finally_fallthrough`.
569611fn handle_jump < ' cfg > (
570612 queue : & mut VecDeque < PathState < ' cfg > > ,
571613 path : & PathState < ' cfg > ,
572614 block : BlockId ,
573615 finally_fallthrough : bool ,
616+ effective_terminator : Option < Terminator > ,
617+ pre_finally_terminator : Option < Option < Terminator > > ,
574618) {
575619 // If this jump is exiting a finally clause and this path is visiting
576620 // an exception handlers chain
@@ -587,16 +631,45 @@ fn handle_jump<'cfg>(
587631 visited : path. visited . clone ( ) ,
588632 terminator : path. terminator ,
589633 exception_handlers : Some ( handlers) ,
634+ pre_finally_terminator : None ,
635+ } ) ;
636+ }
637+ } else if finally_fallthrough {
638+ // Exiting a finally block in the normal (non-exception) context.
639+ // Determine the terminator to propagate past the try/finally:
640+ // - If the finally block itself terminates (e.g. contains a `return`
641+ // or `throw`), prefer that as the most proximate cause.
642+ // - Otherwise, if we entered finally via a dead implicit jump (the
643+ // try body had `break`/`continue`/`return`), restore the saved
644+ // pre-finally terminator so code after the try/finally is still
645+ // correctly seen as unreachable.
646+ let continuation_terminator = path
647+ . terminator
648+ . or ( path. pre_finally_terminator . unwrap_or ( None ) ) ;
649+
650+ if !path. visited . contains ( block. index ( ) ) {
651+ queue. push_back ( PathState {
652+ next_block : block,
653+ visited : path. visited . clone ( ) ,
654+ terminator : continuation_terminator,
655+ exception_handlers : path. exception_handlers ,
656+ pre_finally_terminator : None ,
590657 } ) ;
591658 }
592659 } else if !path. visited . contains ( block. index ( ) ) {
593660 // Push the jump target block to the queue if it hasn't
594- // been visited yet in this path
661+ // been visited yet in this path.
662+ // Propagate `pre_finally_terminator` from the current path so it
663+ // survives across internal jumps inside a finally block (e.g. an
664+ // if/else inside finally) and is available when the block exits via
665+ // `finally_fallthrough`.
666+ let effective_pre_finally = pre_finally_terminator. or ( path. pre_finally_terminator ) ;
595667 queue. push_back ( PathState {
596668 next_block : block,
597669 visited : path. visited . clone ( ) ,
598- terminator : path . terminator ,
670+ terminator : effective_terminator ,
599671 exception_handlers : path. exception_handlers ,
672+ pre_finally_terminator : effective_pre_finally,
600673 } ) ;
601674 }
602675}
@@ -616,6 +689,7 @@ fn handle_return<'cfg>(
616689 visited : path. visited . clone ( ) ,
617690 terminator : path. terminator ,
618691 exception_handlers : Some ( handlers) ,
692+ pre_finally_terminator : None ,
619693 } ) ;
620694 }
621695}
@@ -967,6 +1041,16 @@ pub struct UnreachableRange {
9671041 terminators : Vec < PathTerminator > ,
9681042}
9691043
1044+ /// The terminator state of a path through the control flow graph.
1045+ ///
1046+ /// Each level of `Option` carries distinct semantics:
1047+ /// - Outer `Option`: `None` = the path has no terminator yet (code is still live);
1048+ /// `Some(_)` = a terminating instruction has been encountered on this path.
1049+ /// - Inner `Option<PathTerminator>`: `None` = the terminator has no associated
1050+ /// syntax node (e.g. an implicit jump); `Some(pt)` = the terminator corresponds
1051+ /// to a concrete syntax node that can be pointed to in a diagnostic.
1052+ type Terminator = Option < PathTerminator > ;
1053+
9701054#[ derive( Debug , Clone , Copy ) ]
9711055struct PathTerminator {
9721056 kind : JsSyntaxKind ,
0 commit comments