docs: Optional<Fix> deleteFix(TryTree tree, ImmutableList<CatchTree> catchBlocks, VisitorState state) {
      SuggestedFix.Builder fix = SuggestedFix.builder();
      if (tree.getFinallyBlock() != null || catchBlocks.size() < tree.getCatches().size()) {
        // If the try statement has a finally region, or other catch blocks, delete only the
        // unnecessary blocks.
        catchBlocks.forEach(fix::delete);
      } else {
        // The try statement has no finally region and all catch blocks are unnecessary. Replace it
        // with the try statements, deleting all catches.
        List<? extends StatementTree> tryStatements = tree.getBlock().getStatements();
  
        // If the try block is empty, all of the catches are dead, so just delete the whole try and
        // don't modify the signature of the method
        if (tryStatements.isEmpty()) {
          return Optional.of(fix.delete(tree).build());
        } else {
          String source = state.getSourceCode().toString();
          // Replace the full region to work around a GJF partial formatting bug that prevents it from
          // re-indenting unchanged lines. This means that fixes may overlap, but that's (hopefully)
          // unlikely.
          // TODO(b/24140798): emit more precise replacements if GJF is fixed
          fix.replace(
              tree,
              source.substring(
                  ((JCTree) tryStatements.get(0)).getStartPosition(),
                  state.getEndPosition(Iterables.getLast(tryStatements))));
        }
      }
      MethodTree enclosing = findEnclosing(state.getPath());
      if (enclosing == null) {
        // There isn't an enclosing method, possibly because we're in a lambda or initializer block.
        return Optional.empty();
      }
      if (isExpectedExceptionTest(ASTHelpers.getSymbol(enclosing), state)) {
        // Replacing the original exception with fail() may break badly-structured expected-exception
        // tests, so don't use that fix for methods annotated with @Test(expected=...).
        return Optional.empty();
      }
  
      // Fix up the enclosing method's throws declaration to include the new thrown exception types.
      Collection<Type> thrownTypes = ASTHelpers.getSymbol(enclosing).getThrownTypes();
      Types types = state.getTypes();
      // Find all types in the deleted catch blocks that are not already in the throws declaration.
      ImmutableList<Type> toThrow =
          catchBlocks.stream()
              .map(c -> ASTHelpers.getType(c.getParameter()))
              // convert multi-catch to a list of component types
              .flatMap(
                  t ->
                      t instanceof UnionClassType
                          ? ImmutableList.copyOf(((UnionClassType) t).getAlternativeTypes()).stream()
                          : Stream.of(t))
              .filter(t -> thrownTypes.stream().noneMatch(x -> types.isAssignable(t, x)))
              .collect(toImmutableList());
      if (!toThrow.isEmpty()) {
        if (!JUnitMatchers.TEST_CASE.matches(enclosing, state)) {
          // Don't add throws declarations to methods that don't look like test cases, since it may
          // not be a safe local refactoring.
          return Optional.empty();
        }
        String throwsString =
            toThrow.stream()
                .map(t -> SuggestedFixes.qualifyType(state, fix, t))
                .distinct()
                .collect(joining(", "));
        if (enclosing.getThrows().isEmpty()) {
          // Add a new throws declaration.
          fix.prefixWith(enclosing.getBody(), "throws " + throwsString);
        } else {
          // Append to an existing throws declaration.
          fix.postfixWith(Iterables.getLast(enclosing.getThrows()), ", " + throwsString);
        }
      }
      return Optional.of(fix.build());
    }