Repository: DmitrySoshnikov/regexp-tree
Pull request #50 (2017-04-11 22:53:14)

add setChild/appendChild
also: getForNode updates cached path if parameters are given


Patch:
diff --git a/src/traverse/node-path.js b/src/traverse/node-path.js
index d9a4060..739a727 100644
--- a/src/traverse/node-path.js
+++ b/src/traverse/node-path.js
@@ -5,6 +5,9 @@
 
 'use strict';
 
+const DEFAULT_COLLECTION_PROP = 'expressions';
+const DEFAULT_SINGLE_PROP     = 'expression';
+
 /**
  * NodePath class encapsulates a traversing node,
  * its parent node, property name in the parent node, and
@@ -33,21 +36,71 @@ class NodePath {
     this.index = index;
   }
 
+  _enforceProp(property) {
+    if (!this.node.hasOwnProperty(property)) {
+      throw new Error(
+        `Node of type ${this.node.type} doesn't have "${property}" collection.`
+      );
+    }
+  }
+
+  /**
+   * Sets a node into a children collection or the single child.
+   * By default child nodes are supposed to be under `expressions` property.
+   * An explicit property can be passed.
+   *
+   * @param Object node - a node to set into a collection or as single child
+   * @param number index - index at which to set
+   * @param string property - name of the collection or single property
+   */
+  setChild(node, index = null, property = null) {
+
+    if (index != null) {
+      if (!property) {
+        property = DEFAULT_COLLECTION_PROP;
+      }
+      this._enforceProp(property);
+      this.node[property][index] = node;
+      return NodePath.getForNode(node, this, property, index);
+    } else {
+      if (!property) {
+        property = DEFAULT_SINGLE_PROP;
+      }
+      this._enforceProp(property);
+      this.node[property] = node;
+      return NodePath.getForNode(node, this, property);
+    }
+  }
+
   /**
-   * Inserts a node into a collection. By default child nodes
-   * are supposed to by under `expressions` property. An explicit
-   * property can be passed.
+   * Appends a node to a children collection.
+   * By default child nodes are supposed to be under `expressions` property.
+   * An explicit property can be passed.
+   *
+   * @param Object node - a node to set into a collection or as single child
+   * @param string property - name of the collection or single property
+   */
+  appendChild(node, property = null) {
+
+    if (!property) {
+      property = DEFAULT_COLLECTION_PROP;
+    }
+    this._enforceProp(property);
+    const end = this.node[property].length;
+    return this.setChild(node, end, property);
+  }
+
+  /**
+   * Inserts a node into a collection.
+   * By default child nodes are supposed to be under `expressions` property.
+   * An explicit property can be passed.
    *
    * @param Object node - a node to insert into a collection
    * @param number index - index at which to insert
    * @param string property - name of the collection property
    */
-  insertChildAt(node, index, property = 'expressions') {
-    if (!this.node.hasOwnProperty(property)) {
-      throw new Error(
-        `Node of type ${this.node.type} doesn't have "${property}" collection.`
-      );
-    }
+  insertChildAt(node, index, property = DEFAULT_COLLECTION_PROP) {
+    this._enforceProp(property);
 
     this.node[property].splice(index, 0, node);
 
@@ -165,11 +218,11 @@ class NodePath {
       return NodePath.getForNode(
         this.node.expressions[n],
         this,
-        "expressions",
+        DEFAULT_COLLECTION_PROP,
         n
       );
     } else if (this.node.expression && n == 0) {
-      return NodePath.getForNode(this.node.expression, this, "expression");
+      return NodePath.getForNode(this.node.expression, this, DEFAULT_SINGLE_PROP);
     }
     return null;
   }
@@ -247,7 +300,18 @@ class NodePath {
       );
     }
 
-    return NodePath.registry.get(node);
+    let path = NodePath.registry.get(node);
+
+    if (parentPath !== null) {
+      path.parentPath = parentPath;
+    }
+    if (prop !== null) {
+      path.property = prop;
+    }
+    if (index !== null) {
+      path.index = index;
+    }
+    return path;
   }
 
   /**


Test patch:
diff --git a/src/traverse/__tests__/node-path-test.js b/src/traverse/__tests__/node-path-test.js
index 6458ba9..d77f27a 100644
--- a/src/traverse/__tests__/node-path-test.js
+++ b/src/traverse/__tests__/node-path-test.js
@@ -309,7 +309,7 @@ describe('NodePath', () => {
             value: 'a',
             kind: 'simple',
           },
-          // 'a' replaced with 'c'
+          // 'b' replaced with 'c'
           cNode
         ]
       },
@@ -317,6 +317,60 @@ describe('NodePath', () => {
     });
   });
 
+  it('sets a child node to two new nested nodes', () => {
+    const ast = parser.parse('/ab/');
+
+    const bodyPath = new NodePath(ast.body);
+    const bCharPath = bodyPath.getChild(1);
+
+    const cNode = {
+      type: 'Char',
+      value: 'c',
+      kind: 'simple',
+    };
+
+    const groupNode = {
+      type: "Group",
+      capturing: true,
+      expression: null
+    };
+
+    const alterNode = {
+      type: "Alternative",
+      expressions: []
+    };
+
+    bCharPath.replace(groupNode);
+    expect(bCharPath.node).toEqual(groupNode);
+
+    const groupPath = bodyPath.getChild(1);
+
+    groupPath.setChild(cNode);
+
+    const cPath = NodePath.getForNode(cNode);
+    expect(cPath.parentPath).toBe(groupPath);
+    const cPath2 = groupPath.getChild();
+    expect(cPath2).toBe(cPath);
+    expect(cPath2.parentPath).toBe(groupPath);
+
+    const alterPath = groupPath.setChild(alterNode);
+    alterPath.appendChild(cNode);
+
+    const dNode = {
+      type: 'Char',
+      value: 'd',
+      kind: 'simple',
+    };
+    alterPath.appendChild(dNode);
+
+    const dPath = NodePath.getForNode(dNode);
+    expect(cPath.parentPath).toBe(alterPath);
+    expect(dPath.parentPath).toBe(alterPath);
+    expect(alterPath.parentPath).toBe(groupPath);
+
+    expect(generator.generate(ast)).toBe('/a(cd)/');
+  });
+
   it('getPreviousSibling', () => {
     const ast = parser.parse('/ab/');