1515 */
1616package com .google .javascript .jscomp ;
1717
18+ import static com .google .common .base .Preconditions .checkNotNull ;
1819import static com .google .common .base .Preconditions .checkState ;
1920
2021import com .google .common .collect .BiMap ;
2122import com .google .common .collect .HashBiMap ;
2223import com .google .errorprone .annotations .CanIgnoreReturnValue ;
2324import com .google .javascript .jscomp .NodeUtil .Visitor ;
2425import com .google .javascript .rhino .Node ;
26+ import java .util .ArrayList ;
27+ import java .util .Collections ;
2528import java .util .LinkedHashSet ;
2629import java .util .Set ;
30+ import java .util .function .Supplier ;
2731
2832/**
2933 * A Class to assist in AST change tracking verification. To validate a "snapshot" is taken
@@ -180,15 +184,30 @@ private void verifyNodeChange(final String passNameMsg, Node n, Node snapshot) {
180184 if (n .isRoot ()) {
181185 return ;
182186 }
187+ EqualsResult result = getInequivalenceReasonExcludingFunctions (n , snapshot );
183188 if (n .getChangeTime () > snapshot .getChangeTime ()) {
184- if (equalsExcludingFunctions (n , snapshot )) {
189+ // If the current node is marked as changed (changeTime > snapshot.getChangeTime)
190+ // but is actually equal to the snapshot, that's an error.
191+ if (result .equals ()) {
185192 throw new IllegalStateException (
186193 passNameMsg + "unchanged scope marked as changed: " + getNameForNode (n ));
187194 }
188195 } else {
189- if (!equalsExcludingFunctions (n , snapshot )) {
196+ // If the current node is NOT marked as changed (changeTime <= snapshot.getChangeTime)
197+ // but is actually different from the snapshot, that's an error.
198+ if (!result .equals ()) {
190199 throw new IllegalStateException (
191- passNameMsg + "changed scope not marked as changed: " + getNameForNode (n ));
200+ String .format (
201+ """
202+ "%schanged scope not marked as changed: %s.
203+ %s
204+ Ancestor nodes:
205+ %s
206+ """ ,
207+ passNameMsg ,
208+ getNameForNode (n ),
209+ result .errorMessage .get (),
210+ path (n , result .errorNode ())));
192211 }
193212 }
194213 }
@@ -210,30 +229,65 @@ String getNameForNode(Node n) {
210229 }
211230 }
212231
232+ /** Returns the path from the ancestor to the child node. */
233+ private String path (Node ancestor , Node child ) {
234+ ArrayList <String > childToAncestor = new ArrayList <>();
235+ for (Node current = child ; current != ancestor ; current = current .getParent ()) {
236+ childToAncestor .add (current .toString ());
237+ }
238+ childToAncestor .add (ancestor .toString ());
239+ Collections .reverse (childToAncestor );
240+ StringBuilder result = new StringBuilder ();
241+ for (int i = 0 ; i < childToAncestor .size (); i ++) {
242+ result .repeat (' ' , i * 2 ); // indent
243+ result .append (childToAncestor .get (i ));
244+ result .append ("\n " );
245+ }
246+ return result .toString ();
247+ }
248+ 249+ private record EqualsResult (boolean equals , Node errorNode , Supplier <String > errorMessage ) {
250+ static EqualsResult equal () {
251+ return new EqualsResult (true , null , () -> null );
252+ }
253+ 254+ static EqualsResult notEqual (Node errorNode , String error , Object after , Object before ) {
255+ return new EqualsResult (
256+ false ,
257+ errorNode ,
258+ () ->
259+ String .format (
260+ """
261+ %s
262+ Before: %s
263+ After: %s
264+ """ ,
265+ error , before , after ));
266+ }
267+ }
268+ 213269 /**
214- * @return Whether the two node are equivalent while ignoring differences any descendant functions
215- * differences .
270+ * Checks whether the two given nodes are equivalent, while ignoring differences in descendant
271+ * functions .
216272 */
217- private static boolean equalsExcludingFunctions (Node thisNode , Node thatNode ) {
218- if (thisNode == null || thatNode == null ) {
219- return thisNode == null && thatNode == null ;
273+ private static EqualsResult getInequivalenceReasonExcludingFunctions (
274+ Node thisNode , Node thatNode ) {
275+ checkNotNull (thisNode );
276+ checkNotNull (thatNode );
277+ if (thisNode .getChildCount () != thatNode .getChildCount ()) {
278+ return EqualsResult .notEqual (
279+ thisNode ,
280+ "differing child count" ,
281+ thisNode + ": " + thisNode .getChildCount (),
282+ thatNode + ": " + thatNode .getChildCount ());
220283 }
221284 if (!thisNode .isEquivalentWithSideEffectsToShallow (thatNode )) {
222- return false ;
223- }
224- if (thisNode .getChildCount () != thatNode .getChildCount ()) {
225- return false ;
285+ return EqualsResult .notEqual (thisNode , "shallow inequivalence" , thisNode , thatNode );
226286 }
227- 228287 if (thisNode .isFunction () && thatNode .isFunction ()) {
229288 if (NodeUtil .isFunctionDeclaration (thisNode ) != NodeUtil .isFunctionDeclaration (thatNode )) {
230- return false ;
231- }
232- }
233- 234- if (thisNode .hasParent () && thisNode .getParent ().isParamList ()) {
235- if (thisNode .isUnusedParameter () != thatNode .isUnusedParameter ()) {
236- return false ;
289+ return EqualsResult .notEqual (
290+ thisNode , "mismatched isFunctionDeclaration" , thisNode , thatNode );
237291 }
238292 }
239293
@@ -244,25 +298,27 @@ private static boolean equalsExcludingFunctions(Node thisNode, Node thatNode) {
244298 // Don't compare function expression name, parameters or bodies.
245299 // But do check that that the node is there.
246300 if (thatChild .getToken () != thisChild .getToken ()) {
247- return false ;
301+ return EqualsResult . notEqual ( thisNode , "different tokens" , thisChild , thatChild ) ;
248302 }
249303 // Only compare function names for function declarations (not function expressions)
250304 // as they change the outer scope definition.
251305 if (thisChild .isFunction () && NodeUtil .isFunctionDeclaration (thisChild )) {
252306 String thisName = thisChild .getFirstChild ().getString ();
253307 String thatName = thatChild .getFirstChild ().getString ();
254308 if (!thisName .equals (thatName )) {
255- return false ;
309+ return EqualsResult . notEqual ( thisNode , "function name changed" , thisName , thatName ) ;
256310 }
257311 }
258- } else if (!equalsExcludingFunctions (thisChild , thatChild )) {
259- return false ;
312+ } else {
313+ EqualsResult result = getInequivalenceReasonExcludingFunctions (thisChild , thatChild );
314+ if (!result .equals ()) {
315+ return result ;
316+ }
260317 }
261318 thisChild = thisChild .getNext ();
262319 thatChild = thatChild .getNext ();
263320 }
264321
265- return true ;
322+ return EqualsResult . equal () ;
266323 }
267324}
268-
0 commit comments