NormalCompletionVisitor.java

/*
 * Copyright (C) 2015-2016 Federico Tomassetti
 * Copyright (C) 2017-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */
package com.github.javaparser.symbolsolver.javaparsermodel;

import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.stmt.*;
import com.github.javaparser.ast.visitor.GenericVisitorWithDefaults;
import com.github.javaparser.resolution.UnsolvedSymbolException;
import com.github.javaparser.resolution.types.ResolvedReferenceType;
import com.github.javaparser.resolution.types.ResolvedType;
import java.util.List;
import java.util.Optional;

/**
 * When deciding into which scope pattern variables should be introduced, it is sometimes necessary to determine whether
 * a statement can complete normally {@see https://docs.oracle.com/javase/specs/jls/se22/html/jls-14.html#jls-14.22}.
 * The JLS specifies that a statement can complete normally only if it is reachable and specifies rules for what it
 * means for a statement to be reachable, but that part can be ignored in JavaParser since having unreachable code
 * results in a compilation error and is thus not supported. This means that all of the rules are implemented with the
 * assumption that provided nodes are reachable.
 *
 * An example of where this is needed is for the following rule regarding pattern variables introduced by if-statements.
 * 6.3.2.2. if Statements {@see https://docs.oracle.com/javase/specs/jls/se22/html/jls-6.html#jls-6.3.2.2}:
 *   The following rules apply to a statement if (e) S (��14.9.1):
 *     A pattern variable is introduced by if (e) S iff (i) it is introduced by e when false and (ii) S cannot complete
 *     normally.
 *
 * This means that in this example:
 * {@code
 *   if (!(x instanceof Foo f)) {
 *       return;
 *   }
 *   System.out.println(f);
 * }
 * f will be in scope for the println call since the block making up the then-block of the if statement (S in the rule
 * above) cannot complete normally (since the last statement, return, cannot complete normally).
 * But, in this example:
 * {@code
 *   if (!(x instanceof Foo f)) { }
 * }
 * f is not introduced by the if statement since the empty then-block can complete normally.
 */
public class NormalCompletionVisitor extends GenericVisitorWithDefaults<Boolean, Void> {

    private static String[] nonEnhancedSwitchTypes = new String[] {
        "java.lang.Byte", "java.lang.Character", "java.lang.Integer", "java.lang.Short", "java.lang.String"
    };

    @Override
    public Boolean defaultAction(Node n, Void unused) {
        return true;
    }

    /**
     * A break statement cannot complete normally.
     */
    @Override
    public Boolean visit(BreakStmt breakStmt, Void unused) {
        return false;
    }

    /**
     * A continue statement cannot complete normally.
     */
    @Override
    public Boolean visit(ContinueStmt continueStmt, Void unused) {
        return false;
    }

    /**
     * A return statement cannot complete normally.
     */
    @Override
    public Boolean visit(ReturnStmt returnStmt, Void unused) {
        return false;
    }

    /**
     * A throw statement cannot complete normally.
     */
    @Override
    public Boolean visit(ThrowStmt throwStmt, Void unused) {
        return false;
    }

    /**
     * A yield statement cannot complete normally.
     */
    @Override
    public Boolean visit(YieldStmt yieldStmt, Void unused) {
        return false;
    }

    /**
     * An empty block that is not a switch block can complete normally iff it is reachable.
     *
     * A non-empty block that is not a switch block can complete normally iff the last statement in it can complete
     * normally.
     *
     * The first statement in a non-empty block that is not a switch block is reachable iff the block is reachable.
     *
     * Every other statement S in a non-empty block that is not a switch block is reachable iff the statement preceding
     * S can complete normally.
     */
    @Override
    public Boolean visit(BlockStmt block, Void unused) {
        return block.getStatements().isEmpty()
                || block.getStatements().getLast().get().accept(this, unused);
    }

    /**
     * A labeled statement can complete normally if at least one of the following is true:
     * - The contained statement can complete normally.
     * - There is a reachable break statement that exits the labeled statement.
     */
    @Override
    public Boolean visit(LabeledStmt labeledStmt, Void unused) {
        if (labeledStmt.getStatement().accept(this, unused)) {
            return true;
        }

        List<BreakStmt> matchingBreaks = labeledStmt.findAll(
                BreakStmt.class,
                breakStmt -> breakStmt.getLabel().isPresent()
                        && breakStmt
                                .getLabel()
                                .get()
                                .getIdentifier()
                                .equals(labeledStmt.getLabel().getIdentifier()));

        return !matchingBreaks.isEmpty();
    }

    /**
     * An if-then statement can complete normally iff it is reachable.
     *
     * An if-then-else statement can complete normally iff the then-statement can complete normally or the
     * else-statement can complete normally.
     */
    @Override
    public Boolean visit(IfStmt ifStmt, Void unused) {
        if (!ifStmt.getElseStmt().isPresent()) {
            return true;
        }

        return ifStmt.getThenStmt().accept(this, unused)
                || ifStmt.getElseStmt().get().accept(this, unused);
    }

    /**
     * A while statement can complete normally iff at least one of the following is true:
     * - The while statement is reachable and the condition expression is not a constant expression with value true.
     * - There is a reachable break statement that exits the while statement.
     */
    @Override
    public Boolean visit(WhileStmt whileStmt, Void unused) {
        /**
         * FIXME: To implement this properly, constant expression evaluation would need to be implemented.
         *  this is specifically necessary for the first statement in the docstring.
         *
         * Simply returning true here will only cause issues where the while statement is the only statement, or the
         * last statement in a block for which it should be determined if it completes normally. For example, keeping
         * in mind the rule:
         *   A pattern variable is introduced by if (e) S iff
         *     (i) it is introduced by e when false and
         *     (ii) S cannot complete normally.
         *
         * {@code
         * while(true) {}
         * }
         * cannot complete normally since
         *   (i) the condition is a constant expression with value true and
         *   (ii) there is no break statement that exits the loop.
         *
         * Therefore, in:
         * {@code
         * if (!(x instanceof Foo f)) {
         *     while (true) {}
         * }
         * System.out.println(f);
         * }
         * the if statement should introduce the variable f into scope, so the f that is printed should be the pattern
         * variable.
         *
         * Simply returning true in this visit method would mean that {@code while (true) {}} is erroneously considered
         * to complete normally and this example will not be handled correctly.
         *
         * As long as the last statement is not the while, however, JavaParser will still handle this correctly for
         * compiling code.
         */
        return true;
    }

    /**
     * A do statement can complete normally iff at least one of the following is true:
     * - The contained statement can complete normally and the condition expression is not a constant expression
     *   with value true.
     * - The do statement contains a reachable continue statement with no label, and the do statement is the innermost
     *   while, do, or for statement that contains that continue statement, and the continue statement continues that do
     *   statement, and the condition expression is not a constant expression with value true.
     * - The do statement contains a reachable continue statement with label L, and the do statement has label L, and
     *   the continue statement continues that do statement, and the condition expression is not a constant expression
     *   with value true.
     * - There is a reachable break statement that exits the do statement.
     */
    @Override
    public Boolean visit(DoStmt doStmt, Void unused) {
        // FIXME: See comment in while visit method
        return true;
    }

    /**
     * A basic for statement can complete normally iff at least one of the following is true:
     * - The for statement is reachable, there is a condition expression, and the condition expression is not a constant
     *   expression with value true.
     * - There is a reachable break statement that exits the for statement.
     */
    @Override
    public Boolean visit(ForStmt forStmt, Void unused) {
        // FIXME: See comment in while visit method
        return true;
    }

    /**
     * A synchronized statement can complete normally iff the contained statement can complete normally.
     */
    @Override
    public Boolean visit(SynchronizedStmt synchronizedStmt, Void unused) {
        return synchronizedStmt.getBody().accept(this, unused);
    }

    /**
     * A try statement can complete normally iff both of the following are true:
     * - The try block can complete normally or any catch block can complete normally.
     * - If the try statement has a finally block, then the finally block can complete normally.
     */
    @Override
    public Boolean visit(TryStmt tryStmt, Void unused) {
        if (tryStmt.getFinallyBlock().isPresent() && !(tryStmt.getFinallyBlock().get()).accept(this, unused)) {
            return false;
        }

        if (tryStmt.getTryBlock().accept(this, unused)) {
            return true;
        }

        return tryStmt.getCatchClauses().stream()
                .anyMatch(catchClause -> catchClause.getBody().accept(this, unused));
    }

    /**
     * A switch statement whose switch block is empty, or contains only switch labels, can complete normally.
     *
     * A switch statement whose switch block consists of switch labeled statement groups can complete normally iff at
     *   least one of the following is true:
     * - The last statement in the switch block can complete normally.
     * - There is at least one switch label after the last switch block statement group.
     * - There is a reachable break statement that exits the switch statement.
     * - The switch statement is not enhanced and its switch block does not contain a default label.
     *
     * A switch statement whose switch block consists of switch rules can complete normally iff at least one of the
     *   following is true:
     * - One of the switch rules introduces a switch rule expression (which is necessarily a statement expression).
     * - One of the switch rules introduces a switch rule block that can complete normally.
     * - One of the switch rules introduces a switch rule block that contains a reachable break statement which exits
     *   the switch statement.
     * - The switch statement is not enhanced and its switch block does not contain a default label.
     */
    @Override
    public Boolean visit(SwitchStmt switchStmt, Void unused) {
        // TODO
        NodeList<SwitchEntry> entries = switchStmt.getEntries();

        if (entries.isEmpty()) {
            return true;
        }

        if (entries.get(0).getType().equals(SwitchEntry.Type.STATEMENT_GROUP)) {
            // In this case, the switch stmt is a classic switch, so check:
            // - The last statement in the switch block can complete normally.
            // - There is at least one switch label after the last switch block statement group.
            SwitchEntry lastEntry = entries.getLast().get();
            if (lastEntry.getStatements().isEmpty()) {
                return true;
            }

            Statement lastStmt = lastEntry.getStatements().getLast().get();
            if (lastStmt.accept(this, unused)) {
                return true;
            }
        } else if (entries.stream().anyMatch(entry -> switchRuleCompletesNormally(entry, unused))) {
            return true;
        }

        if (containsCorrespondingBreak(switchStmt)) {
            return true;
        }

        if (!isEnhanced(switchStmt) && !entries.stream().anyMatch(entry -> entry.isDefault())) {
            return true;
        }

        return false;
    }

    /**
     * A switch statement whose switch block consists of switch rules can complete normally iff at least one of the
     *   following is true:
     * - One of the switch rules introduces a switch rule expression (which is necessarily a statement
     *   expression).
     * - One of the switch rules introduces a switch rule block that can complete normally.
     */
    private boolean switchRuleCompletesNormally(SwitchEntry switchEntry, Void unused) {
        if (switchEntry.getType().equals(SwitchEntry.Type.EXPRESSION)) {
            return true;
        }

        if (switchEntry.getStatements().getLast().isPresent()
                && switchEntry.getStatements().getLast().get().accept(this, unused)) {
            return true;
        }

        return false;
    }

    /**
     * @param statement should be one of: SwitchStatement, WhileStatement, DoStatement, ForStatement
     * @return true if a break corresponding to the statement is found; false otherwise
     */
    public static boolean containsCorrespondingBreak(Statement statement) {
        List<BreakStmt> breakStatements = statement.findAll(
                BreakStmt.class, breakStmt -> !breakStmt.getLabel().isPresent());

        for (BreakStmt breakStmt : breakStatements) {
            Optional<Node> maybeParentNode = breakStmt.getParentNode();
            while (true) {
                if (!maybeParentNode.isPresent()) {
                    throw new IllegalStateException("Found AST node without a parent in subtree of Statement");
                }

                Node parentNode = maybeParentNode.get();

                if (parentNode instanceof SwitchStmt
                        || parentNode instanceof WhileStmt
                        || parentNode instanceof DoStmt
                        || parentNode instanceof ForStmt
                        || parentNode instanceof ForEachStmt) {
                    if (parentNode == statement) {
                        return true;
                    } else {
                        break;
                    }
                }

                maybeParentNode = maybeParentNode.flatMap(node -> node.getParentNode());
            }
        }

        return false;
    }

    /**
     * From https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.11.2
     *   An enhanced switch statement is one where either
     *   (i) the type of the selector expression is not char, byte, short, int, Character, Byte, Short, Integer, String,
     *       or an enum type, or
     *   (ii) there is a case pattern or null literal associated with the switch block.
     *
     * Since this relies on resolving the type of the selector expression, it might not be possible to determine whether
     * a given switch statement is enhanced or not. If this cannot be determined, assume that it is enhanced.
     */
    private static boolean isEnhanced(SwitchStmt stmt) {
        for (SwitchEntry entry : stmt.getEntries()) {
            if (entry.isDefault() || entry.getLabels().stream().anyMatch(label -> label.isNullLiteralExpr())) {
                return false;
            }
        }

        try {
            ResolvedType selectorType = stmt.getSelector().calculateResolvedType();

            if (selectorType.isPrimitive()) {
                return false;
            }

            for (String nonEnhancedTypeName : nonEnhancedSwitchTypes) {
                if (nonEnhancedTypeName.equals(selectorType.describe())) {
                    return false;
                }
            }

            if (selectorType.isReferenceType()) {
                ResolvedReferenceType selectorRefType = selectorType.asReferenceType();

                if (selectorRefType.getTypeDeclaration().isPresent()
                        && selectorRefType.getTypeDeclaration().get().isEnum()) {
                    return false;
                }
            }
        } catch (UnsolvedSymbolException e) {
            // Assume the switch is enhanced if the selector type cannot be resolved.
            return true;
        }

        return true;
    }
}