Skip to content

Commit f9f2102

Browse files
committed
Merge branch 'api-8' into api-9
2 parents 042cdd2 + 483515a commit f9f2102

29 files changed

+933
-146
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* This file is part of SpongeAPI, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package org.spongepowered.plugin.processor;
26+
27+
import org.checkerframework.checker.nullness.qual.Nullable;
28+
import org.spongepowered.api.event.filter.Getter;
29+
import org.spongepowered.api.event.filter.cause.ContextValue;
30+
import org.spongepowered.api.event.filter.data.GetValue;
31+
import org.spongepowered.api.event.filter.data.Has;
32+
import org.spongepowered.api.event.filter.data.Supports;
33+
34+
import java.lang.annotation.Annotation;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
import java.util.Set;
38+
import javax.lang.model.element.AnnotationValue;
39+
import javax.lang.model.element.Element;
40+
import javax.lang.model.element.ElementKind;
41+
import javax.lang.model.element.ExecutableElement;
42+
import javax.lang.model.element.Modifier;
43+
import javax.lang.model.element.Name;
44+
import javax.lang.model.element.TypeElement;
45+
import javax.lang.model.type.DeclaredType;
46+
import javax.lang.model.type.ExecutableType;
47+
import javax.lang.model.type.TypeKind;
48+
import javax.lang.model.type.TypeMirror;
49+
50+
public enum ListenerParameterAnnotation {
51+
CONTEXT_VALUE(ContextValue.class) {
52+
@Override
53+
void validate(final ParameterContext ctx) {
54+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
55+
if (entry.getKey().getSimpleName().contentEquals("value")) {
56+
final TypeElement key = ctx.elements().getTypeElement("org.spongepowered.api.event.EventContextKey");
57+
final TypeElement keys = ctx.elements().getTypeElement("org.spongepowered.api.event.EventContextKeys");
58+
final Element contained = ProcessorUtils.containingWithNameAndType(keys.getEnclosedElements(), entry.getValue().getValue().toString(), ElementKind.FIELD);
59+
if (contained == null) {
60+
ctx.logError("Could not find a context key matching the provided name", entry.getValue());
61+
return; // cannot resolve, not possible to provide more information
62+
}
63+
if (!contained.getModifiers().contains(Modifier.STATIC)) {
64+
ctx.logError("The @ContextValue annotation must refer to a static field", entry.getValue());
65+
}
66+
if (!ctx.types().isSubtype(contained.asType(), key.asType())) {
67+
ctx.logError("The @ContextValue annotation must refer to a field with type EventContextKey, but got '" + contained.asType() + "' instead", entry.getValue());
68+
}
69+
// validate field type?
70+
break;
71+
}
72+
}
73+
}
74+
},
75+
HAS(Has.class) {
76+
@Override
77+
void validate(final ParameterContext ctx) {
78+
ListenerParameterAnnotation.validateValueContainerChild("@Has", ctx);
79+
ListenerParameterAnnotation.validateKeyReference("@Has", ctx);
80+
}
81+
},
82+
SUPPORTS(Supports.class) {
83+
@Override
84+
void validate(final ParameterContext ctx) {
85+
ListenerParameterAnnotation.validateValueContainerChild("@Supports", ctx);
86+
ListenerParameterAnnotation.validateKeyReference("@Supports", ctx);
87+
}
88+
},
89+
GET_VALUE(GetValue.class) {
90+
@Override
91+
void validate(final ParameterContext ctx) {
92+
ListenerParameterAnnotation.validateKeyReference("@GetValue", ctx);
93+
}
94+
},
95+
GETTER(Getter.class) {
96+
97+
private boolean isOptional(final TypeMirror mirror) {
98+
return mirror.getKind() == TypeKind.DECLARED && ((DeclaredType) mirror).asElement().getSimpleName().contentEquals("Optional");
99+
}
100+
101+
@Override
102+
void validate(final ParameterContext ctx) {
103+
if (!ctx.event().isPresent()) {
104+
return;
105+
}
106+
final TypeElement event = ctx.event().get();
107+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
108+
if (entry.getKey().getSimpleName().contentEquals("value")) {
109+
final CharSequence getterName = (CharSequence) entry.getValue().getValue();
110+
final Element possible = ctx.elements().getAllMembers(event).stream()
111+
.filter(el -> el.getKind() == ElementKind.METHOD && el.getSimpleName().contentEquals(getterName))
112+
.filter(el -> ((ExecutableElement) el).getParameters().isEmpty())
113+
.findFirst().orElse(null);
114+
if (possible == null) {
115+
ctx.logError("No zero-argument method with this name found in event type '" + event.getSimpleName() + "'", entry.getValue());
116+
break;
117+
}
118+
119+
TypeMirror expectedType = ((ExecutableType) ctx.types().asMemberOf(ctx.eventType().get(), possible)).getReturnType();
120+
if (expectedType.getKind() == TypeKind.DECLARED) { // maybe Optional, if so unwrap
121+
final DeclaredType declared = (DeclaredType) expectedType;
122+
if (this.isOptional(declared) && declared.getTypeArguments().size() == 1 && !this.isOptional(ctx.param().asType())) {
123+
expectedType = declared.getTypeArguments().get(0);
124+
}
125+
}
126+
if (!ctx.types().isSameType(expectedType, ctx.param().asType())) {
127+
ctx.logParamError(
128+
"Annotated parameter was of incorrect type for the method referenced in @Getter. The parameter type should be '"
129+
+ expectedType + "'!"
130+
);
131+
}
132+
break;
133+
}
134+
}
135+
}
136+
}
137+
;
138+
139+
private static final Map<String, ListenerParameterAnnotation> BY_CLAZZ = new HashMap<>();
140+
private final String className;
141+
142+
ListenerParameterAnnotation(final Class<? extends Annotation> anno) {
143+
this.className = anno.getName();
144+
}
145+
146+
String className() {
147+
return this.className;
148+
}
149+
150+
abstract void validate(final ParameterContext ctx);
151+
152+
static @Nullable ListenerParameterAnnotation byClassName(final String annotation) {
153+
return ListenerParameterAnnotation.BY_CLAZZ.get(annotation);
154+
}
155+
156+
static void validateValueContainerChild(final String annotation, final ParameterContext ctx) {
157+
final TypeElement valueContainer = ctx.elements().getTypeElement("org.spongepowered.api.data.value.ValueContainer");
158+
if (valueContainer == null) {
159+
return;
160+
}
161+
if (!ctx.types().isAssignable(ctx.param().asType(), valueContainer.asType())) {
162+
ctx.logError(annotation + " is only applicable to parameters whose type is a subtype of ValueContainer");
163+
}
164+
}
165+
166+
static void validateKeyReference(final String annotation, final ParameterContext ctx) {
167+
TypeMirror container = null;
168+
String value = null;
169+
for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : ctx.anno().getElementValues().entrySet()) {
170+
final Name name = entry.getKey().getSimpleName();
171+
if (name.contentEquals("container")) {
172+
container = (TypeMirror) entry.getValue().getValue();
173+
} else if (name.contentEquals("value")) {
174+
value = (String) entry.getValue().getValue();
175+
}
176+
if (container != null && value != null) {
177+
break;
178+
}
179+
}
180+
if (container == null) {
181+
// set to Keys
182+
container = ctx.elements().getTypeElement("org.spongepowered.api.data.Keys").asType();
183+
}
184+
185+
if (container.getKind() != TypeKind.DECLARED) { // otherwise incorrect
186+
return;
187+
}
188+
189+
final TypeElement key = ctx.elements().getTypeElement("org.spongepowered.api.data.Key");
190+
final TypeElement element = (TypeElement) ((DeclaredType) container).asElement();
191+
192+
final Element contained = ProcessorUtils.containingWithNameAndType(element.getEnclosedElements(), value, ElementKind.FIELD);
193+
if (contained == null) {
194+
ctx.logError("Could not find a matching Key in the specified container class");
195+
return; // cannot resolve, not possible to provide more information
196+
}
197+
198+
final Set<Modifier> modifiers = contained.getModifiers();
199+
if (!modifiers.contains(Modifier.STATIC) || !modifiers.contains(Modifier.PUBLIC)) {
200+
ctx.logError("The " + annotation + " annotation must refer to a public static field");
201+
}
202+
203+
if (!ctx.types().isAssignable(contained.asType(), ctx.types().erasure(key.asType()))) {
204+
ctx.logError("The " + annotation + " annotation must refer to a field with type Key, but got '" + contained.asType() + "' instead");
205+
}
206+
}
207+
208+
static {
209+
for (final ListenerParameterAnnotation element : ListenerParameterAnnotation.values()) {
210+
ListenerParameterAnnotation.BY_CLAZZ.put(element.className, element);
211+
}
212+
}
213+
}

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

+100-9
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)