Skip to content
This repository was archived by the owner on Feb 6, 2023. It is now read-only.

Commit ad4f64f

Browse files
niveditcfacebook-github-bot
authored andcommitted
Add data structure invariants for tree data
Summary: 1. Only leaf nodes have text. 2. Leaf nodes cannot have children. 3. Parent and child relationships should be mirrored. 4. Sibling relationship (``prevSibling`` & ``nextSibling``) should be mirrored. 5. All children of a parent must have sibling relationships. 6. The first node has no prev sibling. 7. The last node has no next sibling. 8. The data structure is a tree - fully connected & acyclic on the ``parent -> child`` and ``nextSibling`` edges. We do these checks as: * Go through each node in the block map and check that it is a valid node with respect to its properties and pointers to and from other nodes * Find the first (unique) node of the tree and check that the tree is connected Reviewed By: mitermayer Differential Revision: D9193098 fbshipit-source-id: 0dd6177e3cb8d15c9ee76ce48fa14d191f3ec93c
1 parent ae25b8f commit ad4f64f

File tree

2 files changed

+728
-0
lines changed

2 files changed

+728
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @format
10+
* @flow strict-local
11+
*
12+
* This is unstable and not part of the public API and should not be used by
13+
* production systems. This file may be update/removed without notice.
14+
*/
15+
import type {BlockMap} from 'BlockMap';
16+
import type ContentBlockNode from 'ContentBlockNode';
17+
18+
const warning = require('warning');
19+
20+
const DraftTreeInvariants = {
21+
/**
22+
* Check if the block is valid
23+
*/
24+
isValidBlock(block: ContentBlockNode, blockMap: BlockMap): boolean {
25+
const key = block.getKey();
26+
// is its parent's child
27+
const parentKey = block.getParentKey();
28+
if (parentKey != null) {
29+
const parent = blockMap.get(parentKey);
30+
if (!parent.getChildKeys().includes(key)) {
31+
warning(true, 'Tree is missing parent -> child pointer on %s', key);
32+
return false;
33+
}
34+
}
35+
36+
// is its children's parent
37+
const children = block.getChildKeys().map(k => blockMap.get(k));
38+
if (!children.every(c => c.getParentKey() === key)) {
39+
warning(true, 'Tree is missing child -> parent pointer on %s', key);
40+
return false;
41+
}
42+
43+
// is its previous sibling's next sibling
44+
const prevSiblingKey = block.getPrevSiblingKey();
45+
if (prevSiblingKey != null) {
46+
const prevSibling = blockMap.get(prevSiblingKey);
47+
if (prevSibling.getNextSiblingKey() !== key) {
48+
warning(
49+
true,
50+
"Tree is missing nextSibling pointer on %s's prevSibling",
51+
key,
52+
);
53+
return false;
54+
}
55+
}
56+
57+
// is its next sibling's previous sibling
58+
const nextSiblingKey = block.getNextSiblingKey();
59+
if (nextSiblingKey != null) {
60+
const nextSibling = blockMap.get(nextSiblingKey);
61+
if (nextSibling.getPrevSiblingKey() !== key) {
62+
warning(
63+
true,
64+
"Tree is missing prevSibling pointer on %s's nextSibling",
65+
key,
66+
);
67+
return false;
68+
}
69+
}
70+
71+
// no 2-node cycles
72+
if (nextSiblingKey !== null && prevSiblingKey !== null) {
73+
if (prevSiblingKey === nextSiblingKey) {
74+
warning(true, 'Tree has a two-node cycle at %s', key);
75+
return false;
76+
}
77+
}
78+
79+
// if it's a leaf node, it has text but no children
80+
if (block.text != '') {
81+
if (block.getChildKeys().size > 0) {
82+
warning(true, 'Leaf node %s has children', key);
83+
return false;
84+
}
85+
}
86+
return true;
87+
},
88+
89+
/**
90+
* Checks that this is a connected tree on all the blocks
91+
* starting from the first block, traversing nextSibling and child pointers
92+
* should be a tree (preorder traversal - parent, then children)
93+
* num of connected node === number of blocks
94+
*/
95+
isConnectedTree(blockMap: BlockMap): boolean {
96+
// exactly one node has no previous sibling + no parent
97+
const eligibleFirstNodes = blockMap
98+
.toArray()
99+
.filter(
100+
block =>
101+
block.getParentKey() == null && block.getPrevSiblingKey() == null,
102+
);
103+
if (eligibleFirstNodes.length !== 1) {
104+
warning(true, 'Tree is not connected. More or less than one first node');
105+
return false;
106+
}
107+
const firstNode = eligibleFirstNodes.shift();
108+
let nodesSeen = 0;
109+
let currentKey = firstNode.getKey();
110+
const visitedStack = [];
111+
while (currentKey != null) {
112+
const currentNode = blockMap.get(currentKey);
113+
const childKeys = currentNode.getChildKeys();
114+
const nextSiblingKey = currentNode.getNextSiblingKey();
115+
// if the node has children, add parent's next sibling to stack and go to children
116+
if (childKeys.size > 0) {
117+
if (nextSiblingKey != null) {
118+
visitedStack.unshift(nextSiblingKey);
119+
}
120+
const children = childKeys.map(k => blockMap.get(k));
121+
const firstNode = children.find(
122+
block => block.getPrevSiblingKey() == null,
123+
);
124+
if (firstNode == null) {
125+
warning(true, '%s has no first child', currentKey);
126+
return false;
127+
}
128+
currentKey = firstNode.getKey();
129+
// TODO(T32490138): Deal with multi-node cycles here
130+
} else {
131+
if (currentNode.getNextSiblingKey() != null) {
132+
currentKey = currentNode.getNextSiblingKey();
133+
} else {
134+
currentKey = visitedStack.shift();
135+
}
136+
}
137+
nodesSeen++;
138+
}
139+
140+
if (nodesSeen !== blockMap.size) {
141+
warning(
142+
true,
143+
'Tree is not connected. %s nodes were seen instead of %s',
144+
nodesSeen,
145+
blockMap.size,
146+
);
147+
return false;
148+
}
149+
150+
return true;
151+
},
152+
153+
/**
154+
* Checks that the block map is a connected tree with valid blocks
155+
*/
156+
isValidTree(blockMap: BlockMap): boolean {
157+
const blocks = blockMap.toArray();
158+
if (!blocks.every(block => this.isValidBlock(block, blockMap))) {
159+
return false;
160+
}
161+
return this.isConnectedTree(blockMap);
162+
},
163+
};
164+
165+
module.exports = DraftTreeInvariants;

0 commit comments

Comments
 (0)