Skip to content

Commit

Permalink
feat(experimental): add ChangeCollector to listen on changes and reme…
Browse files Browse the repository at this point in the history
…mbers them (#1941)
  • Loading branch information
pvojtechovsky authored and monperrus committed Apr 6, 2018
1 parent 4c02f59 commit 252d525
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 0 deletions.
228 changes: 228 additions & 0 deletions src/main/java/spoon/experimental/modelobs/ChangeCollector.java
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);
}
}
}
80 changes: 80 additions & 0 deletions src/test/java/spoon/test/change/ChangeCollectorTest.java
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));

}
}
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;
}

0 comments on commit 252d525

Please sign in to comment.