This repository has been archived by the owner on Feb 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
ae25b8f
commit ad4f64f
Showing
2 changed files
with
728 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @format | ||
* @flow strict-local | ||
* | ||
* This is unstable and not part of the public API and should not be used by | ||
* production systems. This file may be update/removed without notice. | ||
*/ | ||
import type {BlockMap} from 'BlockMap'; | ||
import type ContentBlockNode from 'ContentBlockNode'; | ||
|
||
const warning = require('warning'); | ||
|
||
const DraftTreeInvariants = { | ||
/** | ||
* Check if the block is valid | ||
*/ | ||
isValidBlock(block: ContentBlockNode, blockMap: BlockMap): boolean { | ||
const key = block.getKey(); | ||
// is its parent's child | ||
const parentKey = block.getParentKey(); | ||
if (parentKey != null) { | ||
const parent = blockMap.get(parentKey); | ||
if (!parent.getChildKeys().includes(key)) { | ||
warning(true, 'Tree is missing parent -> child pointer on %s', key); | ||
return false; | ||
} | ||
} | ||
|
||
// is its children's parent | ||
const children = block.getChildKeys().map(k => blockMap.get(k)); | ||
if (!children.every(c => c.getParentKey() === key)) { | ||
warning(true, 'Tree is missing child -> parent pointer on %s', key); | ||
return false; | ||
} | ||
|
||
// is its previous sibling's next sibling | ||
const prevSiblingKey = block.getPrevSiblingKey(); | ||
if (prevSiblingKey != null) { | ||
const prevSibling = blockMap.get(prevSiblingKey); | ||
if (prevSibling.getNextSiblingKey() !== key) { | ||
warning( | ||
true, | ||
"Tree is missing nextSibling pointer on %s's prevSibling", | ||
key, | ||
); | ||
return false; | ||
} | ||
} | ||
|
||
// is its next sibling's previous sibling | ||
const nextSiblingKey = block.getNextSiblingKey(); | ||
if (nextSiblingKey != null) { | ||
const nextSibling = blockMap.get(nextSiblingKey); | ||
if (nextSibling.getPrevSiblingKey() !== key) { | ||
warning( | ||
true, | ||
"Tree is missing prevSibling pointer on %s's nextSibling", | ||
key, | ||
); | ||
return false; | ||
} | ||
} | ||
|
||
// no 2-node cycles | ||
if (nextSiblingKey !== null && prevSiblingKey !== null) { | ||
if (prevSiblingKey === nextSiblingKey) { | ||
warning(true, 'Tree has a two-node cycle at %s', key); | ||
return false; | ||
} | ||
} | ||
|
||
// if it's a leaf node, it has text but no children | ||
if (block.text != '') { | ||
if (block.getChildKeys().size > 0) { | ||
warning(true, 'Leaf node %s has children', key); | ||
return false; | ||
} | ||
} | ||
return true; | ||
}, | ||
|
||
/** | ||
* Checks that this is a connected tree on all the blocks | ||
* starting from the first block, traversing nextSibling and child pointers | ||
* should be a tree (preorder traversal - parent, then children) | ||
* num of connected node === number of blocks | ||
*/ | ||
isConnectedTree(blockMap: BlockMap): boolean { | ||
// exactly one node has no previous sibling + no parent | ||
const eligibleFirstNodes = blockMap | ||
.toArray() | ||
.filter( | ||
block => | ||
block.getParentKey() == null && block.getPrevSiblingKey() == null, | ||
); | ||
if (eligibleFirstNodes.length !== 1) { | ||
warning(true, 'Tree is not connected. More or less than one first node'); | ||
return false; | ||
} | ||
const firstNode = eligibleFirstNodes.shift(); | ||
let nodesSeen = 0; | ||
let currentKey = firstNode.getKey(); | ||
const visitedStack = []; | ||
while (currentKey != null) { | ||
const currentNode = blockMap.get(currentKey); | ||
const childKeys = currentNode.getChildKeys(); | ||
const nextSiblingKey = currentNode.getNextSiblingKey(); | ||
// if the node has children, add parent's next sibling to stack and go to children | ||
if (childKeys.size > 0) { | ||
if (nextSiblingKey != null) { | ||
visitedStack.unshift(nextSiblingKey); | ||
} | ||
const children = childKeys.map(k => blockMap.get(k)); | ||
const firstNode = children.find( | ||
block => block.getPrevSiblingKey() == null, | ||
); | ||
if (firstNode == null) { | ||
warning(true, '%s has no first child', currentKey); | ||
return false; | ||
} | ||
currentKey = firstNode.getKey(); | ||
// TODO(T32490138): Deal with multi-node cycles here | ||
} else { | ||
if (currentNode.getNextSiblingKey() != null) { | ||
currentKey = currentNode.getNextSiblingKey(); | ||
} else { | ||
currentKey = visitedStack.shift(); | ||
} | ||
} | ||
nodesSeen++; | ||
} | ||
|
||
if (nodesSeen !== blockMap.size) { | ||
warning( | ||
true, | ||
'Tree is not connected. %s nodes were seen instead of %s', | ||
nodesSeen, | ||
blockMap.size, | ||
); | ||
return false; | ||
} | ||
|
||
return true; | ||
}, | ||
|
||
/** | ||
* Checks that the block map is a connected tree with valid blocks | ||
*/ | ||
isValidTree(blockMap: BlockMap): boolean { | ||
const blocks = blockMap.toArray(); | ||
if (!blocks.every(block => this.isValidBlock(block, blockMap))) { | ||
return false; | ||
} | ||
return this.isConnectedTree(blockMap); | ||
}, | ||
}; | ||
|
||
module.exports = DraftTreeInvariants; |
Oops, something went wrong.