Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

review fix: contract CtAnnotation and array of values #1935

Merged
merged 14 commits into from
Apr 4, 2018
19 changes: 19 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtAnnotation.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public interface CtAnnotation<A extends Annotation> extends CtExpression<A>, CtS

/**
* Gets a value for a given key without any conversion.
* Note that this value type does not necessarily corresponds to the annotation
* type member. For example, in case the annotation type expect an array of Object,
* and a single value is given, Spoon will return only the object without the CtNewArray.
* If you want to get a type closer to the annotation type one, see {@link #getTypedValue(String)}.
*
* @param key
* Name of searched value.
Expand All @@ -77,6 +81,21 @@ public interface CtAnnotation<A extends Annotation> extends CtExpression<A>, CtS
@PropertyGetter(role = VALUE)
<T extends CtExpression> T getValue(String key);

/**
* Gets a value for a given key and try to fix its type based on the
* annotation type. For example, if the annotation type member expects an array of String,
* and it can be resolved, this method will return a CtNewArray instead of a CtLiteral.
*
* Warning: the returned element might be detached from the model
*
* @param key
* Name of searched value.
* @return the value expression or null if not found.
*/
@DerivedProperty
@PropertyGetter(role = VALUE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add @Derived to this method... otherwise the metamodel based code will be confused.

<T extends CtExpression> T getTypedValue(String key);

/**
* Returns this annotation's elements and their values. This is returned in
* the form of a map that associates element names with their corresponding
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/spoon/reflect/declaration/CtElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public interface CtElement extends FactoryAccessor, CtVisitable, Cloneable, CtQu
* Searches for an annotation of the given class that annotates the
* current element.
*
* When used with a shadow element, this method might return an empty list even on an annotated element
* because annotations without a RUNTIME retention policy are lost after compilation.
*
* WARNING: this method uses a class loader proxy, which is costly.
* Use {@link #getAnnotation(CtTypeReference)} preferably.
*
Expand All @@ -71,6 +74,9 @@ public interface CtElement extends FactoryAccessor, CtVisitable, Cloneable, CtQu
/**
* Gets the annotation element for a given annotation type.
*
* When used with a shadow element, this method might return an empty list even on an annotated element
* because annotations without a RUNTIME retention policy are lost after compilation.
*
* @param annotationType
* the annotation type
* @return the annotation if this element is annotated by one annotation of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import spoon.reflect.declaration.CtType;
import spoon.reflect.eval.PartialEvaluator;
import spoon.reflect.path.CtRole;
import spoon.reflect.reference.CtArrayTypeReference;
import spoon.reflect.reference.CtFieldReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtVisitor;
Expand Down Expand Up @@ -357,6 +358,31 @@ public <T extends CtExpression> T getValue(String key) {
return (T) this.elementValues.get(key);
}

@Override
public <T extends CtExpression> T getTypedValue(String key) {
CtExpression ctExpression = this.getValue(key);

if (ctExpression instanceof CtLiteral) {
CtTypeReference typeReference = this.getAnnotationType();
CtType type = typeReference.getDeclaration();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use getTypeDeclaration?

if (type != null) {
CtMethod method = type.getMethod(key);
if (method != null) {
CtTypeReference returnType = method.getType();
if (returnType instanceof CtArrayTypeReference) {
CtNewArray newArray = getFactory().Core().createNewArray();
CtArrayTypeReference typeReference2 = this.getFactory().createArrayTypeReference();
typeReference2.setComponentType(ctExpression.getType().clone());
newArray.setType(typeReference2);
newArray.addElement(ctExpression.clone());
return (T) newArray;
}
}
}
}
return (T) ctExpression;
}

public Map<String, Object> getElementValues() {
TreeMap<String, Object> res = new TreeMap<>();
for (Entry<String, CtExpression> elementValue : elementValues.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package spoon.support.reflect.declaration;

import org.apache.log4j.Logger;
import spoon.Launcher;
import spoon.SpoonException;
import spoon.reflect.CtModelImpl;
import spoon.reflect.annotations.MetamodelPropertyField;
Expand All @@ -27,6 +28,7 @@
import spoon.reflect.declaration.CtAnnotation;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtNamedElement;
import spoon.reflect.declaration.CtShadowable;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.ParentNotInitializedException;
import spoon.reflect.factory.Factory;
Expand Down Expand Up @@ -173,6 +175,12 @@ public <A extends Annotation> CtAnnotation<A> getAnnotation(CtTypeReference<A> a
}

public List<CtAnnotation<? extends Annotation>> getAnnotations() {
if (this instanceof CtShadowable) {
CtShadowable shadowable = (CtShadowable) this;
if (shadowable.isShadow()) {
Launcher.LOGGER.debug("Some annotations might be unreachable from the shadow element: " + this.getShortRepresentation());
}
}
return unmodifiableList(annotations);
}

Expand Down
112 changes: 112 additions & 0 deletions src/test/java/spoon/test/annotation/AnnotationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import spoon.SpoonException;
import spoon.processing.AbstractAnnotationProcessor;
import spoon.processing.ProcessingManager;
import spoon.reflect.CtModel;
import spoon.reflect.annotations.PropertyGetter;
import spoon.reflect.code.CtBlock;
import spoon.reflect.code.CtConstructorCall;
Expand Down Expand Up @@ -66,6 +67,7 @@
import spoon.test.annotation.testclasses.repeatable.Tag;
import spoon.test.annotation.testclasses.repeatandarrays.RepeatedArrays;
import spoon.test.annotation.testclasses.repeatandarrays.TagArrays;
import spoon.test.annotation.testclasses.shadow.DumbKlass;
import spoon.test.annotation.testclasses.spring.AliasFor;
import spoon.test.annotation.testclasses.typeandfield.SimpleClass;

Expand All @@ -74,11 +76,13 @@
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import static org.hamcrest.core.Is.is;
Expand All @@ -92,6 +96,7 @@
import static org.junit.Assert.fail;
import static spoon.testing.utils.ModelUtils.buildClass;
import static spoon.testing.utils.ModelUtils.canBeBuilt;
import static spoon.testing.utils.ModelUtils.createFactory;

public class AnnotationTest {

Expand Down Expand Up @@ -1392,4 +1397,111 @@ public void testAnnotationTypeAndFieldOnField() throws IOException {
assertTrue("Content :"+fileContent, fileContent.contains("@spoon.test.annotation.testclasses.typeandfield.AnnotTypeAndField"));
assertTrue("Content :"+fileContent, fileContent.contains("public java.lang.String mandatoryField;"));
}

@Test
public void testAnnotationAndShadowDefaultRetentionPolicy() {
// contract: When the default retention policy is used in an annotation, it's lost in shadow classes
final Launcher launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/annotation/testclasses/shadow");
CtModel model = launcher.buildModel();
CtClass<?> dumbKlass = model.getElements(new NamedElementFilter<CtClass>(CtClass.class, "DumbKlass")).get(0);
CtMethod<?> fooMethod = dumbKlass.getMethodsByName("foo").get(0);

final Factory shadowFactory = createFactory();
CtType<?> shadowDumbKlass = shadowFactory.Type().get(DumbKlass.class);
CtMethod<?> shadowFooMethod = shadowDumbKlass.getMethodsByName("foo").get(0);

assertEquals(0, shadowFooMethod.getAnnotations().size());
}

@Test
public void testAnnotationAndShadowClassRetentionPolicy() {
// contract: When the Class retention policy is used in an annotation, it's lost in shadow classes
final Launcher launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/annotation/testclasses/shadow");
CtModel model = launcher.buildModel();
CtClass<?> dumbKlass = model.getElements(new NamedElementFilter<>(CtClass.class, "DumbKlass")).get(0);
CtMethod<?> fooMethod = dumbKlass.getMethodsByName("fooClass").get(0);

final Factory shadowFactory = createFactory();
CtType<?> shadowDumbKlass = shadowFactory.Type().get(DumbKlass.class);
CtMethod<?> shadowFooMethod = shadowDumbKlass.getMethodsByName("fooClass").get(0);

assertEquals(0, shadowFooMethod.getAnnotations().size());
}

@Test
public void testAnnotationAndShadowRuntimeRetentionPolicy() {
// contract: When the runtime retention policy is used in an annotation, it's available through shadow classes
final Launcher launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/annotation/testclasses/shadow");
CtModel model = launcher.buildModel();
CtClass<?> dumbKlass = model.getElements(new NamedElementFilter<>(CtClass.class, "DumbKlass")).get(0);
CtMethod<?> fooMethod = dumbKlass.getMethodsByName("barOneValue").get(0);

final Factory shadowFactory = createFactory();
CtType<?> shadowDumbKlass = shadowFactory.Type().get(DumbKlass.class);
CtMethod<?> shadowFooMethod = shadowDumbKlass.getMethodsByName("barOneValue").get(0);

assertEquals(fooMethod.getAnnotations().size(), shadowFooMethod.getAnnotations().size());
}

@Test
public void testAnnotationArray() throws Exception {
// contract: getValue should return a value as close as possible from the sourcecode:
// i.e. even if the annotation should return an Array, it should return a single element
// if the value is given without the braces. The same behaviour should be used both for
// spooned source code and shadow classes.

Method barOneValueMethod = DumbKlass.class.getMethod("barOneValue");
Method barMultipleValueMethod = DumbKlass.class.getMethod("barMultipleValues");

Annotation annotationOneValue = barOneValueMethod.getAnnotations()[0];
Annotation annotationMultiple = barMultipleValueMethod.getAnnotations()[0];

Object oneValue = annotationOneValue.getClass().getMethod("role").invoke(annotationOneValue);
Object multipleValue = annotationMultiple.getClass().getMethod("role").invoke(annotationMultiple);

// in Java both values are String arrays with same values
assertTrue("[Java] annotation are not arrays type", oneValue instanceof String[] && multipleValue instanceof String[]);
assertEquals("[Java] annotation string values are not the same", ((String[]) oneValue)[0], ((String[]) multipleValue)[0]);

// in shadow classes, same behaviour: both annotation have the same values
final Factory shadowFactory = createFactory();
CtType<?> shadowDumbKlass = shadowFactory.Type().get(DumbKlass.class);
CtMethod<?> shadowBarOne = shadowDumbKlass.getMethodsByName("barOneValue").get(0);
CtAnnotation shadowAnnotationOne = shadowBarOne.getAnnotations().get(0);

CtMethod<?> shadowMultiple = shadowDumbKlass.getMethodsByName("barMultipleValues").get(0);
CtAnnotation shadowAnnotationMultiple = shadowMultiple.getAnnotations().get(0);

// FIXME: this should change
assertEquals("[Shadow] Annotation one and multiple are not of the same type", shadowAnnotationOne.getAnnotationType(), shadowAnnotationMultiple.getAnnotationType());
assertEquals("[Shadow] Annotation one and multiples values are not the same", shadowAnnotationOne.getValue("role"), shadowAnnotationMultiple.getValue("role"));

// but with Spoon, we consider two different values
final Launcher launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/annotation/testclasses/shadow");
CtModel model = launcher.buildModel();
CtClass<?> dumbKlass = model.getElements(new NamedElementFilter<>(CtClass.class, "DumbKlass")).get(0);
CtMethod<?> barOneValue = dumbKlass.getMethodsByName("barOneValue").get(0);
CtAnnotation annotationOne = barOneValue.getAnnotations().get(0);

CtMethod<?> barMultipleValue = dumbKlass.getMethodsByName("barMultipleValues").get(0);
CtAnnotation annotationMultipleVal = barMultipleValue.getAnnotations().get(0);

assertEquals("[Spoon] Annotation one and multiple are not of the same type", annotationOne.getAnnotationType(), annotationMultipleVal.getAnnotationType());
assertTrue(annotationOne.getValue("role") instanceof CtLiteral);
assertTrue(annotationMultipleVal.getValue("role") instanceof CtNewArray);

assertTrue(annotationOne.getTypedValue("role") instanceof CtNewArray);
assertTrue(annotationMultipleVal.getTypedValue("role") instanceof CtNewArray);
assertEquals(annotationMultipleVal.getTypedValue("role"), annotationOne.getTypedValue("role"));

assertEquals(annotationOne.getAnnotationType(), shadowAnnotationOne.getAnnotationType());
// FIXME: this contract should be fixed in #1914
assertTrue(shadowAnnotationOne.getValue("role") instanceof CtNewArray); // should be CtLiteral
//assertEquals(annotationOne.getValue("role"), shadowAnnotationOne.getValue("role")); // should pass

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package spoon.test.annotation.testclasses.shadow;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(value = RetentionPolicy.CLASS)
public @interface ClassRetention {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package spoon.test.annotation.testclasses.shadow;

public class DumbKlass {

@StandardRetention
public void foo() { }

@ClassRetention
public void fooClass() {}

@RuntimeRetention(role = "bidule")
public void barOneValue() {}

@RuntimeRetention(role = { "bidule"})
public void barMultipleValues() { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package spoon.test.annotation.testclasses.shadow;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(value = RetentionPolicy.RUNTIME)
public @interface RuntimeRetention {
String[] role();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package spoon.test.annotation.testclasses.shadow;

public @interface StandardRetention {
}