/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.devtools.j2objc.translate;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.devtools.j2objc.ast.AssertStatement;
import com.google.devtools.j2objc.ast.Assignment;
import com.google.devtools.j2objc.ast.Block;
import com.google.devtools.j2objc.ast.BooleanLiteral;
import com.google.devtools.j2objc.ast.BreakStatement;
import com.google.devtools.j2objc.ast.CommaExpression;
import com.google.devtools.j2objc.ast.CompilationUnit;
import com.google.devtools.j2objc.ast.ConditionalExpression;
import com.google.devtools.j2objc.ast.ConstructorInvocation;
import com.google.devtools.j2objc.ast.DoStatement;
import com.google.devtools.j2objc.ast.EnhancedForStatement;
import com.google.devtools.j2objc.ast.Expression;
import com.google.devtools.j2objc.ast.ExpressionStatement;
import com.google.devtools.j2objc.ast.ForStatement;
import com.google.devtools.j2objc.ast.IfStatement;
import com.google.devtools.j2objc.ast.InfixExpression;
import com.google.devtools.j2objc.ast.MethodDeclaration;
import com.google.devtools.j2objc.ast.ParenthesizedExpression;
import com.google.devtools.j2objc.ast.PostfixExpression;
import com.google.devtools.j2objc.ast.PrefixExpression;
import com.google.devtools.j2objc.ast.ReturnStatement;
import com.google.devtools.j2objc.ast.SimpleName;
import com.google.devtools.j2objc.ast.Statement;
import com.google.devtools.j2objc.ast.SuperConstructorInvocation;
import com.google.devtools.j2objc.ast.SwitchStatement;
import com.google.devtools.j2objc.ast.SynchronizedStatement;
import com.google.devtools.j2objc.ast.ThrowStatement;
import com.google.devtools.j2objc.ast.TreeNode;
import com.google.devtools.j2objc.ast.TreeUtil;
import com.google.devtools.j2objc.ast.UnitTreeVisitor;
import com.google.devtools.j2objc.ast.VariableDeclarationExpression;
import com.google.devtools.j2objc.ast.VariableDeclarationFragment;
import com.google.devtools.j2objc.ast.VariableDeclarationStatement;
import com.google.devtools.j2objc.ast.WhileStatement;
import com.google.devtools.j2objc.types.GeneratedVariableElement;
import com.google.devtools.j2objc.util.ElementUtil;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

/**
 * Detects unsequenced modifications which are errors in ObjC and extracts
 * portions of the expression to eliminate the errors.
 *
 * This pass must occur after rewriting of labeled break and continue statements
 * otherwise extracted loop conditions will be out of order with the labels
 * inserted by the labeled break and continue rewriting.
 *
 * @author Keith Stanger
 */
public class UnsequencedExpressionRewriter extends UnitTreeVisitor {

  private ExecutableElement currentMethod = null;
  private int count = 1;
  private List<VariableAccess> orderedAccesses = Lists.newArrayList();
  private TreeNode currentTopNode = null;
  private boolean hasModification = false;

  public UnsequencedExpressionRewriter(CompilationUnit unit) {
    super(unit);
  }

  /**
   * Metadata for a given read or write access of a variable within an
   * expression.
   */
  private static class VariableAccess {

    private final VariableElement variable;
    private final Expression expression;
    private final boolean isModification;

    private VariableAccess(
        VariableElement variable, Expression expression, boolean isModification) {
      this.variable = variable;
      this.expression = expression;
      this.isModification = isModification;
    }
  }

  private void addVariableAccess(VariableElement var, Expression node, boolean isModification) {
    if (var != null && !ElementUtil.isInstanceVar(var)) {
      hasModification |= isModification;
      orderedAccesses.add(new VariableAccess(var, node, isModification));
    }
  }

  private void newExpression(TreeNode topNode) {
    orderedAccesses.clear();
    currentTopNode = topNode;
    hasModification = false;
  }

  @Override
  public boolean visit(MethodDeclaration node) {
    currentMethod = node.getExecutableElement();
    count = 1;
    return true;
  }

  @Override
  public void endVisit(MethodDeclaration node) {
    currentMethod = null;
  }

  private ImmutableList<VariableAccess> getUnsequencedAccesses() {
    if (!hasModification) {
      return ImmutableList.of();
    }
    ListMultimap<VariableElement, VariableAccess> accessesByVar =
        MultimapBuilder.hashKeys().arrayListValues().build();
    for (VariableAccess access : orderedAccesses) {
      accessesByVar.put(access.variable, access);
    }
    Set<VariableAccess> unsequencedAccesses = Sets.newHashSet();
    for (VariableElement var : accessesByVar.keySet()) {
      findUnsequenced(accessesByVar.get(var), unsequencedAccesses);
    }
    List<VariableAccess> orderedUnsequencedAccesses =
        Lists.newArrayListWithCapacity(unsequencedAccesses.size());
    for (VariableAccess access : orderedAccesses) {
      if (unsequencedAccesses.contains(access)) {
        orderedUnsequencedAccesses.add(access);
      }
    }
    return ImmutableList.copyOf(orderedUnsequencedAccesses);
  }

  private void extractUnsequenced(Statement stmt) {
    List<VariableAccess> accesses = getUnsequencedAccesses();
    if (!accesses.isEmpty()) {
      extractOrderedAccesses(
          TreeUtil.asStatementList(stmt).subList(0, 0), currentTopNode, accesses);
    }
  }

  private void visitAndExtract(Expression expr, Statement stmt) {
    if (expr != null) {
      newExpression(expr);
      expr.accept(this);
      extractUnsequenced(stmt);
    }
  }

  private void extractOrderedAccesses(
      List<Statement> stmtList, TreeNode subExpr, List<VariableAccess> toExtract) {
    for (int i = 0; i < toExtract.size(); i++) {
      VariableAccess access = toExtract.get(i);
      TreeNode topConditional = getTopConditional(access.expression, subExpr);
      if (topConditional != null) {
        // Conditional expressions require special handling when extracting the
        // access because execution of the access may not be guaranteed.
        // Here we collect all accesses that are descendant of the conditional
        // expression and pass them to an appropriate extraction method.
        int j = i + 1;
        for (; j < toExtract.size(); j++) {
          if (getTopConditional(toExtract.get(j).expression, subExpr) != topConditional) {
            break;
          }
        }
        if (topConditional instanceof InfixExpression) {
          extractInfixConditional(
              stmtList, (InfixExpression) topConditional, toExtract.subList(i, j));
        } else if (topConditional instanceof ConditionalExpression) {
          extractConditionalExpression(
              stmtList, (ConditionalExpression) topConditional, toExtract.subList(i, j));
        } else {
          throw new AssertionError(
              "Unexpected conditional node type: " + topConditional.getClass().toString());
        }
        i = j - 1;
      } else {
        VariableElement newVar = GeneratedVariableElement.newLocalVar(
            "unseq$" + count++, access.expression.getTypeMirror(), currentMethod);
        stmtList.add(new VariableDeclarationStatement(newVar, access.expression.copy()));
        access.expression.replaceWith(new SimpleName(newVar));
      }
    }
  }

  private TreeNode getTopConditional(TreeNode node, TreeNode limit) {
    TreeNode topConditional = null;
    while (node != limit) {
      node = node.getParent();
      if (isConditional(node)) {
        topConditional = node;
      }
    }
    return topConditional;
  }

  private void extractConditionalExpression(
      List<Statement> stmtList, ConditionalExpression conditional, List<VariableAccess> toExtract) {
    Expression condition = conditional.getExpression();
    Expression thenExpr = conditional.getThenExpression();
    Expression elseExpr = conditional.getElseExpression();
    List<VariableAccess> conditionAccesses = Lists.newArrayList();
    List<VariableAccess> thenAccesses = Lists.newArrayList();
    List<VariableAccess> elseAccesses = Lists.newArrayList();
    boolean needsExtraction = false;
    for (VariableAccess access : toExtract) {
      TreeNode node = access.expression;
      while (node.getParent() != conditional) {
        node = node.getParent();
      }
      if (node == condition) {
        conditionAccesses.add(access);
      } else if (node == thenExpr) {
        thenAccesses.add(access);
      } else if (node == elseExpr) {
        elseAccesses.add(access);
      } else {
        throw new AssertionError();
      }
      if (node != condition) {
        // We need to extract an if-statement if there is an access that
        // executes conditionally.
        needsExtraction = true;
      }
    }
    extractOrderedAccesses(stmtList, condition, conditionAccesses);
    // The recursive call might replace the condition child.
    condition = conditional.getExpression();
    if (needsExtraction) {
      VariableElement resultVar = GeneratedVariableElement.newLocalVar(
          "unseq$" + count++, conditional.getTypeMirror(), currentMethod);
      conditional.replaceWith(new SimpleName(resultVar));
      stmtList.add(new VariableDeclarationStatement(resultVar, null));
      IfStatement newIf = new IfStatement();
      newIf.setExpression(condition.copy());
      stmtList.add(newIf);
      Block thenBlock = new Block();
      newIf.setThenStatement(thenBlock);
      List<Statement> thenStmts = thenBlock.getStatements();
      extractOrderedAccesses(thenStmts, thenExpr, thenAccesses);
      // The recursive call might replace the then expression child.
      thenExpr = conditional.getThenExpression();
      thenStmts.add(new ExpressionStatement(
          new Assignment(new SimpleName(resultVar), thenExpr.copy())));
      Block elseBlock = new Block();
      newIf.setElseStatement(elseBlock);
      List<Statement> elseStmts = elseBlock.getStatements();
      extractOrderedAccesses(elseStmts, elseExpr, elseAccesses);
      // The recursive call might replace the else expression child.
      elseExpr = conditional.getElseExpression();
      elseStmts.add(new ExpressionStatement(
          new Assignment(new SimpleName(resultVar), elseExpr.copy())));
    } else {
      extractOrderedAccesses(stmtList, thenExpr, thenAccesses);
      extractOrderedAccesses(stmtList, elseExpr, elseAccesses);
    }
  }

  private void extractInfixConditional(
      List<Statement> stmtList, InfixExpression conditional, List<VariableAccess> toExtract) {
    InfixExpression.Operator op = conditional.getOperator();
    List<Expression> branches = conditional.getOperands();
    int lastIfExtractIdx = 0;
    VariableElement conditionalVar = null;
    int lastExtracted = 0;
    Expression lastBranch = null;
    for (int i = 0; i < toExtract.size(); i++) {
      VariableAccess access = toExtract.get(i);
      TreeNode node = access.expression;
      while (node.getParent() != conditional) {
        node = node.getParent();
      }
      assert node instanceof Expression;
      Expression branch = (Expression) node;

      // Extract all accesses from the previous branch.
      if (lastBranch != null && branch != lastBranch) {
        extractOrderedAccesses(stmtList, lastBranch, toExtract.subList(lastExtracted, i));
        lastExtracted = i;
      }
      lastBranch = branch;

      // If there's a new access in a new branch, then we extract an if-statement.
      if (branch != branches.get(lastIfExtractIdx)) {
        TypeMirror boolType = typeUtil.getBoolean();
        if (conditionalVar == null) {
          conditionalVar = GeneratedVariableElement.newLocalVar(
              "unseq$" + count++, boolType, currentMethod);
          conditional.replaceWith(new SimpleName(conditionalVar));
          stmtList.add(new VariableDeclarationStatement(conditionalVar, null));
        }
        List<Expression> subBranches = branches.subList(lastIfExtractIdx, branches.indexOf(branch));
        IfStatement newIf = new IfStatement();
        Expression ifExpr = new Assignment(
            new SimpleName(conditionalVar), conditionalFromSubBranches(subBranches, op));
        if (op == InfixExpression.Operator.CONDITIONAL_OR) {
          ifExpr = new PrefixExpression(
              boolType, PrefixExpression.Operator.NOT,
              ParenthesizedExpression.parenthesize(ifExpr));
        }
        newIf.setExpression(ifExpr);
        stmtList.add(newIf);
        Block thenBlock = new Block();
        stmtList = thenBlock.getStatements();
        newIf.setThenStatement(thenBlock);
        lastIfExtractIdx = branches.indexOf(branch);
      }
    }
    extractOrderedAccesses(
        stmtList, lastBranch, toExtract.subList(lastExtracted, toExtract.size()));
    if (conditionalVar != null) {
      List<Expression> remainingBranches = Lists.newArrayList();
      remainingBranches.add(new SimpleName(conditionalVar));
      remainingBranches.addAll(branches.subList(lastIfExtractIdx, branches.size()));
      stmtList.add(new ExpressionStatement(new Assignment(
          new SimpleName(conditionalVar), conditionalFromSubBranches(remainingBranches, op))));
    }
  }

  private Expression conditionalFromSubBranches(
      List<Expression> branches, InfixExpression.Operator op) {
    assert branches.size() >= 1;
    if (branches.size() == 1) {
      return branches.get(0).copy();
    } else {
      InfixExpression result = new InfixExpression(typeUtil.getBoolean(), op);
      TreeUtil.copyList(branches, result.getOperands());
      return result;
    }
  }

  private boolean isConditional(TreeNode node) {
    if (node instanceof InfixExpression) {
      InfixExpression infixExpr = (InfixExpression) node;
      InfixExpression.Operator op = infixExpr.getOperator();
      if (op == InfixExpression.Operator.CONDITIONAL_AND
          || op == InfixExpression.Operator.CONDITIONAL_OR) {
        return true;
      }
    } else if (node instanceof ConditionalExpression) {
      return true;
    }
    return false;
  }

  private void findUnsequenced(List<VariableAccess> accesses, Set<VariableAccess> toExtract) {
    // No conflicts with only one access.
    if (accesses.size() == 1) {
      return;
    }
    Set<VariableAccess> modifications = Sets.newHashSet();
    for (VariableAccess access : accesses) {
      if (access.isModification) {
        modifications.add(access);
      }
    }
    for (VariableAccess modification : modifications) {
      Set<TreeNode> ancestors = getAncestors(modification.expression);
      boolean seenMod = false;
      for (VariableAccess access : accesses) {
        if (modification == access) {
          seenMod = true;
        } else if (isUnsequenced(modification, ancestors, access)) {
          // Only need to extract the first access.
          toExtract.add(seenMod ? modification : access);
        }
      }
    }
  }

  private Set<TreeNode> getAncestors(TreeNode node) {
    Set<TreeNode> ancestors = Sets.newHashSet();
    while (node != currentTopNode) {
      ancestors.add(node);
      node = node.getParent();
    }
    return ancestors;
  }

  private boolean isUnsequenced(
      VariableAccess modification, Set<TreeNode> modificationAncestors, VariableAccess access) {
    TreeNode commonAncestor = currentTopNode;
    TreeNode node = access.expression;
    while (node != currentTopNode) {
      if (modificationAncestors.contains(node)) {
        commonAncestor = node;
        break;
      }
      node = node.getParent();
    }
    // If either access is executed in a conditional branch that does not
    // contain the other access, then they are not unsequenced.
    if (isConditional(commonAncestor)
        && (isWithinConditionalBranch(modification.expression, commonAncestor)
            || isWithinConditionalBranch(access.expression, commonAncestor))) {
      return false;
    } else if (commonAncestor instanceof CommaExpression) {
      return false;
    } else if (commonAncestor instanceof Assignment && modification.expression == commonAncestor) {
      // "i = 1 + (i = 2);" is not unsequenced.
      // "i = 1 + i++;" is unsequenced (according to clang).
      return access.expression instanceof PrefixExpression
          || access.expression instanceof PostfixExpression;
    }
    return true;
  }

  private boolean isWithinConditionalBranch(TreeNode node, TreeNode limit) {
    while (node != limit) {
      TreeNode parent = node.getParent();
      if (isConditional(parent) && getConditionChild(parent) != node) {
        return true;
      }
      node = parent;
    }
    return false;
  }

  private Expression getConditionChild(TreeNode conditional) {
    if (conditional instanceof InfixExpression) {
      return ((InfixExpression) conditional).getOperand(0);
    } else if (conditional instanceof ConditionalExpression) {
      return ((ConditionalExpression) conditional).getExpression();
    } else {
      throw new AssertionError(
          "Unexpected conditional node type: " + conditional.getClass().toString());
    }
  }

  @Override
  public void endVisit(SimpleName node) {
    addVariableAccess(TreeUtil.getVariableElement(node), node, false);
  }

  @Override
  public boolean visit(ExpressionStatement node) {
    visitAndExtract(node.getExpression(), node);
    return false;
  }

  @Override
  public boolean visit(ReturnStatement node) {
    visitAndExtract(node.getExpression(), node);
    return false;
  }

  @Override
  public boolean visit(AssertStatement node) {
    Expression expr = node.getExpression();
    visitAndExtract(expr, node);
    Expression msg = node.getMessage();
    if (msg != null) {
      newExpression(msg);
      msg.accept(this);
      List<VariableAccess> toExtract = getUnsequencedAccesses();
      if (!toExtract.isEmpty()) {
        // If the message expression needs any extraction, then we first extract
        // the entire boolean expression to preserve ordering between the two.
        VariableElement exprVar = GeneratedVariableElement.newLocalVar(
            "unseq$" + count++, expr.getTypeMirror(), currentMethod);
        TreeUtil.insertBefore(node, new VariableDeclarationStatement(
            exprVar, node.getExpression().copy()));
        node.setExpression(new SimpleName(exprVar));
        extractOrderedAccesses(
            TreeUtil.asStatementList(node).subList(0, 0), currentTopNode, toExtract);
      }
    }
    return false;
  }

  @Override
  public boolean visit(ConstructorInvocation node) {
    newExpression(node);
    for (Expression arg : node.getArguments()) {
      arg.accept(this);
    }
    extractUnsequenced(node);
    return false;
  }

  @Override
  public boolean visit(SuperConstructorInvocation node) {
    newExpression(node);
    for (Expression arg : node.getArguments()) {
      arg.accept(this);
    }
    extractUnsequenced(node);
    return false;
  }

  @Override
  public boolean visit(EnhancedForStatement node) {
    visitAndExtract(node.getExpression(), node);
    node.getBody().accept(this);
    return false;
  }

  @Override
  public boolean visit(VariableDeclarationStatement node) {
    extractVariableDeclarationFragments(
        node.getFragments(), TreeUtil.asStatementList(node).subList(0, 0));
    return false;
  }

  @Override
  public boolean visit(IfStatement node) {
    visitAndExtract(node.getExpression(), node);
    node.getThenStatement().accept(this);
    Statement elseStmt = node.getElseStatement();
    if (elseStmt != null) {
      elseStmt.accept(this);
    }
    return false;
  }

  @Override
  public boolean visit(SwitchStatement node) {
    visitAndExtract(node.getExpression(), node);
    for (Statement stmt : node.getStatements()) {
      stmt.accept(this);
    }
    return false;
  }

  @Override
  public boolean visit(SynchronizedStatement node) {
    visitAndExtract(node.getExpression(), node);
    node.getBody().accept(this);
    return false;
  }

  @Override
  public boolean visit(ThrowStatement node) {
    visitAndExtract(node.getExpression(), node);
    return false;
  }

  private IfStatement createLoopTermination(Expression loopCondition) {
    IfStatement newIf = new IfStatement();
    newIf.setExpression(new PrefixExpression(
        typeUtil.getBoolean(), PrefixExpression.Operator.NOT,
        ParenthesizedExpression.parenthesize(loopCondition.copy())));
    newIf.setThenStatement(new BreakStatement());
    return newIf;
  }

  @Override
  public boolean visit(WhileStatement node) {
    node.getBody().accept(this);
    newExpression(node.getExpression());
    node.getExpression().accept(this);
    List<VariableAccess> toExtract = getUnsequencedAccesses();
    if (!toExtract.isEmpty()) {
      // Convert "while (cond)" into "while (true) { if (!(cond)) break; ... }".
      List<Statement> stmtList = TreeUtil.asStatementList(node.getBody()).subList(0, 0);
      extractOrderedAccesses(stmtList, currentTopNode, toExtract);
      stmtList.add(createLoopTermination(node.getExpression()));
      node.setExpression(new BooleanLiteral(true, typeUtil));
    }
    return false;
  }

  @Override
  public boolean visit(DoStatement node) {
    node.getBody().accept(this);
    newExpression(node.getExpression());
    node.getExpression().accept(this);
    List<VariableAccess> toExtract = getUnsequencedAccesses();
    if (!toExtract.isEmpty()) {
      // Convert "while (cond)" into "while (true) { if (!(cond)) break; ... }".
      List<Statement> stmtList = TreeUtil.asStatementList(node.getBody());
      extractOrderedAccesses(stmtList, currentTopNode, toExtract);
      stmtList.add(createLoopTermination(node.getExpression()));
      node.setExpression(new BooleanLiteral(true, typeUtil));
    }
    return false;
  }

  private void extractVariableDeclarationFragments(
      List<VariableDeclarationFragment> fragments, List<Statement> stmtList) {
    for (int i = 0; i < fragments.size(); i++) {
      VariableDeclarationFragment frag = fragments.get(i);
      Expression init = frag.getInitializer();
      if (init == null) {
        continue;
      }
      newExpression(init);
      init.accept(this);
      List<VariableAccess> toExtract = getUnsequencedAccesses();
      if (!toExtract.isEmpty()) {
        if (i > 0) {
          // Extract all fragments before the current one to preserve ordering.
          VariableDeclarationStatement newDecl =
              new VariableDeclarationStatement(fragments.get(0).copy());
          for (int j = 1; j < i; j++) {
            newDecl.addFragment(fragments.get(j).copy());
          }
          stmtList.add(newDecl);
          fragments.subList(0, i).clear();
        }
        extractOrderedAccesses(stmtList, currentTopNode, toExtract);
        i = 0;
      }
    }
  }

  private void extractExpressionList(
      List<Expression> expressions, List<Statement> stmtList, boolean extractModifiedExpression) {
    for (int i = 0; i < expressions.size(); i++) {
      Expression expr = expressions.get(i);
      newExpression(expr);
      expr.accept(this);
      List<VariableAccess> unsequencedAccesses = getUnsequencedAccesses();
      if (!unsequencedAccesses.isEmpty()) {
        for (int j = 0; j < i; j++) {
          stmtList.add(new ExpressionStatement(expressions.get(j).copy()));
        }
        expressions.subList(0, i).clear();
        extractOrderedAccesses(stmtList, currentTopNode, unsequencedAccesses);
        i = 0;
        if (extractModifiedExpression) {
          stmtList.add(new ExpressionStatement(expressions.get(0).copy()));
          expressions.remove(0);
          i = -1;
        }
      }
    }
  }

  @Override
  public boolean visit(ForStatement node) {
    List<Expression> initializers = node.getInitializers();
    // The for-loop initializers can either be a single variable declaration
    // expression or a list of initializer expressions.
    if (initializers.size() == 1 && initializers.get(0) instanceof VariableDeclarationExpression) {
      VariableDeclarationExpression decl = (VariableDeclarationExpression) initializers.get(0);
      extractVariableDeclarationFragments(
          decl.getFragments(), TreeUtil.asStatementList(node).subList(0, 0));
    } else {
      extractExpressionList(initializers, TreeUtil.asStatementList(node).subList(0, 0), false);
    }
    Expression expr = node.getExpression();
    if (expr != null) {
      newExpression(expr);
      expr.accept(this);
      List<VariableAccess> toExtract = getUnsequencedAccesses();
      if (!toExtract.isEmpty()) {
        // Convert "if (;cond;)" into "if (;;) { if (!(cond)) break; ...}".
        List<Statement> stmtList = TreeUtil.asStatementList(node.getBody()).subList(0, 0);
        extractOrderedAccesses(stmtList, currentTopNode, toExtract);
        stmtList.add(createLoopTermination(node.getExpression()));
        node.setExpression(null);
      }
    }
    extractExpressionList(node.getUpdaters(), TreeUtil.asStatementList(node.getBody()), true);
    node.getBody().accept(this);
    return false;
  }

  @Override
  public boolean visit(Assignment node) {
    Expression lhs = node.getLeftHandSide();
    VariableElement lhsVar = TreeUtil.getVariableElement(lhs);
    // Access order is important. If the lhs is a variable, then we must record
    // its access after visiting the rhs. Otherwise, visit both sides.
    if (lhsVar == null) {
      lhs.accept(this);
    }
    node.getRightHandSide().accept(this);
    addVariableAccess(lhsVar, node, true);
    return false;
  }

  @Override
  public boolean visit(PrefixExpression node) {
    PrefixExpression.Operator op = node.getOperator();
    if (op == PrefixExpression.Operator.INCREMENT || op == PrefixExpression.Operator.DECREMENT) {
      addVariableAccess(TreeUtil.getVariableElement(node.getOperand()), node, true);
    } else {
      node.getOperand().accept(this);
    }
    return false;
  }

  @Override
  public boolean visit(PostfixExpression node) {
    PostfixExpression.Operator op = node.getOperator();
    assert op == PostfixExpression.Operator.INCREMENT || op == PostfixExpression.Operator.DECREMENT;
    addVariableAccess(TreeUtil.getVariableElement(node.getOperand()), node, true);
    return false;
  }
}
