/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import com.google.javascript.rhino.Node;

/**
 * Unit tests for {@link CoalesceVariableNames}
 *
 */
public final class CoalesceVariableNamesTest extends CompilerTestCase {
  // The spacing in this file is not exactly standard but it greatly helps
  // picking out which variable names are merged.

  private boolean usePseudoName = false;

  @Override
  protected int getNumRepetitions() {
   return 1;
  }

  @Override
  public void setUp() {
    usePseudoName = false;
  }

  @Override
  public CompilerPass getProcessor(final Compiler compiler) {
    return new CompilerPass() {
      @Override
      public void process(Node externs, Node js) {
        NodeTraversal.traverse(compiler, js,
            new CoalesceVariableNames(compiler, usePseudoName));
      }
    };
  }

  public void testSimple() {
    inFunction("var x; var y; x=1; x; y=1; y; return y",
               "var x;        x=1; x; x=1; x; return x");

    inFunction("var x,y; x=1; x; y=1; y",
               "var x  ; x=1; x; x=1; x");

    inFunction("var x,y; x=1; y=2; y; x");

    inFunction("y=0; var x, y; y; x=0; x",
               "y=0; var y   ; y; y=0;y");

    inFunction("var x,y; x=1; y=x; y",
               "var x  ; x=1; x=x; x");

    inFunction("var x,y; x=1; y=x+1; y",
               "var x  ; x=1; x=x+1; x");

    inFunction("x=1; x; y=2; y; var x; var y",
               "x=1; x; x=2; x; var x");

    inFunction("var x=1; var y=x+1; return y",
               "var x=1;     x=x+1; return x");

    inFunction("var x=1; var y=0; x+=1; y");

    inFunction("var x=1; x+=1; var y=0; y",
               "var x=1; x+=1;     x=0; x");

    inFunction("var x=1; foo(bar(x+=1)); var y=0; y",
               "var x=1; foo(bar(x+=1));     x=0; x");

    inFunction("var y, x=1; f(x+=1, y)");

    inFunction("var x; var y; y += 1, y, x = 1; x");
  }

  public void testMergeThreeVarNames() {
    inFunction("var x,y,z; x=1; x; y=1; y; z=1; z",
               "var x    ; x=1; x; x=1; x; x=1; x");
  }

  public void testDifferentBlock() {
    inFunction("if(1) { var x = 0; x } else { var y = 0; y }",
               "if(1) { var x = 0; x } else {     x = 0; x }");
  }

  public void testLoops() {
    inFunction("var x; while(1) { x; x = 1; var y = 1; y }");
    inFunction("var y = 1; y; while(1) { var x = 1; x }",
               "var y = 1; y; while(1) {     y = 1; y }");
  }

  public void testEscaped() {
    inFunction("var x = 1; x; function f() { x };  var y = 0; y; f()");
  }

  public void testFor() {
    inFunction("var x = 1; x; for (;;) var y; y = 1; y",
               "var x = 1; x; for (;;)      ; x = 1; x");
  }

  public void testForIn() {
    // We lose some precision here, unless we have "branched-backward-dataflow".
    inFunction("var x = 1, k; x;      ; for (var y in k) { y }",
               "var x = 1, k; x;      ; for (var y in k) { y }");

    inFunction("var x = 1, k; x; y = 1; for (var y in k) { y }",
               "var x = 1, k; x; x = 1; for (    x in k) { x }");
  }

  public void testLoopInductionVar() {
    inFunction(
        "for(var x = 0; x < 10; x++){}" +
        "for(var y = 0; y < 10; y++){}" +
        "for(var z = 0; z < 10; z++){}",

        "for(var x = 0; x < 10; x++){}" +
        "for(x = 0; x < 10; x++){}" +
        "for(x = 0; x < 10; x++){}");

    inFunction(
        "for(var x = 0; x < 10; x++){z}" +
        "for(var y = 0, z = 0; y < 10; y++){z}",

        "for(var x = 0; x < 10; x++){z}" +
        "for(var x = 0, z = 0; x < 10; x++){z}");

    inFunction("var x = 1; x; for (var y; y=1; ) {y}",
               "var x = 1; x; for (     ; x=1; ) {x}");

    inFunction("var x = 1; x; y = 1; while(y) var y; y",
               "var x = 1; x; x = 1; while(x); x");

    inFunction("var x = 1; x; f:var y; y=1",
               "var x = 1; x; x=1");
  }

  public void testSwitchCase() {
    inFunction("var x = 1; switch(x) { case 1: var y; case 2: } y = 1; y",
               "var x = 1; switch(x) { case 1:        case 2: } x = 1; x");
  }

  public void testDuplicatedVar() {
    // Is there a shorter version without multiple declarations?
    inFunction("z = 1; var x = 0; x; z; var y = 2, z = 1; y; z;",
               "z = 1; var x = 0; x; z; var x = 2, z = 1; x; z;");
  }

  public void testTryCatch() {
    inFunction("try {} catch (e) { } var x = 4; x;",
               "try {} catch (e) { } var x = 4; x;");
    inFunction("var x = 4; x; try {} catch (e) { }",
               "var x = 4; x; try {} catch (e) { }");
  }

  public void testDeadAssignment() {
    inFunction("var x = 6; var y; y = 4 ; x");
    inFunction("var y = 3; var y; y += 4; x");
    inFunction("var y = 3; var y; y ++  ; x");
    inFunction("y = 3; var x; var y = 1 ; x");
  }

  public void testParameter() {
    test("function FUNC(param) {var x = 0; x}",
         "function FUNC(param) {param = 0; param}");
  }

  public void testParameter2() {
    // Make sure two formal parameter name never merges.
    test("function FUNC(x,y) {x = 0; x; y = 0; y}");
    test("function FUNC(x,y,z) {x = 0; x; y = 0; z = 0; z}");
  }

  public void testParameter3() {
    // Make sure the formal parameter declaration is consider a def.
    test("function FUNC(x) {var y; y = 0; x; y}");
  }

  public void testParameter4() {
    // Make sure that we do not merge two-arg functions because of the
    // IE sort bug (see comments in computeEscaped)
    test("function FUNC(x, y) {var a,b; y; a=0; a; x; b=0; b}",
         "function FUNC(x, y) {var a; y; a=0; a; x; a=0; a}");
  }

  public void testParameter4b() {
    // Merge parameters
    test("function FUNC(x, y, z) {var a,b; y; a=0; a; x; b=0; b}",
         "function FUNC(x, y, z) {         y; y=0; y; x; x=0; x}");
  }

  public void testLiveRangeChangeWithinCfgNode() {
    inFunction("var x, y; x = 1, y = 2, y, x");
    inFunction("var x, y; x = 1,x; y");

    // We lose some precisions within the node itself.
    inFunction("var x; var y; y = 1, y, x = 1; x");
    inFunction("var x; var y; y = 1; y, x = 1; x", "var x; x = 1; x, x = 1; x");
    inFunction("var x, y; y = 1, x = 1, x, y += 1, y");
    inFunction("var x, y; y = 1, x = 1, x, y ++, y");
  }

  public void testLiveRangeChangeWithinCfgNode2() {
    inFunction("var x; var y; var a; var b;" +
               "y = 1, a = 1, y, a, x = 1, b = 1; x; b");
    inFunction("var x; var y; var a; var b;" +
               "y = 1, a = 1, y, a, x = 1; x; b = 1; b",
               "var x; var y; var a;       " +
               "y = 1, a = 1, y, a, x = 1; x; x = 1; x");
    inFunction("var x; var y; var a; var b;" +
               "y = 1, a = 1, y, x = 1; a; x; b = 1; b",
               "var x; var y; var a;       " +
               "y = 1, a = 1, y, x = 1; a; x; x = 1; x");
  }

  public void testFunctionNameReuse() {
// TODO(user): Figure out why this increase code size most of the time.
//    inFunction("function x() {}; x(); var y = 1; y",
//               "function x() {}; x(); x = 1; x");
//    inFunction("x(); var y = 1; y; function x() {}",
//               "x(); x = 1; x; function x() {}");
//    inFunction("x(); var y = 1; function x() {}; y",
//               "x(); x = 1; function x() {}; x");
//    // Can't merge because of possible escape.
//    inFunction("function x() {return x}; x(); var y = 1; y",
//               "function x() {return x}; x(); var y = 1; y");
//
//    inFunction("var y = 1; y; x; function x() {}",
//               "var y = 1; y; x; function x() {}");
//    inFunction("var y = 1; y; function x() {}; x",
//               "var y = 1; y; function x() {}; x");
//    inFunction("var y = 1; y; function x() {}; x = 1; x",
//               "var y = 1; y; function x() {}; y = 1; y");
//    inFunction("var y = 1; y; x = 1; function x() {}; x",
//               "var y = 1; y; y = 1; function x() {}; y");
  }

  public void testBug1401831() {
    // Verify that we don't wrongly merge "opt_a2" and "i" without considering
    // arguments[0] aliasing it.
    String src = "function f(opt_a2) {" +
        "  var buffer;" +
        "  if (opt_a2) {" +
        "    for(var i = 0; i < arguments.length; i++) {" +
        "      buffer += arguments[i];" +
        "    }" +
        "  }" +
        "  return buffer;" +
        "}";
    test(src, src);
  }

  public void testDeterministic() {
    // Make the variable interference graph a pentagon.
    //         a - b
    //        /     \
    //        e     c
    //         \   /
    //           d
    // The coloring partitioning would be:
    //  a = { a, c }
    //  b = { b, d }
    //  e = { e }
    inFunction("var a,b,c,d,e;" +
               "  a=1; b=1; a; b;" +
               "  b=1; c=1; b; c;" +
               "  c=1; d=1; c; d;" +
               "  d=1; e=1; d; e;" +
               "  e=1; a=1; e; a;",

               "var a,b,    e;" +
               "  a=1; b=1; a; b;" +
               "  b=1; a=1; b; a;" +
               "  a=1; b=1; a; b;" +
               "  b=1; e=1; b; e;" +
               "  e=1; a=1; e; a;");

    // If we favor "d" first by declaring "d" earlier,
    // the coloring partitioning would be:
    //  b = { b, e }
    //  d = { d, a }
    //  c = { c }
    inFunction("var d,a,b,c,e;" +
               "  a=1; b=1; a; b;" +
               "  b=1; c=1; b; c;" +
               "  c=1; d=1; c; d;" +
               "  d=1; e=1; d; e;" +
               "  e=1; a=1; e; a;",

               "var d,  b,c  ;" +
               "  d=1; b=1; d; b;" +
               "  b=1; c=1; b; c;" +
               "  c=1; d=1; c; d;" +
               "  d=1; b=1; d; b;" +
               "  b=1; d=1; b; d;");
  }

  // Sometimes live range can be cross even within a VAR declaration.
  public void testVarLiveRangeCross() {
    inFunction("var a={}; var b=a.S(); b",
               "var a={};     a=a.S(); a");
    inFunction("var a={}; var b=a.S(), c=b.SS(); b; c",
               "var a={}; var b=a.S(), a=b.SS(); b; a");
    inFunction("var a={}; var b=a.S(), c=a.SS(), d=a.SSS(); b; c; d",
               "var a={}; var b=a.S(), c=a.SS(), a=a.SSS(); b; c; a");
    inFunction("var a={}; var b=a.S(), c=a.SS(), d=a.SSS(); b; c; d",
               "var a={}; var b=a.S(), c=a.SS(), a=a.SSS(); b; c; a");
    inFunction("var a={}; d=1; d; var b=a.S(), c=a.SS(), d=a.SSS(); b; c; d");
  }

  public void testBug1445366() {
    // An assignment might not be complete if the RHS throws an exception.
    inFunction(
        " var iframe = getFrame();" +
        " try {" +
        "   var win = iframe.contentWindow;" +
        " } catch (e) {" +
        " } finally {" +
        "   if (win)" +
        "     this.setupWinUtil_();" +
        "   else" +
        "     this.load();" +
        " }");

    // Verify that we can still coalesce it if there are no handlers.
    inFunction(
        " var iframe = getFrame();" +
        " var win = iframe.contentWindow;" +
        " if (win)" +
        "   this.setupWinUtil_();" +
        " else" +
        "   this.load();",

        " var iframe = getFrame();" +
        " iframe = iframe.contentWindow;" +
        " if (iframe)" +
        "   this.setupWinUtil_();" +
        " else" +
        "   this.load();");
  }

  public void testCannotReuseAnyParamsBug() {
    testSame("function handleKeyboardShortcut(e, key, isModifierPressed) {\n" +
        "  if (!isModifierPressed) {\n" +
        "    return false;\n" +
        "  }\n" +
        "  var command;\n" +
        "  switch (key) {\n" +
        "    case 'b': // Ctrl+B\n" +
        "      command = COMMAND.BOLD;\n" +
        "      break;\n" +
        "    case 'i': // Ctrl+I\n" +
        "      command = COMMAND.ITALIC;\n" +
        "      break;\n" +
        "    case 'u': // Ctrl+U\n" +
        "      command = COMMAND.UNDERLINE;\n" +
        "      break;\n" +
        "    case 's': // Ctrl+S\n" +
        "      return true;\n" +
        "  }\n" +
        "\n" +
        "  if (command) {\n" +
        "    this.fieldObject.execCommand(command);\n" +
        "    return true;\n" +
        "  }\n" +
        "\n" +
        "  return false;\n" +
        "};");
  }

  public void testForInWithAssignment() {
    inFunction(
      "var _f = function (commands) {" +
          "  var k, v, ref;" +
          "  for (k in ref = commands) {" +
          "    v = ref[k];" +
          "    alert(k + ':' + v);" +
          "  }" +
          "}",

      "var _f = function (commands) {" +
          "  var k,ref;" +
          "  for (k in ref = commands) {" +
          "    commands = ref[k];" +
          "    alert(k + ':' + commands);" +
          "  }" +
          "}"
        );
  }

  public void testUsePseduoNames() {
    usePseudoName = true;
    inFunction("var x   = 0; print(x  ); var   y = 1; print(  y)",
               "var x_y = 0; print(x_y);     x_y = 1; print(x_y)");

    inFunction("var x_y = 1; var x   = 0; print(x  ); var     y = 1;" +
               "print(  y); print(x_y);",

               "var x_y = 1; var x_y$ = 0; print(x_y$);     x_y$ = 1;" + "" +
               "print(x_y$); print(x_y);");

    inFunction("var x_y = 1; function f() {" +
               "var x    = 0; print(x  ); var y = 1; print( y);" +
               "print(x_y);}",

               "var x_y = 1; function f() {" +
               "var x_y$ = 0; print(x_y$); x_y$ = 1; print(x_y$);" +
               "print(x_y);}");

    inFunction("var x   = 0; print(x  ); var   y = 1; print(  y); " +
               "var closure_var; function bar() { print(closure_var); }",
               "var x_y = 0; print(x_y);     x_y = 1; print(x_y); " +
               "var closure_var; function bar() { print(closure_var); }");
  }

  public void testMaxVars() {
    String code = "";
    for (int i = 0;
         i < LiveVariablesAnalysis.MAX_VARIABLES_TO_ANALYZE + 1; i++) {
      code += String.format("var x%d = 0; print(x%d);", i, i);
    }
    inFunction(code);
  }

  private void inFunction(String src) {
    inFunction(src, src);
  }

  private void inFunction(String src, String expected) {
    test("function FUNC(){" + src + "}",
         "function FUNC(){" + expected + "}");
  }

  private void test(String src) {
    test(src, src);
  }
}
