Repository: shepherdwind/velocity.js (ts)
Issue: set 命令bug
set变量层级大于2时，后面的set指令会覆盖前面set的值

即形如下面的VM指令

~~~
#set($a.b.c1 = 1 )
#set($a.b.c2 = 2)
~~~

执行后 a = {b:{c2:2}}，c1会被覆盖掉

出错代码：

~~~
var Velocity = require('velocityjs');
var tpl = '#set($a = "{}")\n#set($a.b = "{}")\n#set($a.b.c1 = 1)\n#set($a.b.c2 = 2)\n<h1>$a.b.c1</h1>';
console.log(Velocity.render(tpl, {}));
~~~

原因：
src/compile/set.js 第61行，重置了前面的设置
Pull request description: fix(set): set will overwrite value, close #63
such as code #set($a.b.c1 = 1 ) , and with context { a: b: {c2: 2} } .
After set value, context.a.b will be {c1: 1}, c2 is overwrite by #set

Patch:
diff --git a/src/compile/set.js b/src/compile/set.js
index a686027..3f4def2 100644
--- a/src/compile/set.js
+++ b/src/compile/set.js
@@ -53,14 +53,24 @@ module.exports = function(Velocity, utils){
         var len = ref.path ? ref.path.length: 0;
 
         //console.log(val);
-        utils.forEach(ref.path, function(exp, i){
+        utils.some(ref.path, function(exp, i){
 
           var isEnd = len === i + 1;
           var key = exp.id;
           if (exp.type === 'index')  key = key.value;
-          baseRef[key] = isEnd? val: {};
+
+          if (isEnd) {
+            return baseRef[key] = val;
+          }
+
           baseRef = baseRef[key];
 
+          // such as
+          // #set($a.d.c2 = 2)
+          // but $a.d is undefined , value set fail
+          if (baseRef === undefined) {
+            return true;
+          }
         });
 
       }


Test patch:
diff --git a/tests/compile.js b/tests/compile.js
index d72b5f6..1376291 100644
--- a/tests/compile.js
+++ b/tests/compile.js
@@ -188,121 +188,6 @@ describe('Compile', function() {
     })
   })
 
-  describe('Set && Expression', function() {
-
-    it('set equal to reference', function() {
-      var vm = '#set( $monkey = $bill ) ## variable reference'
-      assert.equal("hello", getContext(vm, {bill: 'hello'}).monkey)
-    })
-
-    it('empty map', function() {
-      var vm = '#set($foo = {})'
-      assert.deepEqual({}, getContext(vm).foo)
-    })
-
-    it('#set array', function() {
-      var vm = '#set($foo = []) #set($foo[0] = 12)'
-      assert.equal(12, getContext(vm).foo[0])
-    })
-
-    it('set equal to literal', function() {
-      var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" +
-               '#set( $monkey.Number = 123 ) ##number literal'
-      assert.equal("monica", getContext(vm).monkey.Friend)
-      assert.equal("123", getContext(vm).monkey.Number)
-    })
-    
-    it('set equal to result of method ', function () {
-      var vm = "#set( $monkey = 'monica' ) ## string literal\n" +
-               '#set( $result = $monkey.substring(1) ) ##calling method'
-      assert.equal("monica", getContext(vm).monkey)
-      assert.equal("onica", getContext(vm).result)
-    })
-
-    it('set equal to result of method ', function () {
-      var vm = "#set( $monkey = 1234 ) ## number literal\n" +
-               '#set( $result = $monkey.toString() ) ##calling method'
-      assert.equal("1234", getContext(vm).monkey)
-      assert.equal("1234", getContext(vm).result)
-    })
-
-    it('equal to method/property reference', function() {
-      var vm = "#set($monkey.Blame = $spindoctor.Leak) ## property \n" +
-               '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method'
-      var obj = {
-        spindoctor: {
-          weave: function(name) {
-            return name
-          },
-          Leak: "hello world"
-        },
-        web: "name"
-      }
-
-      assert.equal("hello world", getContext(vm, obj).monkey.Blame)
-      assert.equal("name", getContext(vm, obj).monkey.Plan)
-    })
-
-
-    it('equal to map/list', function() {
-      var vms = [
-        '#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList',
-        '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map'
-      ]
-
-      var list = ["Not", "my", "fault"]
-      var map = {banana: "good", 'roast beef': "bad"}
-      assert.deepEqual(list, getContext(vms[0], {my: "my"}).monkey.Say)
-      assert.deepEqual(map, getContext(vms[1]).monkey.Map)
-    })
-
-    it('expression simple math', function() {
-      assert.equal(10, getContext('#set($foo = 2 * 5)').foo)
-      assert.equal(2, getContext('#set($foo = 4 / 2)').foo)
-      assert.equal(-3, getContext('#set($foo = 2 - 5)').foo)
-      assert.equal(1, getContext('#set($foo = 5 % 2)').foo)
-      assert.equal(7, getContext('#set($foo = 7)').foo)
-    })
-
-    it('math with decimal', function() {
-      assert.equal(10.5, getContext('#set($foo = 2.1 * 5)').foo)
-      assert.equal(2.1, getContext('#set($foo = 4.2 / 2)').foo)
-      assert.equal(-7.5, getContext('#set($foo = - 2.5 - 5)').foo)
-    })
-
-    it('expression complex math', function() {
-      assert.equal(20, getContext('#set($foo = (7 + 3) * (10 - 8))').foo)
-      assert.equal(-20, getContext('#set($foo = -(7 + 3) * (10 - 8))').foo)
-      assert.equal(-1, getContext('#set($foo = -7 + 3 * (10 - 8))').foo)
-    })
-
-    it('expression compare', function() {
-      assert.equal(false, getContext('#set($foo = 10 > 11)').foo)
-      assert.equal(true, getContext('#set($foo = 10 < 11)').foo)
-      assert.equal(true, getContext('#set($foo = 10 != 11)').foo)
-      assert.equal(true, getContext('#set($foo = 10 <= 11)').foo)
-      assert.equal(true, getContext('#set($foo = 11 <= 11)').foo)
-      assert.equal(false, getContext('#set($foo = 12 <= 11)').foo)
-      assert.equal(true, getContext('#set($foo = 12 >= 11)').foo)
-      assert.equal(false, getContext('#set($foo = 10 == 11)').foo)
-    })
-
-    it('expression logic', function() {
-      assert.equal(false, getContext('#set($foo = 10 == 11 && 3 > 1)').foo)
-      assert.equal(true, getContext('#set($foo = 10 < 11 && 3 > 1)').foo)
-      assert.equal(true, getContext('#set($foo = 10 > 11 || 3 > 1)').foo)
-      assert.equal(true, getContext('#set($foo = !(10 > 11) && 3 > 1)').foo)
-      assert.equal(false, getContext('#set($foo = $a > $b)', {a: 1, b: 2}).foo)
-      assert.equal(false, getContext('#set($foo = $a && $b)', {a: 1, b: 0}).foo)
-      assert.equal(true, getContext('#set($foo = $a || $b)', {a: 1, b: 0}).foo)
-    })
-
-    it('#set context should be global, #25', function() {
-      var vm = '#macro(local) #set($val =1) $val #end #local() $val'
-      var ret = render(vm).replace(/\s+/g, '')
-      assert.equal('11', ret)
-    })
-  })
 
   describe('Literals', function() {
 
diff --git a/tests/set.test.js b/tests/set.test.js
new file mode 100644
index 0000000..ee0f19a
--- /dev/null
+++ b/tests/set.test.js
@@ -0,0 +1,150 @@
+var Velocity = require('../src/velocity')
+var assert = require("assert")
+var parse = Velocity.parse
+var Compile = Velocity.Compile
+
+describe('Set && Expression', function() {
+  var render = Velocity.render;
+
+  function getContext(str, context, macros) {
+    var compile = new Compile(parse(str))
+    compile.render(context, macros)
+    return compile.context
+  }
+
+  it('set equal to reference', function() {
+    var vm = '#set( $monkey = $bill ) ## variable reference'
+    assert.equal("hello", getContext(vm, {bill: 'hello'}).monkey)
+  })
+
+  it('empty map', function() {
+    var vm = '#set($foo = {})'
+    assert.deepEqual({}, getContext(vm).foo)
+  })
+
+  it('#set array', function() {
+    var vm = '#set($foo = []) #set($foo[0] = 12)'
+    assert.equal(12, getContext(vm).foo[0])
+  })
+
+  it('set equal to literal', function() {
+    var vm = "#set( $monkey.Friend = 'monica' ) ## string literal\n" +
+             '#set( $monkey.Number = 123 ) ##number literal'
+    assert.equal("monica", getContext(vm).monkey.Friend)
+    assert.equal("123", getContext(vm).monkey.Number)
+  })
+  
+  it('set equal to result of method ', function () {
+    var vm = "#set( $monkey = 'monica' ) ## string literal\n" +
+             '#set( $result = $monkey.substring(1) ) ##calling method'
+    assert.equal("monica", getContext(vm).monkey)
+    assert.equal("onica", getContext(vm).result)
+  })
+
+  it('set equal to result of method ', function () {
+    var vm = "#set( $monkey = 1234 ) ## number literal\n" +
+             '#set( $result = $monkey.toString() ) ##calling method'
+    assert.equal("1234", getContext(vm).monkey)
+    assert.equal("1234", getContext(vm).result)
+  })
+
+  it('equal to method/property reference', function() {
+    var vm = "#set($monkey.Blame = $spindoctor.Leak) ## property \n" +
+             '#set( $monkey.Plan = $spindoctor.weave($web) ) ## method'
+    var obj = {
+      spindoctor: {
+        weave: function(name) {
+          return name
+        },
+        Leak: "hello world"
+      },
+      web: "name"
+    }
+
+    assert.equal("hello world", getContext(vm, obj).monkey.Blame)
+    assert.equal("name", getContext(vm, obj).monkey.Plan)
+  })
+
+
+  it('equal to map/list', function() {
+    var vms = [
+      '#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList',
+      '#set( $monkey.Map = {"banana" : "good", "roast beef" : "bad"}) ## Map'
+    ]
+
+    var list = ["Not", "my", "fault"]
+    var map = {banana: "good", 'roast beef': "bad"}
+    assert.deepEqual(list, getContext(vms[0], {my: "my"}).monkey.Say)
+    assert.deepEqual(map, getContext(vms[1]).monkey.Map)
+  })
+
+  it('expression simple math', function() {
+    assert.equal(10, getContext('#set($foo = 2 * 5)').foo)
+    assert.equal(2, getContext('#set($foo = 4 / 2)').foo)
+    assert.equal(-3, getContext('#set($foo = 2 - 5)').foo)
+    assert.equal(1, getContext('#set($foo = 5 % 2)').foo)
+    assert.equal(7, getContext('#set($foo = 7)').foo)
+  })
+
+  it('math with decimal', function() {
+    assert.equal(10.5, getContext('#set($foo = 2.1 * 5)').foo)
+    assert.equal(2.1, getContext('#set($foo = 4.2 / 2)').foo)
+    assert.equal(-7.5, getContext('#set($foo = - 2.5 - 5)').foo)
+  })
+
+  it('expression complex math', function() {
+    assert.equal(20, getContext('#set($foo = (7 + 3) * (10 - 8))').foo)
+    assert.equal(-20, getContext('#set($foo = -(7 + 3) * (10 - 8))').foo)
+    assert.equal(-1, getContext('#set($foo = -7 + 3 * (10 - 8))').foo)
+  })
+
+  it('expression compare', function() {
+    assert.equal(false, getContext('#set($foo = 10 > 11)').foo)
+    assert.equal(true, getContext('#set($foo = 10 < 11)').foo)
+    assert.equal(true, getContext('#set($foo = 10 != 11)').foo)
+    assert.equal(true, getContext('#set($foo = 10 <= 11)').foo)
+    assert.equal(true, getContext('#set($foo = 11 <= 11)').foo)
+    assert.equal(false, getContext('#set($foo = 12 <= 11)').foo)
+    assert.equal(true, getContext('#set($foo = 12 >= 11)').foo)
+    assert.equal(false, getContext('#set($foo = 10 == 11)').foo)
+  })
+
+  it('expression logic', function() {
+    assert.equal(false, getContext('#set($foo = 10 == 11 && 3 > 1)').foo)
+    assert.equal(true, getContext('#set($foo = 10 < 11 && 3 > 1)').foo)
+    assert.equal(true, getContext('#set($foo = 10 > 11 || 3 > 1)').foo)
+    assert.equal(true, getContext('#set($foo = !(10 > 11) && 3 > 1)').foo)
+    assert.equal(false, getContext('#set($foo = $a > $b)', {a: 1, b: 2}).foo)
+    assert.equal(false, getContext('#set($foo = $a && $b)', {a: 1, b: 0}).foo)
+    assert.equal(true, getContext('#set($foo = $a || $b)', {a: 1, b: 0}).foo)
+  })
+
+  it('#set context should be global, #25', function() {
+    var vm = '#macro(local) #set($val =1) $val #end #local() $val'
+    var ret = render(vm).replace(/\s+/g, '')
+    assert.equal('11', ret)
+  })
+
+  describe('set mult level var', function() {
+    it('normal, fix #63', function() {
+      var tpl = `
+        #set($a = { "b": {} })
+        #set($a.b.c1 = 1)
+        #set($a.b.c2 = 2)
+      `
+      var context = getContext(tpl)
+      context.a.b.should.have.properties('c1', 'c2')
+    })
+
+    it('set fail', function() {
+      var tpl = `
+        #set($a = { "b": {} })
+        #set($a.b.c1 = 1)
+        #set($a.b.c2 = 2)
+        #set($a.d.c2 = 2)
+      `
+      var context = getContext(tpl)
+      context.a.should.not.have.property('d')
+    })
+  })
+})