Skip to content

Commit 9770da0

Browse files
committed
ap: Validate event parameter annotations, and mark as incremental
1 parent ea2484a commit 9770da0

File tree

8 files changed

+467
-9
lines changed

8 files changed

+467
-9
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package org.spongepowered.plugin.processor;
2+
3+
import org.checkerframework.checker.nullness.qual.Nullable;
4+
import org.spongepowered.api.event.filter.Getter;
5+
import org.spongepowered.api.event.filter.cause.ContextValue;
6+
import org.spongepowered.api.event.filter.data.GetValue;
7+
import org.spongepowered.api.event.filter.data.Has;
8+
import org.spongepowered.api.event.filter.data.Supports;
9+
10+
import java.lang.annotation.Annotation;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Set;
15+
16+
import javax.lang.model.element.AnnotationValue;
17+
import javax.lang.model.element.Element;
18+
import javax.lang.model.element.ElementKind;
19+
import javax.lang.model.element.ExecutableElement;
20+
import javax.lang.model.element.Modifier;
21+
import javax.lang.model.element.Name;
22+
import javax.lang.model.element.TypeElement;
23+
import javax.lang.model.type.DeclaredType;
24+
import javax.lang.model.type.ExecutableType;
25+
import javax.lang.model.type.TypeKind;
26+
import javax.lang.model.type.TypeMirror;
27+
28+
public enum ListenerParameterAnnotation {
29+
CONTEXT_VALUE(ContextValue.class) {
30+
@Override
31+
void validate(final ParameterContext ctx) {
32+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
33+
if (entry.getKey().getSimpleName().contentEquals("value")) {
34+
final TypeElement key = ctx.elements().getTypeElement("org.spongepowered.api.event.EventContextKey");
35+
final TypeElement keys = ctx.elements().getTypeElement("org.spongepowered.api.event.EventContextKeys");
36+
final Element contained = ProcessorUtils.containingWithNameAndType(keys.getEnclosedElements(), entry.getValue().getValue().toString(), ElementKind.FIELD);
37+
if (contained == null) {
38+
ctx.logError("Could not find a context key matching the provided name", entry.getValue());
39+
return; // cannot resolve, not possible to provide more information
40+
}
41+
if (!contained.getModifiers().contains(Modifier.STATIC)) {
42+
ctx.logError("The @ContextValue annotation must refer to a static field", entry.getValue());
43+
}
44+
if (!ctx.types().isSubtype(contained.asType(), key.asType())) {
45+
ctx.logError("The @ContextValue annotation must refer to a field with type EventContextKey, but got '" + contained.asType() + "' instead", entry.getValue());
46+
}
47+
// validate field type?
48+
break;
49+
}
50+
}
51+
}
52+
},
53+
HAS(Has.class) {
54+
@Override
55+
void validate(final ParameterContext ctx) {
56+
ListenerParameterAnnotation.validateValueContainerChild("@Has", ctx);
57+
ListenerParameterAnnotation.validateKeyReference("@Has", ctx);
58+
}
59+
},
60+
SUPPORTS(Supports.class) {
61+
@Override
62+
void validate(final ParameterContext ctx) {
63+
ListenerParameterAnnotation.validateValueContainerChild("@Supports", ctx);
64+
ListenerParameterAnnotation.validateKeyReference("@Supports", ctx);
65+
}
66+
},
67+
GET_VALUE(GetValue.class) {
68+
@Override
69+
void validate(final ParameterContext ctx) {
70+
ListenerParameterAnnotation.validateKeyReference("@GetValue", ctx);
71+
}
72+
},
73+
GETTER(Getter.class) {
74+
75+
private boolean isOptional(final TypeMirror mirror) {
76+
return mirror.getKind() == TypeKind.DECLARED && ((DeclaredType) mirror).asElement().getSimpleName().contentEquals("Optional");
77+
}
78+
79+
@Override
80+
void validate(final ParameterContext ctx) {
81+
if (!ctx.event().isPresent()) {
82+
return;
83+
}
84+
final TypeElement event = ctx.event().get();
85+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
86+
if (entry.getKey().getSimpleName().contentEquals("value")) {
87+
final CharSequence getterName = (CharSequence) entry.getValue().getValue();
88+
final Element possible = ctx.elements().getAllMembers(event).stream()
89+
.filter(el -> el.getKind() == ElementKind.METHOD && el.getSimpleName().contentEquals(getterName))
90+
.filter(el -> ((ExecutableElement) el).getParameters().isEmpty())
91+
.findFirst().orElse(null);
92+
if (possible == null) {
93+
ctx.logError("No zero-argument method with this name found in event type '" + event.getSimpleName() + "'", entry.getValue());
94+
break;
95+
}
96+
97+
TypeMirror expectedType = ((ExecutableType) ctx.types().asMemberOf(ctx.eventType().get(), possible)).getReturnType();
98+
if (expectedType.getKind() == TypeKind.DECLARED) {// maybe Optional, if so unwrap
99+
final DeclaredType declared = (DeclaredType) expectedType;
100+
if (this.isOptional(declared) && declared.getTypeArguments().size() == 1 && !this.isOptional(ctx.param().asType())) {
101+
expectedType = declared.getTypeArguments().get(0);
102+
}
103+
}
104+
if (!ctx.types().isSameType(expectedType, ctx.param().asType())) {
105+
ctx.logParamError(
106+
"Annotated parameter was of incorrect type for the method referenced in @Getter. The parameter type should be '"
107+
+ expectedType + "'!"
108+
);
109+
}
110+
break;
111+
}
112+
}
113+
}
114+
}
115+
;
116+
117+
private static final Map<String, ListenerParameterAnnotation> BY_CLAZZ = new HashMap<>();
118+
private final String className;
119+
120+
ListenerParameterAnnotation(final Class<? extends Annotation> anno) {
121+
this.className = anno.getName();
122+
}
123+
124+
String className() {
125+
return this.className;
126+
}
127+
128+
abstract void validate(final ParameterContext ctx);
129+
130+
static @Nullable ListenerParameterAnnotation byClassName(final String annotation) {
131+
return ListenerParameterAnnotation.BY_CLAZZ.get(annotation);
132+
}
133+
134+
static void validateValueContainerChild(final String annotation, final ParameterContext ctx) {
135+
final TypeElement valueContainer = ctx.elements().getTypeElement("org.spongepowered.api.data.value.ValueContainer");
136+
if (valueContainer == null) {
137+
return;
138+
}
139+
if (!ctx.types().isAssignable(ctx.param().asType(), valueContainer.asType())) {
140+
ctx.logError(annotation + " is only applicable to parameters whose type is a subtype of ValueContainer");
141+
}
142+
}
143+
144+
static void validateKeyReference(final String annotation, final ParameterContext ctx) {
145+
TypeMirror container = null;
146+
String value = null;
147+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
148+
final Name name = entry.getKey().getSimpleName();
149+
if (name.contentEquals("container")) {
150+
container = (TypeMirror) entry.getValue().getValue();
151+
} else if (name.contentEquals("value")) {
152+
value = (String) entry.getValue().getValue();
153+
}
154+
if (container != null && value != null) {
155+
break;
156+
}
157+
}
158+
if (container == null) {
159+
// set to Keys
160+
container = ctx.elements().getTypeElement("org.spongepowered.api.data.Keys").asType();
161+
}
162+
163+
if (container.getKind() != TypeKind.DECLARED) { // otherwise incorrect
164+
return;
165+
}
166+
167+
final TypeElement key = ctx.elements().getTypeElement("org.spongepowered.api.data.Key");
168+
final TypeElement element = (TypeElement) ((DeclaredType) container).asElement();
169+
170+
final Element contained = ProcessorUtils.containingWithNameAndType(element.getEnclosedElements(), value, ElementKind.FIELD);
171+
if (contained == null) {
172+
ctx.logError("Could not find a matching Key in the specified container class");
173+
return; // cannot resolve, not possible to provide more information
174+
}
175+
176+
final Set<Modifier> modifiers = contained.getModifiers();
177+
if (!modifiers.contains(Modifier.STATIC) || !modifiers.contains(Modifier.PUBLIC)) {
178+
ctx.logError("The " + annotation + " annotation must refer to a public static field");
179+
}
180+
181+
if (!ctx.types().isAssignable(contained.asType(), ctx.types().erasure(key.asType()))) {
182+
ctx.logError("The " + annotation + " annotation must refer to a field with type Key, but got '" + contained.asType() + "' instead");
183+
}
184+
}
185+
186+
static {
187+
for (final ListenerParameterAnnotation element : ListenerParameterAnnotation.values()) {
188+
ListenerParameterAnnotation.BY_CLAZZ.put(element.className, element);
189+
}
190+
}
191+
}

src/ap/java/org/spongepowered/plugin/processor/ListenerProcessor.java

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,52 @@
2828

2929
import org.spongepowered.api.event.Event;
3030
import org.spongepowered.api.event.Listener;
31+
import org.spongepowered.api.event.filter.IsCancelled;
32+
import org.spongepowered.api.event.filter.type.Exclude;
33+
import org.spongepowered.api.event.filter.type.Include;
3134

35+
import java.util.Collections;
36+
import java.util.HashSet;
3237
import java.util.List;
38+
import java.util.Map;
3339
import java.util.Set;
3440
import javax.annotation.processing.AbstractProcessor;
35-
import javax.annotation.processing.Messager;
3641
import javax.annotation.processing.RoundEnvironment;
3742
import javax.annotation.processing.SupportedAnnotationTypes;
3843
import javax.annotation.processing.SupportedSourceVersion;
3944
import javax.lang.model.SourceVersion;
45+
import javax.lang.model.element.AnnotationMirror;
46+
import javax.lang.model.element.AnnotationValue;
4047
import javax.lang.model.element.Element;
4148
import javax.lang.model.element.ElementKind;
4249
import javax.lang.model.element.ExecutableElement;
4350
import javax.lang.model.element.Modifier;
4451
import javax.lang.model.element.TypeElement;
4552
import javax.lang.model.element.VariableElement;
53+
import javax.lang.model.type.DeclaredType;
4654
import javax.lang.model.type.TypeKind;
4755
import javax.lang.model.type.TypeMirror;
4856
import javax.lang.model.util.Elements;
4957
import javax.lang.model.util.Types;
50-
import javax.tools.Diagnostic;
5158

5259
@SupportedAnnotationTypes(ListenerProcessor.LISTENER_ANNOTATION_CLASS)
5360
@SupportedSourceVersion(SourceVersion.RELEASE_8)
5461
public class ListenerProcessor extends AbstractProcessor {
5562

5663
static final String LISTENER_ANNOTATION_CLASS = "org.spongepowered.api.event.Listener";
5764
private static final String EVENT_CLASS = Event.class.getName();
65+
private static final String IS_CANCELLED_ANNOTATION = IsCancelled.class.getName();
66+
private static final String INCLUDE_ANNOTATION = Include.class.getName();
67+
private static final String EXCLUDE_ANNOTATION = Exclude.class.getName();
68+
69+
@Override
70+
public Set<String> getSupportedAnnotationTypes() {
71+
final Set<String> types = new HashSet<>(super.getSupportedAnnotationTypes());
72+
for (final ListenerParameterAnnotation annotation : ListenerParameterAnnotation.values()) {
73+
types.add(annotation.className());
74+
}
75+
return Collections.unmodifiableSet(types);
76+
}
5877

5978
@Override
6079
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
@@ -66,32 +85,94 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
6685
}
6786
final ExecutableElement method = (ExecutableElement) e;
6887

69-
final Messager msg = this.processingEnv.getMessager();
7088
if (method.getModifiers().contains(Modifier.STATIC)) {
71-
msg.printMessage(Diagnostic.Kind.ERROR, "method must not be static", method);
89+
this.error("method must not be static", method);
7290
}
7391
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
74-
msg.printMessage(Diagnostic.Kind.ERROR, "method must be public", method);
92+
this.error("method must be public", method);
7593
}
7694
if (method.getModifiers().contains(Modifier.ABSTRACT)) {
77-
msg.printMessage(Diagnostic.Kind.ERROR, "method must not be abstract", method);
95+
this.error("method must not be abstract", method);
7896
}
7997
if (method.getEnclosingElement().getKind().isInterface()) {
80-
msg.printMessage(Diagnostic.Kind.ERROR, "interfaces cannot declare listeners", method);
98+
this.error( "interfaces cannot declare listeners", method);
8199
}
82100
if (method.getReturnType().getKind() != TypeKind.VOID) {
83-
msg.printMessage(Diagnostic.Kind.ERROR, "method must return void", method);
101+
this.error("method must return void", method);
84102
}
85103
final List<? extends VariableElement> parameters = method.getParameters();
104+
final DeclaredType eventType;
86105
if (parameters.isEmpty() || !this.isTypeSubclass(parameters.get(0), ListenerProcessor.EVENT_CLASS)) {
87-
msg.printMessage(Diagnostic.Kind.ERROR, "method must have an Event as its first parameter", method);
106+
this.error( "method must have an Event as its first parameter", method);
107+
eventType = null;
108+
} else {
109+
eventType = (DeclaredType) parameters.get(0).asType();
110+
}
111+
112+
final Types types = this.processingEnv.getTypeUtils();
113+
if (eventType != null) {
114+
for (final AnnotationMirror annotation : method.getAnnotationMirrors()) {
115+
final String name = this.processingEnv.getElementUtils()
116+
.getBinaryName((TypeElement) annotation.getAnnotationType().asElement()).toString();
117+
if (name.equals(ListenerProcessor.IS_CANCELLED_ANNOTATION)) {
118+
// ensure the event parameter inherits from Cancellable
119+
final TypeElement cancellable =
120+
this.processingEnv.getElementUtils().getTypeElement("org.spongepowered.api.event.Cancellable");
121+
if (cancellable != null && !types.isAssignable(eventType, cancellable.asType())) {
122+
this.error("A listener for a non-Cancellable method cannot be annotated with @IsCancelled", method);
123+
}
124+
} else if (name.equals(ListenerProcessor.INCLUDE_ANNOTATION) || name.equals(ListenerProcessor.EXCLUDE_ANNOTATION)) {
125+
// ensure that all referenced types are subtypes of Cancellable
126+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
127+
: annotation.getElementValues().entrySet()) {
128+
if (entry.getKey().getSimpleName().contentEquals("value")) {
129+
@SuppressWarnings("unchecked") final List<? extends AnnotationValue> values =
130+
(List<? extends AnnotationValue>) entry.getValue().getValue();
131+
for (final AnnotationValue subtype : values) {
132+
if (!types.isAssignable((TypeMirror) subtype.getValue(), eventType)) {
133+
this.error(
134+
"All filtered types must be subtypes of the event type '" + eventType.asElement().getSimpleName() + "'",
135+
method,
136+
annotation,
137+
subtype
138+
);
139+
}
140+
}
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
148+
final ParameterContext ctx = new ParameterContext(this.processingEnv, eventType);
149+
for (int i = 1; i < parameters.size(); ++i) {
150+
this.checkParameter(ctx, parameters.get(i));
88151
}
89152
}
90153
}
91154

92155
return false;
93156
}
94157

158+
/**
159+
* Check method parameters of event listeners for valid uses of event handler annotations
160+
*
161+
* @param element element to check
162+
*/
163+
private void checkParameter(final ParameterContext ctx, final VariableElement element) {
164+
// for every filtering annotation registered, check if this element is annotated
165+
// then, we get to
166+
for (final AnnotationMirror annotation : element.getAnnotationMirrors()) {
167+
ctx.init(element, annotation);
168+
final CharSequence name = this.processingEnv.getElementUtils().getBinaryName((TypeElement) annotation.getAnnotationType().asElement());
169+
final ListenerParameterAnnotation anno = ListenerParameterAnnotation.byClassName(name.toString());
170+
if (anno != null) {
171+
anno.validate(ctx);
172+
}
173+
}
174+
}
175+
95176
private boolean isTypeSubclass(final Element typedElement, final String subclass) {
96177
final Elements elements = this.processingEnv.getElementUtils();
97178
final Types types = this.processingEnv.getTypeUtils();
@@ -100,4 +181,14 @@ private boolean isTypeSubclass(final Element typedElement, final String subclass
100181
return types.isAssignable(typedElement.asType(), event);
101182
}
102183

184+
// Error collection
185+
186+
private void error(final CharSequence message, final Element element) {
187+
this.processingEnv.getMessager().printMessage(ERROR, message, element);
188+
}
189+
190+
private void error(final CharSequence message, final Element element, final AnnotationMirror annotation, final AnnotationValue value) {
191+
this.processingEnv.getMessager().printMessage(ERROR, message, element, annotation, value);
192+
}
193+
103194
}

0 commit comments

Comments
 (0)