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

Commit

Permalink
Add data structure invariants for tree data
Browse files Browse the repository at this point in the history
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
niveditc authored and facebook-github-bot committed Aug 9, 2018
1 parent ae25b8f commit ad4f64f
Show file tree
Hide file tree
Showing 2 changed files with 728 additions and 0 deletions.
165 changes: 165 additions & 0 deletions src/component/utils/exploration/DraftTreeInvariants.js
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;
Loading

0 comments on commit ad4f64f

Please sign in to comment.