docs: @Override
    protected void visitForNode(ForNode node) {
      boolean hasIfempty = (node.numChildren() == 2);
      // NOTE: below we call id(varName) on a number of variables instead of using
      // VariableDeclaration.ref(),  this is because the refs() might be referenced on the other side
      // of a call to visitChildrenReturningCodeChunk.
      // That will break the normal behavior of FormattingContext being able to tell whether or not an
      // initial statement has already been generated because it eagerly coerces the value to a
      // string.  This will lead to redundant variable declarations.
      // When visitChildrenReturningCodeChunk is gone, this can be cleaned up, but for now we have to
      // manually decide where to declare the variables.
      List<Statement> statements = new ArrayList<>();
      // Build some local variable names.
      ForNonemptyNode nonEmptyNode = (ForNonemptyNode) node.getChild(0);
      String varPrefix = nonEmptyNode.getVarName() + node.getId();
  
      // TODO(b/32224284): A more consistent pattern for local variable management.
      String limitName = varPrefix + "ListLen";
      Expression limitInitializer;
      Optional<RangeArgs> args = RangeArgs.createFromNode(node);
      Function<Expression, Expression> getDataItemFunction;
      if (args.isPresent()) {
        RangeArgs range = args.get();
        // if any of the expressions are too expensive, allocate local variables for them
        final Expression start =
            maybeStashInLocal(
                range.start().isPresent() ? translateExpr(range.start().get()) : Expression.number(0),
                varPrefix + "_RangeStart",
                statements);
        final Expression end =
            maybeStashInLocal(translateExpr(range.limit()), varPrefix + "_RangeEnd", statements);
        final Expression step =
            maybeStashInLocal(
                range.increment().isPresent()
                    ? translateExpr(range.increment().get())
                    : Expression.number(1),
                varPrefix + "_RangeStep",
                statements);
        // the logic we want is
        // step * (end-start) < 0 ? 0 : ( (end-start)/step + ((end-start) % step == 0 ? 0 : 1));
        // but given that all javascript numbers are doubles we can simplify this somewhat.
        // Math.max(0, Match.ceil((end - start)/step))
        // should yield identical results.
        limitInitializer =
            dottedIdNoRequire("Math.max")
                .call(
                    number(0), dottedIdNoRequire("Math.ceil").call(end.minus(start).divideBy(step)));
        // optimize for foreach over a range
        getDataItemFunction = index -> start.plus(index.times(step));
      } else {
        // Define list var and list-len var.
        Expression dataRef = translateExpr(node.getExpr());
        final String listVarName = varPrefix + "List";
        Expression listVar = VariableDeclaration.builder(listVarName).setRhs(dataRef).build().ref();
        // does it make sense to store this in a variable?
        limitInitializer = listVar.dotAccess("length");
        getDataItemFunction = index -> id(listVarName).bracketAccess(index);
      }
  
      // Generate the foreach body as a CodeChunk.
      Expression limit = id(limitName);
      statements.add(VariableDeclaration.builder(limitName).setRhs(limitInitializer).build());
      Statement foreachBody = handleForeachLoop(nonEmptyNode, limit, getDataItemFunction);
  
      if (hasIfempty) {
        // If there is an ifempty node, wrap the foreach body in an if statement and append the
        // ifempty body as the else clause.
        Statement ifemptyBody = visitChildrenReturningCodeChunk(node.getChild(1));
        Expression limitCheck = limit.op(Operator.GREATER_THAN, number(0));
  
        foreachBody = ifStatement(limitCheck, foreachBody).setElse(ifemptyBody).build();
      }
      statements.add(foreachBody);
      jsCodeBuilder.append(Statement.of(statements));
    }