-
-
Notifications
You must be signed in to change notification settings - Fork 353
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(experimental): add ChangeCollector to listen on changes and reme…
…mbers them (#1941)
- Loading branch information
1 parent
4c02f59
commit 252d525
Showing
3 changed files
with
317 additions
and
0 deletions.
There are no files selected for viewing
228 changes: 228 additions & 0 deletions
228
src/main/java/spoon/experimental/modelobs/ChangeCollector.java
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,228 @@ | ||
/** | ||
* Copyright (C) 2006-2017 INRIA and contributors | ||
* Spoon - http://spoon.gforge.inria.fr/ | ||
* | ||
* This software is governed by the CeCILL-C License under French law and | ||
* abiding by the rules of distribution of free software. You can use, modify | ||
* and/or redistribute the software under the terms of the CeCILL-C license as | ||
* circulated by CEA, CNRS and INRIA at http://www.cecill.info. | ||
* | ||
* This program is distributed in the hope that it will be useful, but WITHOUT | ||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. | ||
* | ||
* The fact that you are presently reading this means that you have had | ||
* knowledge of the CeCILL-C license and that you accept its terms. | ||
*/ | ||
package spoon.experimental.modelobs; | ||
|
||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashSet; | ||
import java.util.IdentityHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
import spoon.compiler.Environment; | ||
import spoon.reflect.CtModel; | ||
import spoon.reflect.declaration.CtElement; | ||
import spoon.reflect.declaration.ModifierKind; | ||
import spoon.reflect.path.CtRole; | ||
import spoon.reflect.visitor.EarlyTerminatingScanner; | ||
import spoon.reflect.visitor.chain.CtScannerListener; | ||
import spoon.reflect.visitor.chain.ScanningMode; | ||
|
||
/** | ||
* Listens on changes on the spoon model and remembers them | ||
*/ | ||
public class ChangeCollector { | ||
private final Map<CtElement, Set<CtRole>> elementToChangeRole = new IdentityHashMap<>(); | ||
private final ChangeListener changeListener = new ChangeListener(); | ||
|
||
/** | ||
* @param env to be checked {@link Environment} | ||
* @return {@link ChangeCollector} attached to the `env` or null if there is none | ||
*/ | ||
public static ChangeCollector getChangeCollector(Environment env) { | ||
FineModelChangeListener mcl = env.getModelChangeListener(); | ||
if (mcl instanceof ChangeListener) { | ||
return ((ChangeListener) mcl).getChangeCollector(); | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Attaches itself to {@link CtModel} to listen to all changes of it's child elements | ||
* TODO: it would be nicer if we might listen on changes on {@link CtElement} | ||
* @param env to be attached to {@link Environment} | ||
* @return this to support fluent API | ||
*/ | ||
public ChangeCollector attachTo(Environment env) { | ||
env.setModelChangeListener(changeListener); | ||
return this; | ||
} | ||
|
||
/** | ||
* @param currentElement the {@link CtElement} whose changes has to be checked | ||
* @return set of {@link CtRole}s whose attribute was directly changed on `currentElement` since this {@link ChangeCollector} was attached | ||
* The 'directly' means that value of attribute of `currentElement` was changed. | ||
* Use {@link #getChanges(CtElement)} to detect changes in child elements too | ||
*/ | ||
public Set<CtRole> getDirectChanges(CtElement currentElement) { | ||
Set<CtRole> changes = elementToChangeRole.get(currentElement); | ||
if (changes == null) { | ||
return Collections.emptySet(); | ||
} | ||
return Collections.unmodifiableSet(changes); | ||
} | ||
|
||
/** | ||
* @param currentElement the {@link CtElement} whose changes has to be checked | ||
* @return set of {@link CtRole}s whose attribute was changed on `currentElement` | ||
* or any child of this attribute was changed | ||
* since this {@link ChangeCollector} was attached | ||
*/ | ||
public Set<CtRole> getChanges(CtElement currentElement) { | ||
final Set<CtRole> changes = new HashSet<>(getDirectChanges(currentElement)); | ||
final Scanner scanner = new Scanner(); | ||
scanner.setListener(new CtScannerListener() { | ||
int depth = 0; | ||
CtRole checkedRole; | ||
@Override | ||
public ScanningMode enter(CtElement element) { | ||
if (depth == 0) { | ||
//we are checking children of role checkedRole | ||
checkedRole = scanner.getScannedRole(); | ||
} | ||
if (changes.contains(checkedRole)) { | ||
//we already know that som echild of `checkedRole` attribute is modified. Skip others | ||
return ScanningMode.SKIP_ALL; | ||
} | ||
if (elementToChangeRole.containsKey(element)) { | ||
//we have found a modified element in children of `checkedRole` | ||
changes.add(checkedRole); | ||
return ScanningMode.SKIP_ALL; | ||
} | ||
depth++; | ||
//continue searching for an modification | ||
return ScanningMode.NORMAL; | ||
} | ||
@Override | ||
public void exit(CtElement element) { | ||
depth--; | ||
} | ||
}); | ||
currentElement.accept(scanner); | ||
return Collections.unmodifiableSet(changes); | ||
} | ||
|
||
private static class Scanner extends EarlyTerminatingScanner<Void> { | ||
CtRole getScannedRole() { | ||
return scannedRole; | ||
} | ||
} | ||
|
||
/** | ||
* Called whenever anything changes in the spoon model | ||
* @param currentElement the modified element | ||
* @param role the modified attribute of that element | ||
*/ | ||
protected void onChange(CtElement currentElement, CtRole role) { | ||
Set<CtRole> roles = elementToChangeRole.get(currentElement); | ||
if (roles == null) { | ||
roles = new HashSet<>(); | ||
elementToChangeRole.put(currentElement, roles); | ||
} | ||
if (role.getSuperRole() != null) { | ||
role = role.getSuperRole(); | ||
} | ||
roles.add(role); | ||
} | ||
|
||
private class ChangeListener implements FineModelChangeListener { | ||
private ChangeCollector getChangeCollector() { | ||
return ChangeCollector.this; | ||
} | ||
|
||
@Override | ||
public void onObjectUpdate(CtElement currentElement, CtRole role, CtElement newValue, CtElement oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onObjectUpdate(CtElement currentElement, CtRole role, Object newValue, Object oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onObjectDelete(CtElement currentElement, CtRole role, CtElement oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onListAdd(CtElement currentElement, CtRole role, List field, CtElement newValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onListAdd(CtElement currentElement, CtRole role, List field, int index, CtElement newValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onListDelete(CtElement currentElement, CtRole role, List field, Collection<? extends CtElement> oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onListDelete(CtElement currentElement, CtRole role, List field, int index, CtElement oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onListDeleteAll(CtElement currentElement, CtRole role, List field, List oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public <K, V> void onMapAdd(CtElement currentElement, CtRole role, Map<K, V> field, K key, CtElement newValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public <K, V> void onMapDeleteAll(CtElement currentElement, CtRole role, Map<K, V> field, Map<K, V> oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onSetAdd(CtElement currentElement, CtRole role, Set field, CtElement newValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public <T extends Enum> void onSetAdd(CtElement currentElement, CtRole role, Set field, T newValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onSetDelete(CtElement currentElement, CtRole role, Set field, CtElement oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onSetDelete(CtElement currentElement, CtRole role, Set field, Collection<ModifierKind> oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onSetDelete(CtElement currentElement, CtRole role, Set field, ModifierKind oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
|
||
@Override | ||
public void onSetDeleteAll(CtElement currentElement, CtRole role, Set field, Set oldValue) { | ||
onChange(currentElement, role); | ||
} | ||
} | ||
} |
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,80 @@ | ||
package spoon.test.change; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertNull; | ||
import static org.junit.Assert.assertSame; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
|
||
import org.junit.Test; | ||
|
||
import spoon.experimental.modelobs.ChangeCollector; | ||
import spoon.reflect.cu.CompilationUnit; | ||
import spoon.reflect.declaration.CtElement; | ||
import spoon.reflect.declaration.CtField; | ||
import spoon.reflect.declaration.CtType; | ||
import spoon.reflect.factory.Factory; | ||
import spoon.reflect.path.CtRole; | ||
import spoon.test.change.testclasses.SubjectOfChange; | ||
import spoon.testing.utils.ModelUtils; | ||
|
||
public class ChangeCollectorTest { | ||
|
||
@Test | ||
public void testChangeCollector() throws Exception { | ||
//contract: test ChangeCollector | ||
CtType<?> ctClass = ModelUtils.buildClass(SubjectOfChange.class); | ||
|
||
Factory f = ctClass.getFactory(); | ||
|
||
assertNull(ChangeCollector.getChangeCollector(f.getEnvironment())); | ||
|
||
ChangeCollector changeCollector = new ChangeCollector().attachTo(f.getEnvironment()); | ||
|
||
assertSame(changeCollector, ChangeCollector.getChangeCollector(f.getEnvironment())); | ||
|
||
//contract: after ChangeCollector is created there is no direct or indirect change | ||
assertEquals(0, changeCollector.getChanges(f.getModel().getRootPackage()).size()); | ||
f.getModel().getRootPackage().filterChildren(null).forEach((CtElement e) -> { | ||
assertEquals(0, changeCollector.getDirectChanges(e).size()); | ||
}); | ||
|
||
ctClass.setSimpleName("aaa"); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.SUB_PACKAGE)), changeCollector.getChanges(f.getModel().getRootPackage())); | ||
assertEquals(new HashSet<>(), changeCollector.getDirectChanges(f.getModel().getRootPackage())); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.CONTAINED_TYPE)), changeCollector.getChanges(ctClass.getPackage())); | ||
assertEquals(new HashSet<>(Arrays.asList()), changeCollector.getDirectChanges(ctClass.getPackage())); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME)), changeCollector.getChanges(ctClass)); | ||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME)), changeCollector.getDirectChanges(ctClass)); | ||
|
||
CtField<?> field = ctClass.getField("someField"); | ||
field.getDefaultExpression().delete(); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME, CtRole.TYPE_MEMBER)), changeCollector.getChanges(ctClass)); | ||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME)), changeCollector.getDirectChanges(ctClass)); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.DEFAULT_EXPRESSION)), changeCollector.getChanges(field)); | ||
assertEquals(new HashSet<>(Arrays.asList(CtRole.DEFAULT_EXPRESSION)), changeCollector.getDirectChanges(field)); | ||
|
||
|
||
/* | ||
* TODO: | ||
* field.delete(); | ||
* calls internally setTypeMembers, which deletes everything and then adds remaining | ||
*/ | ||
ctClass.removeTypeMember(field); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME, CtRole.TYPE_MEMBER)), changeCollector.getChanges(ctClass)); | ||
assertEquals(new HashSet<>(Arrays.asList(CtRole.NAME, CtRole.TYPE_MEMBER)), changeCollector.getDirectChanges(ctClass)); | ||
|
||
assertEquals(new HashSet<>(Arrays.asList(CtRole.DEFAULT_EXPRESSION)), changeCollector.getChanges(field)); | ||
assertEquals(new HashSet<>(Arrays.asList(CtRole.DEFAULT_EXPRESSION)), changeCollector.getDirectChanges(field)); | ||
|
||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/test/java/spoon/test/change/testclasses/SubjectOfChange.java
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,9 @@ | ||
package spoon.test.change.testclasses; | ||
|
||
public class SubjectOfChange { | ||
|
||
public SubjectOfChange() { | ||
} | ||
|
||
int someField = 1; | ||
} |