/*
 * Copyright 2011 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;

/**
 * Verifies that valid candidates for object literals are inlined as
 * expected, and invalid candidates are not touched.
 *
 */
public class InlineObjectLiteralsTest extends CompilerTestCase {

  public InlineObjectLiteralsTest() {
    enableNormalize();
  }

  @Override
  public void setUp() {
    super.enableLineNumberCheck(true);
  }

  @Override
  protected CompilerPass getProcessor(final Compiler compiler) {
    return new InlineObjectLiterals(
        compiler,
        compiler.getUniqueNameIdSupplier());
  }

  // Test object literal -> variable inlining

  public void testObject0() {
    // Don't mess with global variables, that is the job of CollapseProperties.
    testSame("var a = {x:1}; f(a.x);");
  }

  public void testObject1() {
    testLocal("var a = {x:x(), y:y()}; f(a.x, a.y);",
         "var JSCompiler_object_inline_x_0=x();" +
         "var JSCompiler_object_inline_y_1=y();" +
         "f(JSCompiler_object_inline_x_0, JSCompiler_object_inline_y_1);");
  }

  public void testObject1a() {
    testLocal("var a; a = {x:x, y:y}; f(a.x, a.y);",
         "var JSCompiler_object_inline_x_0;" +
         "var JSCompiler_object_inline_y_1;" +
         "(JSCompiler_object_inline_x_0=x," +
         "JSCompiler_object_inline_y_1=y, true);" +
         "f(JSCompiler_object_inline_x_0, JSCompiler_object_inline_y_1);");
  }

  public void testObject2() {
    testLocal("var a = {y:y}; a.x = z; f(a.x, a.y);",
         "var JSCompiler_object_inline_y_0 = y;" +
         "var JSCompiler_object_inline_x_1;" +
         "JSCompiler_object_inline_x_1=z;" +
         "f(JSCompiler_object_inline_x_1, JSCompiler_object_inline_y_0);");
  }

  public void testObject3() {
    // Inlining the 'y' would cause the 'this' to be different in the
    // target function.
    testSameLocal("var a = {y:y,x:x}; a.y(); f(a.x);");
    testSameLocal("var a; a = {y:y,x:x}; a.y(); f(a.x);");
  }

  public void testObject4() {
    // Object literal is escaped.
    testSameLocal("var a = {y:y}; a.x = z; f(a.x, a.y); g(a);");
    testSameLocal("var a; a = {y:y}; a.x = z; f(a.x, a.y); g(a);");
  }

  public void testObject5() {
    testLocal("var a = {x:x, y:y}; var b = {a:a}; f(b.a.x, b.a.y);",
         "var a = {x:x, y:y};" +
         "var JSCompiler_object_inline_a_0=a;" +
         "f(JSCompiler_object_inline_a_0.x, JSCompiler_object_inline_a_0.y);");
  }

  public void testObject6() {
    testLocal("for (var i = 0; i < 5; i++) { var a = {i:i,x:x}; f(a.i, a.x); }",
         "for (var i = 0; i < 5; i++) {" +
         "  var JSCompiler_object_inline_i_0=i;" +
         "  var JSCompiler_object_inline_x_1=x;" +
         "  f(JSCompiler_object_inline_i_0,JSCompiler_object_inline_x_1)" +
         "}");
    testLocal("if (c) { var a = {i:i,x:x}; f(a.i, a.x); }",
         "if (c) {" +
         "  var JSCompiler_object_inline_i_0=i;" +
         "  var JSCompiler_object_inline_x_1=x;" +
         "  f(JSCompiler_object_inline_i_0,JSCompiler_object_inline_x_1)" +
         "}");
  }

  public void testObject7() {
    testLocal("var a = {x:x, y:f()}; g(a.x);",
      "var JSCompiler_object_inline_x_0=x;" +
         "var JSCompiler_object_inline_y_1=f();" +
         "g(JSCompiler_object_inline_x_0)");
  }

  public void testObject8() {
    testSameLocal("var a = {x:x,y:y}; var b = {x:y}; f((c?a:b).x);");

    testLocal("var a; if(c) { a={x:x, y:y}; } else { a={x:y}; } f(a.x);",
         "var JSCompiler_object_inline_x_0;" +
         "var JSCompiler_object_inline_y_1;" +
         "if(c) JSCompiler_object_inline_x_0=x," +
         "      JSCompiler_object_inline_y_1=y," +
         "      true;" +
         "else JSCompiler_object_inline_x_0=y," +
         "     JSCompiler_object_inline_y_1=void 0," +
         "     true;" +
         "f(JSCompiler_object_inline_x_0)");
    testLocal("var a = {x:x,y:y}; var b = {x:y}; c ? f(a.x) : f(b.x);",
         "var JSCompiler_object_inline_x_0 = x; " +
         "var JSCompiler_object_inline_y_1 = y; " +
         "var JSCompiler_object_inline_x_2 = y; " +
         "c ? f(JSCompiler_object_inline_x_0):f(JSCompiler_object_inline_x_2)");
  }

  public void testObject9() {
    // There is a call, so no inlining
    testSameLocal("function f(a,b) {" +
             "  var x = {a:a,b:b}; x.a(); return x.b;" +
             "}");

    testLocal("function f(a,b) {" +
         "  var x = {a:a,b:b}; g(x.a); x = {a:a,b:2}; return x.b;" +
         "}",
         "function f(a,b) {" +
         "  var JSCompiler_object_inline_a_0 = a;" +
         "  var JSCompiler_object_inline_b_1 = b;" +
         "  g(JSCompiler_object_inline_a_0);" +
         "  JSCompiler_object_inline_a_0 = a," +
         "  JSCompiler_object_inline_b_1=2," +
         "  true;" +
         "  return JSCompiler_object_inline_b_1" +
         "}");

    testLocal("function f(a,b) { " +
         "  var x = {a:a,b:b}; g(x.a); x.b = x.c = 2; return x.b; " +
         "}",
         "function f(a,b) { " +
         "  var JSCompiler_object_inline_a_0=a;" +
         "  var JSCompiler_object_inline_b_1=b; " +
         "  var JSCompiler_object_inline_c_2;" +
         "  g(JSCompiler_object_inline_a_0);" +
         "  JSCompiler_object_inline_b_1=JSCompiler_object_inline_c_2=2;" +
         "  return JSCompiler_object_inline_b_1" +
         "}");
  }

  public void testObject10() {
    testLocal("var x; var b = f(); x = {a:a, b:b}; if(x.a) g(x.b);",
         "var JSCompiler_object_inline_a_0;" +
         "var JSCompiler_object_inline_b_1;" +
         "var b = f();" +
         "JSCompiler_object_inline_a_0=a,JSCompiler_object_inline_b_1=b,true;" +
         "if(JSCompiler_object_inline_a_0) g(JSCompiler_object_inline_b_1)");
    testLocal("var x = {}; var b = f(); x = {a:a, b:b}; if(x.a) g(x.b) + x.c",
         "var x = {}; var b = f(); x = {a:a, b:b}; if(x.a) g(x.b) + x.c");
    testLocal("var x; var b = f(); x = {a:a, b:b}; x.c = c; if(x.a) g(x.b) + x.c",
         "var JSCompiler_object_inline_a_0;" +
         "var JSCompiler_object_inline_b_1;" +
         "var JSCompiler_object_inline_c_2;" +
         "var b = f();" +
         "JSCompiler_object_inline_a_0 = a,JSCompiler_object_inline_b_1 = b, " +
         "  JSCompiler_object_inline_c_2=void 0,true;" +
         "JSCompiler_object_inline_c_2 = c;" +
         "if (JSCompiler_object_inline_a_0)" +
         "  g(JSCompiler_object_inline_b_1) + JSCompiler_object_inline_c_2;");
    testLocal("var x = {a:a}; if (b) x={b:b}; f(x.a||x.b);",
         "var JSCompiler_object_inline_a_0 = a;" +
         "var JSCompiler_object_inline_b_1;" +
         "if(b) JSCompiler_object_inline_b_1 = b," +
         "      JSCompiler_object_inline_a_0 = void 0," +
         "      true;" +
         "f(JSCompiler_object_inline_a_0 || JSCompiler_object_inline_b_1)");
    testLocal("var x; var y = 5; x = {a:a, b:b, c:c}; if (b) x={b:b}; f(x.a||x.b);",
         "var JSCompiler_object_inline_a_0;" +
         "var JSCompiler_object_inline_b_1;" +
         "var JSCompiler_object_inline_c_2;" +
         "var y=5;" +
         "JSCompiler_object_inline_a_0=a," +
         "JSCompiler_object_inline_b_1=b," +
         "JSCompiler_object_inline_c_2=c," +
         "true;" +
         "if (b) JSCompiler_object_inline_b_1=b," +
         "       JSCompiler_object_inline_a_0=void 0," +
         "       JSCompiler_object_inline_c_2=void 0," +
         "       true;" +
         "f(JSCompiler_object_inline_a_0||JSCompiler_object_inline_b_1)");
  }

  public void testObject11() {
    testSameLocal("var x = {a:b}; (x = {a:a}).c = 5; f(x.a);");
    testSameLocal("var x = {a:a}; f(x[a]); g(x[a]);");
  }

  public void testObject12() {
    testLocal("var a; a = {x:1, y:2}; f(a.x, a.y2);",
        "var a; a = {x:1, y:2}; f(a.x, a.y2);");
  }

  public void testObject13() {
    testSameLocal("var x = {a:1, b:2}; x = {a:3, b:x.a};");
  }

  public void testObject14() {
    testSameLocal("var x = {a:1}; if ('a' in x) { f(); }");
    testSameLocal("var x = {a:1}; for (var y in x) { f(y); }");
  }

  public void testObject15() {
    testSameLocal("x = x || {}; f(x.a);");
  }

  public void testObject16() {
    testLocal("function f(e) { bar(); x = {a: foo()}; var x; print(x.a); }",
         "function f(e) { " +
         "  var JSCompiler_object_inline_a_0;" +
         "  bar();" +
         "  JSCompiler_object_inline_a_0 = foo(), true;" +
         "  print(JSCompiler_object_inline_a_0);" +
         "}");
  }

  public void testObject17() {
    // Note: Some day, with careful analysis, these two uses could be
    // disambiguated, and the second assignment could be inlined.
    testSameLocal(
      "var a = {a: function(){}};" +
      "a.a();" +
      "a = {a1: 100};" +
      "print(a.a1);");
  }

  public void testObject18() {
    testSameLocal("var a,b; b=a={x:x, y:y}; f(b.x);");
  }

  public void testObject19() {
    testSameLocal("var a,b; if(c) { b=a={x:x, y:y}; } else { b=a={x:y}; } f(b.x);");
  }

  public void testObject20() {
    testSameLocal("var a,b; if(c) { b=a={x:x, y:y}; } else { b=a={x:y}; } f(a.x);");
  }

  public void testObject21() {
    testSameLocal("var a,b; b=a={x:x, y:y};");
    testSameLocal("var a,b; if(c) { b=a={x:x, y:y}; }" +
             "else { b=a={x:y}; } f(a.x); f(b.x)");
    testSameLocal("var a, b; if(c) { if (a={x:x, y:y}) f(); } " +
             "else { b=a={x:y}; } f(a.x);");
    testSameLocal("var a,b; b = (a = {x:x, y:x});");
    testSameLocal("var a,b; a = {x:x, y:x}; b = a");
    testSameLocal("var a,b; a = {x:x, y:x}; b = x || a");
    testSameLocal("var a,b; a = {x:x, y:x}; b = y && a");
    testSameLocal("var a,b; a = {x:x, y:x}; b = y ? a : a");
    testSameLocal("var a,b; a = {x:x, y:x}; b = y , a");
    testSameLocal("b = x || (a = {x:1, y:2});");
  }

  public void testObject22() {
    testLocal("while(1) { var a = {y:1}; if (b) a.x = 2; f(a.y, a.x);}",
      "for(;1;){" +
      " var JSCompiler_object_inline_y_0=1;" +
      " var JSCompiler_object_inline_x_1;" +
      " if(b) JSCompiler_object_inline_x_1=2;" +
      " f(JSCompiler_object_inline_y_0,JSCompiler_object_inline_x_1)" +
      "}");

    testLocal("var a; while (1) { f(a.x, a.y); a = {x:1, y:1};}",
        "var a; while (1) { f(a.x, a.y); a = {x:1, y:1};}");
  }

  public void testObject23() {
    testLocal("function f() {\n" +
         "  var templateData = {\n" +
         "    linkIds: {\n" +
         "      CHROME: 'cl',\n" +
         "      DISMISS: 'd'\n" +
         "    }\n" +
         "  };\n" +
         "  var html = templateData.linkIds.CHROME \n" +
         "       + \":\" + templateData.linkIds.DISMISS;\n" +
         "}",
         "function f(){" +
         "var JSCompiler_object_inline_CHROME_1='cl';" +
         "var JSCompiler_object_inline_DISMISS_2='d';" +
         "var html=JSCompiler_object_inline_CHROME_1 +" +
         " ':' +JSCompiler_object_inline_DISMISS_2}");
  }

  public void testObject24() {
    testLocal("function f() {\n" +
         "  var linkIds = {\n" +
         "      CHROME: 1,\n" +
         "  };\n" +
         "  var g = function () {var o = {a: linkIds};}\n" +
         "}",
         "function f(){var linkIds={CHROME:1};" +
         "var g=function(){var JSCompiler_object_inline_a_0=linkIds}}");
  }

  public void testObject25() {
    testLocal("var a = {x:f(), y:g()}; a = {y:g(), x:f()}; f(a.x, a.y);",
         "var JSCompiler_object_inline_x_0=f();" +
         "var JSCompiler_object_inline_y_1=g();" +
         "JSCompiler_object_inline_y_1=g()," +
         "  JSCompiler_object_inline_x_0=f()," +
         "  true;" +
         "f(JSCompiler_object_inline_x_0,JSCompiler_object_inline_y_1)");
  }

  public void testObject26() {
    testLocal("var a = {}; a.b = function() {}; new a.b.c",
         "var JSCompiler_object_inline_b_0;" +
         "JSCompiler_object_inline_b_0=function(){};" +
         "new JSCompiler_object_inline_b_0.c");
  }

  public void testBug545() {
    testLocal("var a = {}", "");
    testLocal("var a; a = {}", "true");
  }

  public void testIssue724() {
    testSameLocal(
        "var getType; getType = {};" +
        "return functionToCheck && " +
        "   getType.toString.apply(functionToCheck) === " +
        "   '[object Function]';");
  }

  public void testNoInlineDeletedProperties() {
    testSameLocal(
        "var foo = {bar:1};" +
        "delete foo.bar;" +
        "return foo.bar;");
  }

  private final String LOCAL_PREFIX = "function local(){";
  private final String LOCAL_POSTFIX = "}";

  private void testLocal(String code, String result) {
    test(LOCAL_PREFIX + code + LOCAL_POSTFIX,
         LOCAL_PREFIX + result + LOCAL_POSTFIX);
  }

  private void testSameLocal(String code) {
    testLocal(code, code);
  }
}
