• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright 2020, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// TODO (b/162300507): Get rid of cloning
18import cloneDeep from 'lodash.clonedeep';
19
20export const DiffType = Object.freeze({
21  NONE: 'none',
22  ADDED: 'added',
23  DELETED: 'deleted',
24  ADDED_MOVE: 'addedMove',
25  DELETED_MOVE: 'deletedMove',
26  MODIFIED: 'modified',
27});
28
29export function asRawTreeViewObject(obj) {
30  const children = obj.children?.map(child => child.rawTreeViewObject) ?? []
31
32  return {
33    kind: obj.kind,
34    name: obj.name,
35    shortName: obj.shortName,
36    stableId: obj.stableId,
37    chips: obj.chips,
38    obj: obj.obj,
39    children,
40    ref: obj,
41  };
42}
43
44export function defaultModifiedCheck(newNode, oldNode) {
45  if (!newNode && !oldNode) {
46    return false;
47  }
48
49  if ((newNode && !oldNode) || (!newNode && oldNode)) {
50    return true;
51  }
52
53  return JSON.stringify(newNode.obj) !== JSON.stringify(oldNode.obj);
54}
55
56function isPrimitive(test) {
57  return test !== Object(test);
58}
59
60export class DiffGenerator {
61  constructor(tree) {
62    if (tree.rawTreeViewObject) {
63      this.tree = tree.rawTreeViewObject;
64    } else {
65      this.tree = tree;
66    }
67  }
68
69  compareWith(tree) {
70    if (tree?.rawTreeViewObject) {
71      this.diffWithTree = tree.rawTreeViewObject;
72    } else {
73      this.diffWithTree = tree;
74    }
75
76    return this;
77  }
78
79  withUniqueNodeId(getNodeId) {
80    this.getNodeId = (node) => {
81      const id = getNodeId(node);
82      if (id === null || id === undefined) {
83        console.error('Null node ID for node', node);
84        throw new Error('Node ID can\'t be null or undefined');
85      }
86      return id;
87    };
88    return this;
89  }
90
91  withModifiedCheck(isModified) {
92    this.isModified = isModified;
93    return this;
94  }
95
96  generateDiffTree() {
97    this.newMapping = this._generateIdToNodeMapping(this.tree);
98    this.oldMapping = this.diffWithTree ?
99      this._generateIdToNodeMapping(this.diffWithTree) : {};
100
101    const diffTrees =
102      this._generateDiffTree(this.tree, this.diffWithTree, [], []);
103
104    let diffTree;
105    if (diffTrees.length > 1) {
106      diffTree = {
107        kind: '',
108        name: 'DiffTree',
109        children: diffTrees,
110        stableId: 'DiffTree',
111      };
112    } else {
113      diffTree = diffTrees[0];
114    }
115
116    return Object.freeze(diffTree);
117  }
118
119  _generateIdToNodeMapping(node, acc) {
120    acc = acc || {};
121
122    const nodeId = this.getNodeId(node);
123
124    if (acc[nodeId]) {
125      throw new Error(`Duplicate node id '${nodeId}' detected...`);
126    }
127
128    acc[this.getNodeId(node)] = node;
129
130    if (node.children) {
131      for (const child of node.children) {
132        this._generateIdToNodeMapping(child, acc);
133      }
134    }
135
136    return acc;
137  }
138
139  _objIsEmpty(obj) {
140    return Object.keys(obj).length === 0 && obj.constructor === Object;
141  }
142
143  _cloneNodeWithoutChildren(node) {
144    const clone = {};
145
146    for (const key in node) {
147      if (key !== 'children') {
148        const val = node[key];
149        clone[key] = isPrimitive(val) ? val : cloneDeep(val);
150      }
151    }
152
153    return clone;
154  }
155
156  _generateDiffTree(newTree, oldTree, newTreeSiblings, oldTreeSiblings) {
157    const diffTrees = [];
158
159    // NOTE: A null ID represents a non existent node.
160    const newId = newTree ? this.getNodeId(newTree) : null;
161    const oldId = oldTree ? this.getNodeId(oldTree) : null;
162
163    const newTreeSiblingIds = newTreeSiblings.map(this.getNodeId);
164    const oldTreeSiblingIds = oldTreeSiblings.map(this.getNodeId);
165
166    if (newTree) {
167      // Deep clone newTree omitting children field
168      // Clone is required because trees are frozen objects — we can't
169      // modify the original tree object. Also means there is no side effect.
170      const diffTree = this._cloneNodeWithoutChildren(newTree);
171
172      // Default to no changes
173      diffTree.diff = {type: DiffType.NONE};
174
175      if (newId !== oldId) {
176        // A move, addition, or deletion has occurred
177        let nextOldTree;
178
179        // Check if newTree has been added or moved
180        if (!oldTreeSiblingIds.includes(newId)) {
181          if (this.oldMapping[newId]) {
182            // Objected existed in old tree, the counter party
183            // DELETED_MOVE will be/has been flagged and added to the
184            // diffTree when visiting it in the oldTree.
185            diffTree.diff = {type: DiffType.ADDED_MOVE};
186
187            // TODO: Figure out if oldTree is ever visited now...
188
189            // Switch out oldTree for new one to compare against
190            nextOldTree = this.oldMapping[newId];
191          } else {
192            diffTree.diff = {type: DiffType.ADDED};
193
194            // TODO: Figure out if oldTree is ever visited now...
195
196            // Stop comparing against oldTree
197            nextOldTree = null;
198          }
199        }
200
201        // Check if oldTree has been deleted of moved
202        if (oldTree && !newTreeSiblingIds.includes(oldId)) {
203          const deletedTreeDiff = this._cloneNodeWithoutChildren(oldTree);
204
205          if (this.newMapping[oldId]) {
206            deletedTreeDiff.diff = {type: DiffType.DELETED_MOVE};
207
208            // Stop comparing against oldTree, will be/has been
209            // visited when object is seen in newTree
210            nextOldTree = null;
211          } else {
212            deletedTreeDiff.diff = {type: DiffType.DELETED};
213
214            // Visit all children to check if they have been deleted or moved
215            deletedTreeDiff.children = this._visitChildren(null, oldTree);
216          }
217
218          diffTrees.push(deletedTreeDiff);
219        }
220
221        oldTree = nextOldTree;
222      } else {
223        // TODO: Always have modified check and add modified tags on top of
224        // others
225        // NOTE: Doesn't check for moved and modified at the same time
226        if (this.isModified && this.isModified(newTree, oldTree)) {
227          diffTree.diff = {type: DiffType.MODIFIED};
228        }
229      }
230
231      diffTree.children = this._visitChildren(newTree, oldTree);
232      diffTrees.push(diffTree);
233    } else if (oldTree) {
234      if (!newTreeSiblingIds.includes(oldId)) {
235        // Deep clone oldTree omitting children field
236        const diffTree = this._cloneNodeWithoutChildren(oldTree);
237
238        // newTree doesn't exists, oldTree has either been moved or deleted.
239        if (this.newMapping[oldId]) {
240          diffTree.diff = {type: DiffType.DELETED_MOVE};
241        } else {
242          diffTree.diff = {type: DiffType.DELETED};
243        }
244
245        diffTree.children = this._visitChildren(null, oldTree);
246        diffTrees.push(diffTree);
247      }
248    } else {
249      throw new Error('Both newTree and oldTree are undefined...');
250    }
251
252    return diffTrees;
253  }
254
255  _visitChildren(newTree, oldTree) {
256    // Recursively traverse all children of new and old tree.
257    const diffChildren = [];
258
259    // TODO: Try replacing this with some sort of zipWith.
260    const numOfChildren = Math.max(
261        newTree?.children.length ?? 0, oldTree?.children.length ?? 0);
262    for (let i = 0; i < numOfChildren; i++) {
263      const newChild = i < newTree?.children.length ?
264        newTree.children[i] : null;
265
266      const oldChild = i < oldTree?.children.length ?
267        oldTree.children[i] : null;
268
269      const childDiffTrees = this._generateDiffTree(
270          newChild, oldChild,
271          newTree?.children ?? [], oldTree?.children ?? [],
272      );
273      diffChildren.push(...childDiffTrees);
274    }
275
276    return diffChildren;
277  }
278}
279