Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.mozilla.javascript.EqualObjectGraphs Maven / Gradle / Ivy
Go to download
Rhino is an open-source implementation of JavaScript written entirely in Java. It is typically
embedded into Java applications to provide scripting to end users.
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.mozilla.javascript.debug.DebuggableObject;
/**
* An object that implements deep equality test of objects, including their
* reference graph topology, that is in addition to establishing by-value
* equality of objects, it also establishes that their reachable object graphs
* have identical shape. It is capable of custom-comparing a wide range of
* various objects, including various Rhino Scriptables, Java arrays, Java
* Lists, and to some degree Java Maps and Sets (sorted Maps are okay, as well
* as Sets with elements that can be sorted using their Comparable
* implementation, and Maps whose keysets work the same). The requirement for
* sortable maps and sets is to ensure deterministic order of traversal, which
* is necessary for establishing structural equality of object graphs.
*
* An instance of this object is stateful in that it memoizes pairs of objects
* that already compared equal, so reusing an instance for repeated equality
* tests of potentially overlapping object graph is beneficial for performance
* as long as all equality test invocations returns true. Reuse is not advised
* after an equality test returned false since there is a heuristic in comparing
* cyclic data structures that can memoize false equalities if two cyclic data
* structures end up being unequal.
*/
final class EqualObjectGraphs {
private static final ThreadLocal instance = new ThreadLocal<>();
// Object pairs already known to be equal. Used to short-circuit repeated traversals of objects reachable through
// different paths as well as to detect structural inequality.
private final Map knownEquals = new IdentityHashMap<>();
// Currently compared objects; used to avoid infinite recursion over cyclic object graphs.
private final Map currentlyCompared = new IdentityHashMap<>();
static T withThreadLocal(java.util.function.Function action) {
final EqualObjectGraphs currEq = instance.get();
if (currEq == null) {
final EqualObjectGraphs eq = new EqualObjectGraphs();
instance.set(eq);
try {
return action.apply(eq);
} finally {
instance.set(null);
}
}
return action.apply(currEq);
}
boolean equalGraphs(Object o1, Object o2) {
if (o1 == o2) {
return true;
} else if (o1 == null || o2 == null) {
return false;
}
final Object curr2 = currentlyCompared.get(o1);
if (curr2 == o2) {
// Provisionally afford that if we're already recursively comparing
// (o1, o2) that they'll be equal. NOTE: this is the heuristic
// mentioned in the class JavaDoc that can drive memoizing false
// equalities if cyclic data structures end up being unequal.
// While it would be possible to fix that with additional code, the
// usual usage of equality comparisons is short-circuit-on-false anyway,
// so this edge case should not arise in normal usage and the additional
// code complexity to guard against it is not worth it.
return true;
} else if (curr2 != null) {
// If we're already recursively comparing o1 to some other object,
// this comparison is structurally false.
return false;
}
final Object prev2 = knownEquals.get(o1);
if (prev2 == o2) {
// o1 known to be equal to o2.
return true;
} else if (prev2 != null) {
// o1 known to be equal to something other than o2.
return false;
}
final Object prev1 = knownEquals.get(o2);
assert prev1 != o1; // otherwise we would've already returned at prev2 == o2
if (prev1 != null) {
// o2 known to be equal to something other than o1.
return false;
}
currentlyCompared.put(o1, o2);
final boolean eq = equalGraphsNoMemo(o1, o2);
if (eq) {
knownEquals.put(o1, o2);
knownEquals.put(o2, o1);
}
currentlyCompared.remove(o1);
return eq;
}
private boolean equalGraphsNoMemo(Object o1, Object o2) {
if (o1 instanceof Wrapper) {
return o2 instanceof Wrapper && equalGraphs(((Wrapper)o1).unwrap(), ((Wrapper)o2).unwrap());
} else if (o1 instanceof Scriptable) {
return o2 instanceof Scriptable && equalScriptables((Scriptable)o1, (Scriptable)o2);
} else if (o1 instanceof ConsString) {
return ((ConsString)o1).toString().equals(o2);
} else if (o2 instanceof ConsString) {
return o1.equals(((ConsString)o2).toString());
} else if (o1 instanceof SymbolKey) {
return o2 instanceof SymbolKey && equalGraphs(((SymbolKey)o1).getName(), ((SymbolKey)o2).getName());
} else if (o1 instanceof Object[]) {
return o2 instanceof Object[] && equalObjectArrays((Object[])o1, (Object[])o2);
} else if (o1.getClass().isArray()) {
return Objects.deepEquals(o1, o2);
} else if (o1 instanceof List>) {
return o2 instanceof List> && equalLists((List>)o1, (List>)o2);
} else if (o1 instanceof Map, ?>) {
return o2 instanceof Map, ?> && equalMaps((Map, ?>)o1, (Map, ?>)o2);
} else if (o1 instanceof Set>) {
return o2 instanceof Set> && equalSets((Set>)o1, (Set>)o2);
} else if (o1 instanceof NativeGlobal) {
return o2 instanceof NativeGlobal; // stateless objects
} else if (o1 instanceof JavaAdapter) {
return o2 instanceof JavaAdapter; // stateless objects
} else if (o1 instanceof NativeJavaTopPackage) {
return o2 instanceof NativeJavaTopPackage; // stateless objects
}
// Fallback case for everything else.
return o1.equals(o2);
}
private boolean equalScriptables(final Scriptable s1, final Scriptable s2) {
final Object[] ids1 = getSortedIds(s1);
final Object[] ids2 = getSortedIds(s2);
if (!equalObjectArrays(ids1, ids2)) {
return false;
}
final int l = ids1.length;
for(int i = 0; i < l; ++i) {
if (!equalGraphs(getValue(s1, ids1[i]), getValue(s2, ids2[i]))) {
return false;
}
}
if (!equalGraphs(s1.getPrototype(), s2.getPrototype())) {
return false;
} else if (!equalGraphs(s1.getParentScope(), s2.getParentScope())) {
return false;
}
// Handle special Scriptable implementations
if (s1 instanceof NativeContinuation) {
return s2 instanceof NativeContinuation && NativeContinuation.equalImplementations((NativeContinuation)s1, (NativeContinuation)s2);
} else if (s1 instanceof NativeJavaPackage) {
return s1.equals(s2); // Overridden appropriately
} else if (s1 instanceof IdFunctionObject) {
return s2 instanceof IdFunctionObject && IdFunctionObject.equalObjectGraphs((IdFunctionObject)s1, (IdFunctionObject)s2, this);
} else if (s1 instanceof InterpretedFunction) {
return s2 instanceof InterpretedFunction && equalInterpretedFunctions((InterpretedFunction)s1, (InterpretedFunction)s2);
} else if (s1 instanceof ArrowFunction) {
return s2 instanceof ArrowFunction && ArrowFunction.equalObjectGraphs((ArrowFunction)s1, (ArrowFunction)s2, this);
} else if (s1 instanceof BoundFunction) {
return s2 instanceof BoundFunction && BoundFunction.equalObjectGraphs((BoundFunction)s1, (BoundFunction)s2, this);
} else if (s1 instanceof NativeSymbol) {
return s2 instanceof NativeSymbol && equalGraphs(((NativeSymbol)s1).getKey(), ((NativeSymbol)s2).getKey());
}
return true;
}
private boolean equalObjectArrays(final Object[] a1, final Object[] a2) {
if (a1.length != a2.length) {
return false;
}
for(int i = 0; i < a1.length; ++i) {
if (!equalGraphs(a1[i], a2[i])) {
return false;
}
}
return true;
}
private boolean equalLists(final List> l1, final List> l2) {
if (l1.size() != l2.size()) {
return false;
}
final Iterator> i1 = l1.iterator();
final Iterator> i2 = l2.iterator();
while(i1.hasNext() && i2.hasNext()) {
if (!equalGraphs(i1.next(), i2.next())) {
return false;
}
}
assert !(i1.hasNext() || i2.hasNext());
return true;
}
@SuppressWarnings("rawtypes")
private boolean equalMaps(final Map, ?> m1, final Map, ?> m2) {
if (m1.size() != m2.size()) {
return false;
}
final Iterator i1 = sortedEntries(m1);
final Iterator i2 = sortedEntries(m2);
while(i1.hasNext() && i2.hasNext()) {
final Map.Entry kv1 = i1.next();
final Map.Entry kv2 = i2.next();
if (!(equalGraphs(kv1.getKey(), kv2.getKey()) && equalGraphs(kv1.getValue(), kv2.getValue()))) {
return false;
}
}
assert !(i1.hasNext() || i2.hasNext());
// TODO: assert linked maps traversal order?
return true;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Iterator sortedEntries(final Map m) {
// Yes, this throws ClassCastException if the keys aren't comparable. That's okay. We only support maps with
// deterministic traversal order.
final Map sortedMap = (m instanceof SortedMap, ?> ? m : new TreeMap(m));
return sortedMap.entrySet().iterator();
}
private boolean equalSets(final Set> s1, final Set> s2) {
return equalObjectArrays(sortedSet(s1), sortedSet(s2));
}
private static Object[] sortedSet(final Set> s) {
final Object[] a = s.toArray();
Arrays.sort(a); // ClassCastException possible
return a;
}
private static boolean equalInterpretedFunctions(final InterpretedFunction f1, final InterpretedFunction f2) {
return Objects.equals(f1.getEncodedSource(), f2.getEncodedSource());
}
// Sort IDs deterministically
private static Object[] getSortedIds(final Scriptable s) {
final Object[] ids = getIds(s);
Arrays.sort(ids, (a, b) -> {
if (a instanceof Integer) {
if (b instanceof Integer) {
return ((Integer)a).compareTo((Integer)b);
} else if (b instanceof String || b instanceof Symbol) {
return -1; // ints before strings or symbols
}
} else if (a instanceof String) {
if (b instanceof String) {
return ((String)a).compareTo((String)b);
} else if (b instanceof Integer) {
return 1; // strings after ints
} else if (b instanceof Symbol) {
return -1; // strings before symbols
}
} else if (a instanceof Symbol) {
if (b instanceof Symbol) {
// As long as people bother to reasonably name their symbols,
// this will work. If there's clashes in symbol names (e.g.
// lots of unnamed symbols) it can lead to false inequalities.
return getSymbolName((Symbol)a).compareTo(getSymbolName((Symbol)b));
} else if (b instanceof Integer || b instanceof String) {
return 1; // symbols after ints and strings
}
}
// We can only compare Rhino key types: Integer, String, Symbol
throw new ClassCastException();
});
return ids;
}
private static String getSymbolName(final Symbol s) {
if (s instanceof SymbolKey) {
return ((SymbolKey)s).getName();
} else if (s instanceof NativeSymbol) {
return ((NativeSymbol)s).getKey().getName();
} else {
// We can only handle native Rhino Symbol types
throw new ClassCastException();
}
}
private static Object[] getIds(final Scriptable s) {
if (s instanceof ScriptableObject) {
// Grabs symbols too
return ((ScriptableObject)s).getIds(true, true);
} else if (s instanceof DebuggableObject) {
return ((DebuggableObject)s).getAllIds();
} else {
return s.getIds();
}
}
private static Object getValue(final Scriptable s, final Object id) {
if (id instanceof Symbol) {
return ScriptableObject.getProperty(s, (Symbol)id);
} else if (id instanceof Integer) {
return ScriptableObject.getProperty(s, ((Integer)id).intValue());
} else if (id instanceof String) {
return ScriptableObject.getProperty(s, (String)id);
} else {
throw new ClassCastException();
}
}
}