// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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 net.starlark.java.syntax;

import java.util.List;

/**
 * A visitor for visiting the nodes of a syntax tree in lexical order (not evaluation order!).
 *
 * <p>Comments are *not* visited.
 *
 * <p>A subclass can change the traversal logic by setting {@link #skipNonSymbolIdentifiers}.
 *
 * <p>Typical usage is for a subclass to just override the {@code visit()} method overloads for the
 * nodes that are relevant to its business logic, and to rely on the default implementations in this
 * class to ensure traversal over the remaining node types. Overriding implementations should
 * remember to traverse children using either {@code super.visit()} on the current node, or explicit
 * calls to {@link #visit(Node)}, {@link #visitAll}, or {@link #visitBlock} on child fields.
 *
 * <p>Contrary to usual Java style, it is *not* recommended to strictly group all overloads of
 * {@code visit()} together, but rather to place helper methods for a specific node type next to its
 * associated {@code visit()} overload. Rationale: The benefit of this style rule is that a reader
 * can rely on the absence of an overload in the immediate vicinity of the method as evidence that
 * no such overload exists. But this isn't helpful in the context of the visitor pattern, where the
 * reader expects there to be many unrelated overloads.
 */
public class NodeVisitor {

  /**
   * If set, we only visit {@link Identifier}s that correspond to a definition or use of a symbol in
   * the current file. Specifically, this omits:
   *
   * <ul>
   *   <li>names of keyword arguments (but not names of keyword parameters!)
   *   <li>field names in dot expressions
   * </ul>
   *
   * <p>Note that {@code Identifier}s in such contexts have no {@link Binding} set for them by the
   * resolver.
   */
  protected boolean skipNonSymbolIdentifiers = false;

  // visit() overloads in this class are ordered by node type, first by category (misc / statement /
  // expression), then alphabetically within category. (Subclasses are not obliged to maintain the
  // same method ordering.)

  /** Entrypoint for visiting a node. Clients should avoid calling node-specific overloads. */
  public void visit(Node node) {
    // Double-dispatch pattern.
    // If a node type is added to the AST but no corresponding overload added to this class, we'll
    // see an infinite recursion in this method.
    node.accept(this);
  }

  // ==== Miscellaneous node types ====

  /**
   * Handles all four Argument node types uniformly. Subclasses should not add an overload for a
   * concrete Argument subclass; it won't be called.
   */
  public void visit(Argument node) {
    if (!skipNonSymbolIdentifiers && node instanceof Argument.Keyword keyword) {
      visit(keyword.getIdentifier());
    }
    visit(node.getValue());
  }

  /**
   * @deprecated Not supported.
   * @throws UnsupportedOperationException always.
   */
  @Deprecated
  public void visit(@SuppressWarnings("unused") Comment node) {
    // No reason we can't support this if we needed to.
    throw new UnsupportedOperationException("NodeVisitor does not support visiting comments");
  }

  // Clause and Entry are handled below next to Comprehension and Dict respectively.

  /**
   * Handles all four Parameter node types uniformly. Subclasses should not add an overload for a
   * concrete Parameter subclass; it won't be called.
   */
  public void visit(Parameter node) {
    if (node.getIdentifier() != null) {
      visit(node.getIdentifier());
    }
    if (node.getType() != null) {
      visit(node.getType());
    }
    if (node.getDefaultValue() != null) {
      visit(node.getDefaultValue());
    }
  }

  public void visit(StarlarkFile node) {
    visitBlock(node.getStatements());
  }

  // ==== Statement nodes ====

  public void visit(AssignmentStatement node) {
    visit(node.getLHS());
    if (node.getType() != null) {
      visit(node.getType());
    }
    visit(node.getRHS());
  }

  public void visit(ExpressionStatement node) {
    visit(node.getExpression());
  }

  public void visit(FlowStatement node) {}

  public void visit(ForStatement node) {
    visit(node.getVars());
    visit(node.getCollection());
    visitBlock(node.getBody());
  }

  public void visit(DefStatement node) {
    visit(node.getIdentifier());
    visitAll(node.getTypeParameters());
    visitAll(node.getParameters());
    if (node.getReturnType() != null) {
      visit(node.getReturnType());
    }
    visitBlock(node.getBody());
  }

  public void visit(IfStatement node) {
    visit(node.getCondition());
    visitBlock(node.getThenBlock());
    if (node.getElseBlock() != null) {
      visitBlock(node.getElseBlock());
    }
  }

  public void visit(LoadStatement node) {
    for (LoadStatement.Binding binding : node.getBindings()) {
      visit(binding.getLocalName());
      // We don't visit the original name.
      //
      // Currently, our AST doesn't distinguish between the case when the local name is omitted,
      // versus the case where it is given explicitly and exactly matches the original name. This
      // means that, if we visited both names here, we would end up double-visiting something that
      // often only appears once in the program source.
      //
      // TODO(bazel-team): Disambiguate these cases in LoadStatement.Binding, then visit it here,
      // but ONLY if skipNonSymbolIdentifiers is not set. Mind that subclasses might need updating
      // to continue to avoid traversing the original name.
    }
  }

  public void visit(ReturnStatement node) {
    if (node.getResult() != null) {
      visit(node.getResult());
    }
  }

  public void visit(TypeAliasStatement node) {
    visit(node.getIdentifier());
    visitAll(node.getParameters());
    visit(node.getDefinition());
  }

  public void visit(VarStatement node) {
    visit(node.getIdentifier());
    visit(node.getType());
  }

  // ==== Expression nodes ====

  public void visit(BinaryOperatorExpression node) {
    visit(node.getX());
    visit(node.getY());
  }

  public void visit(CallExpression node) {
    visit(node.getFunction());
    visitAll(node.getArguments());
  }

  public void visit(CastExpression node) {
    visit(node.getType());
    visit(node.getValue());
  }

  public void visit(Comprehension node) {
    visit(node.getBody());
    for (Comprehension.Clause clause : node.getClauses()) {
      if (clause instanceof Comprehension.For) {
        visit((Comprehension.For) clause);
      } else {
        visit((Comprehension.If) clause);
      }
    }
  }

  public void visit(Comprehension.For node) {
    visit(node.getVars());
    visit(node.getIterable());
  }

  public void visit(Comprehension.If node) {
    visit(node.getCondition());
  }

  public void visit(ConditionalExpression node) {
    visit(node.getThenCase());
    visit(node.getCondition());
    if (node.getElseCase() != null) {
      visit(node.getElseCase());
    }
  }

  public void visit(DictExpression node) {
    visitAll(node.getEntries());
  }

  public void visit(DictExpression.Entry node) {
    visit(node.getKey());
    visit(node.getValue());
  }

  public void visit(DotExpression node) {
    visit(node.getObject());
    if (!skipNonSymbolIdentifiers) {
      visit(node.getField());
    }
  }

  public void visit(Ellipsis node) {}

  public void visit(@SuppressWarnings("unused") FloatLiteral node) {}

  public void visit(Identifier node) {}

  public void visit(IndexExpression node) {
    visit(node.getObject());
    visit(node.getKey());
  }

  public void visit(@SuppressWarnings("unused") IntLiteral node) {}

  public void visit(IsInstanceExpression node) {
    visit(node.getValue());
    visit(node.getType());
  }

  public void visit(LambdaExpression node) {
    visitAll(node.getParameters());
    visit(node.getBody());
  }

  public void visit(ListExpression node) {
    visitAll(node.getElements());
  }

  public void visit(SliceExpression node) {
    visit(node.getObject());
    if (node.getStart() != null) {
      visit(node.getStart());
    }
    if (node.getStop() != null) {
      visit(node.getStop());
    }
    if (node.getStep() != null) {
      visit(node.getStep());
    }
  }

  public void visit(@SuppressWarnings("unused") StringLiteral node) {}

  public void visit(UnaryOperatorExpression node) {
    visit(node.getX());
  }

  public void visit(TypeApplication node) {
    visit(node.getConstructor());
    visitAll(node.getArguments());
  }

  // ==== Helpers for sequences of nodes ====

  /**
   * Visits a sequence of nodes (e.g. a list of arguments).
   *
   * <p>See {@link #visitBlock} for a common case.
   */
  // Final because this method is called across completely different categories of nodes, so it is
  // usually a mistake to attempt to override it.
  public final void visitAll(List<? extends Node> nodes) {
    for (Node node : nodes) {
      visit(node);
    }
  }

  /** Convenience/readability method for visiting a block of statements (e.g. an if branch). */
  // Previously this method was non-final and it was recommended to override it to perform an action
  // for every block. However, this is error-prone if the subclass inadvertently calls visitAll()
  // rather than visitBlock() in one of its other overrides. No one seems to be overriding either
  // method, so we made both of them final to avoid this potential problem.
  public final void visitBlock(List<Statement> statements) {
    visitAll(statements);
  }
}
