Skip to content

Commit 04ca4bd

Browse files
authored
Add a compile-time warning when casting from EventBus#create to CancellableEventBus (#106)
1 parent 8127962 commit 04ca4bd

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

eventbus-test/src/test/java/net/minecraftforge/eventbus/test/compiletime/EventBusValidatorTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,26 @@ record CancellableEvent() implements Cancellable, RecordEvent {
3535
""");
3636
assertThat(compilation).hadWarningContaining("should be CancellableEventBus");
3737
}
38+
39+
/**
40+
* Tests that the compile-time validation emits a warning when calling EventBus#create(Class) and casting the result
41+
* to CancellableEventBus, as this relies on internal implementation details that aren't guaranteed to hold true in
42+
* future patch updates. The correct approach is to call CancellableEventBus#create(Class) directly.
43+
*/
44+
@Test
45+
public void testBusFieldNotCasted() {
46+
var compilation = compile("""
47+
record CancellableEvent() implements Cancellable, RecordEvent {
48+
static final CancellableEventBus<CancellableEvent> BUS = (CancellableEventBus<CancellableEvent>) EventBus.create(CancellableEvent.class);
49+
}
50+
""");
51+
assertThat(compilation).hadWarningContaining("should call CancellableEventBus#create(Class) directly");
52+
53+
compilation = compile("""
54+
record CancellableEvent() implements Cancellable, RecordEvent {
55+
static final CancellableEventBus<CancellableEvent> BUS = CancellableEventBus.create(CancellableEvent.class);
56+
}
57+
""");
58+
assertThat(compilation).succeeded();
59+
}
3860
}

eventbus-validator/src/main/java/net/minecraftforge/eventbus/validator/EventBusValidator.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
*/
55
package net.minecraftforge.eventbus.validator;
66

7+
import com.sun.source.tree.IdentifierTree;
8+
import com.sun.source.tree.MemberSelectTree;
9+
import com.sun.source.tree.MethodInvocationTree;
10+
import com.sun.source.tree.TypeCastTree;
11+
import com.sun.source.tree.VariableTree;
12+
import com.sun.source.util.TreePath;
13+
import com.sun.source.util.Trees;
14+
715
import javax.annotation.processing.ProcessingEnvironment;
816
import javax.annotation.processing.RoundEnvironment;
917
import javax.lang.model.element.Element;
@@ -18,11 +26,13 @@
1826

1927
public final class EventBusValidator extends AbstractValidator {
2028
private Types types;
29+
private Trees trees;
2130

2231
@Override
2332
public void init(ProcessingEnvironment processingEnv) {
2433
super.init(processingEnv);
2534
types = processingEnv.getTypeUtils();
35+
trees = Trees.instance(processingEnv);
2636
}
2737

2838
@Override
@@ -38,7 +48,8 @@ private void process(Element element) {
3848
if (element.getKind() == ElementKind.FIELD) {
3949
TypeMirror erasedFieldType = types.erasure(element.asType());
4050
var isStandardEventBus = types.isSameType(erasedFieldType, BusTypes.eventBus);
41-
if (!(isStandardEventBus || types.isSameType(erasedFieldType, BusTypes.cancellableEventBus)))
51+
var isCancellableEventBus = false;
52+
if (!(isStandardEventBus || (isCancellableEventBus = types.isSameType(erasedFieldType, BusTypes.cancellableEventBus))))
4253
return; // only interested in (Cancellable)EventBus fields
4354

4455
// Show a warning if the bus field is not final
@@ -62,11 +73,44 @@ private void process(Element element) {
6273
);
6374
}
6475
}
76+
77+
// Show a warning if the field initialiser casts the result of EventBus.create(...) to CancellableEventBus
78+
if (isCancellableEventBus) {
79+
checkForCastedEventBusCreate(element);
80+
}
6581
}
6682

6783

6884
for (var child : element.getEnclosedElements()) {
6985
process(child);
7086
}
7187
}
88+
89+
private void checkForCastedEventBusCreate(Element fieldElement) {
90+
TreePath path = trees.getPath(fieldElement);
91+
if (path == null || !(path.getLeaf() instanceof VariableTree variableTree)) return;
92+
93+
// Check if initialiser is a cast expression and the expression being cast is a method invocation
94+
if (!(variableTree.getInitializer() instanceof TypeCastTree cast
95+
&& cast.getExpression() instanceof MethodInvocationTree methodInvocation)) return;
96+
97+
// Check if it's a call to EventBus.create(...)
98+
if (!(methodInvocation.getMethodSelect() instanceof MemberSelectTree memberSelectTree
99+
&& memberSelectTree.getExpression() instanceof IdentifierTree identifierTree
100+
&& identifierTree.getName().contentEquals("EventBus")
101+
&& memberSelectTree.getIdentifier().contentEquals("create"))) return;
102+
103+
// And that its result is being cast to CancellableEventBus...
104+
// Note: Can't use types#getTypeMirror here because it returns null for TypeCastTree when wrapped in TreePath in
105+
// this context, so we have to manually resolve the type element from the tree instead.
106+
if (trees.getElement(new TreePath(new TreePath(path, cast), cast.getType())) instanceof TypeElement typeElement) {
107+
if (types.isSameType(types.erasure(typeElement.asType()), BusTypes.cancellableEventBus)) {
108+
processingEnv.getMessager().printWarning(
109+
"CancellableEventBus field " + fieldElement.getEnclosingElement().getSimpleName() + '.' + fieldElement.getSimpleName() +
110+
" should call CancellableEventBus#create(Class) directly instead of casting the result from EventBus#create(Class)",
111+
fieldElement
112+
);
113+
}
114+
}
115+
}
72116
}

0 commit comments

Comments
 (0)