/*
* Copyright 2017 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.javascript.jscomp.NodeUtil.Visitor;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.Map;
/**
* A Class to assist in AST change tracking verification. To validate a "snapshot" is taken
* before and "checkRecordedChanges" at the desired check point.
*/
public class ChangeVerifier {
private final AbstractCompiler compiler;
private final Map<Node, Node> map = new HashMap<>();
private int snapshotChange;
ChangeVerifier(AbstractCompiler compiler) {
this.compiler = compiler;
}
ChangeVerifier snapshot(Node root) {
// remove any existing snapshot data.
map.clear();
snapshotChange = compiler.getChangeStamp();
Node snapshot = root.cloneTree();
associateClones(root, snapshot);
return this;
}
void checkRecordedChanges(Node current) {
checkRecordedChanges("", current);
}
void checkRecordedChanges(String passName, Node root) {
verifyScopeChangesHaveBeenRecorded(passName, root);
}
/**
* Given an AST and its copy, map the root node of each scope of main to the
* corresponding root node of clone
*/
private void associateClones(Node n, Node snapshot) {
// TODO(johnlenz): determine if MODULE_BODY is useful here.
if (NodeUtil.isChangeScopeRoot(n)) {
map.put(n, snapshot);
}
Node child = n.getFirstChild();
Node snapshotChild = snapshot.getFirstChild();
while (child != null) {
associateClones(child, snapshotChild);
child = child.getNext();
snapshotChild = snapshotChild.getNext();
}
}
/** Checks that the scope roots marked as changed have indeed changed */
private void verifyScopeChangesHaveBeenRecorded(
String passName, Node root) {
final String passNameMsg = passName.isEmpty() ? "" : passName + ": ";
NodeUtil.visitPreOrder(root,
new Visitor() {
@Override
public void visit(Node n) {
if (n.isRoot()) {
verifyRoot(n);
} else if (NodeUtil.isChangeScopeRoot(n)) {
Node clone = map.get(n);
if (clone == null) {
verifyNewNode(passNameMsg, n);
} else {
verifyNodeChange(passNameMsg, n, clone);
}
}
}
},
Predicates.<Node>alwaysTrue());
}
private void verifyNewNode(String passNameMsg, Node n) {
int changeTime = n.getChangeTime();
if (changeTime == 0 || changeTime < snapshotChange) {
throw new IllegalStateException(
passNameMsg + "new scope not explicitly marked as changed: " + n.toStringTree());
}
}
private void verifyRoot(Node root) {
Preconditions.checkState(root.isRoot());
if (root.getChangeTime() != 0) {
throw new IllegalStateException("Root nodes should never be marked as changed.");
}
}
private void verifyNodeChange(final String passNameMsg, Node n, Node snapshot) {
if (n.isRoot()) {
return;
}
if (n.getChangeTime() > snapshot.getChangeTime()) {
if (isEquivalentToExcludingFunctions(n, snapshot)) {
throw new IllegalStateException(
passNameMsg + "unchanged scope marked as changed: " + n.toStringTree());
}
} else {
if (!isEquivalentToExcludingFunctions(n, snapshot)) {
throw new IllegalStateException(
passNameMsg + "changed scope not marked as changed: " + n.toStringTree());
}
}
}
/**
* @return Whether the two node are equivalent while ignoring
* differences any descendant functions differences.
*/
private static boolean isEquivalentToExcludingFunctions(
Node thisNode, Node thatNode) {
if (thisNode == null || thatNode == null) {
return thisNode == null && thatNode == null;
}
if (!thisNode.isEquivalentWithSideEffectsToShallow(thatNode)) {
return false;
}
if (thisNode.getChildCount() != thatNode.getChildCount()) {
return false;
}
Node thisChild = thisNode.getFirstChild();
Node thatChild = thatNode.getFirstChild();
while (thisChild != null && thatChild != null) {
if (thisChild.isFunction() || thisChild.isScript()) {
// Don't compare function expression name, parameters or bodies.
// But do check that that the node is there.
if (thatChild.getToken() != thisChild.getToken()) {
return false;
}
// Only compare function names for function declarations (not function expressions)
// as they change the outer scope definition.
if (thisChild.isFunction() && NodeUtil.isFunctionDeclaration(thisChild)) {
String thisName = thisChild.getFirstChild().getString();
String thatName = thatChild.getFirstChild().getString();
if (!thisName.equals(thatName)) {
return false;
}
}
} else if (!isEquivalentToExcludingFunctions(thisChild, thatChild)) {
return false;
}
thisChild = thisChild.getNext();
thatChild = thatChild.getNext();
}
return true;
}
}