Skip to content

Commit 3bc2e8e

Browse files
authored
[7.0] Introduce compile-time validation, enhance strict runtime checks (#86)
1 parent 79fb99e commit 3bc2e8e

File tree

19 files changed

+836
-14
lines changed

19 files changed

+836
-14
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ repositories {
2121
2222
dependencies {
2323
implementation "net.minecraftforge:eventbus:<version>"
24+
annotationProcessor "net.minecraftforge:eventbus-validator:<version>"
2425
}
2526
```
2627

@@ -32,7 +33,7 @@ import net.minecraftforge.eventbus.api.event.RecordEvent;
3233
import net.minecraftforge.eventbus.api.bus.EventBus;
3334

3435
// Define an event and a bus for it
35-
record PlayerLoggedInEvent(String username) implements RecordEvent {
36+
public record PlayerLoggedInEvent(String username) implements RecordEvent {
3637
public static final EventBus<PlayerLoggedInEvent> BUS = EventBus.create(PlayerLoggedInEvent.class);
3738
}
3839

@@ -54,6 +55,30 @@ Attempting to pass a `null` value to a method param that isn't explicitly marked
5455
operation and won't be considered a breaking change if a future version throws an exception in such cases when it didn't
5556
before.
5657

58+
## Validation
59+
To improve startup performance, EventBus 7 relies heavily on dev-time static analysis and compile-time validation to
60+
ensure correct API usage, reducing the need for runtime checks. This also helps catch potential issues early on and
61+
provides more informative error messages that suggest solutions.
62+
63+
It is highly recommended to use the `eventbus-validator` annotation processor during development to enable enhanced
64+
validation.
65+
66+
### Runtime checks
67+
EventBus performs limited validation at runtime and assumes API usage is mostly correct. Incorrect usage of the API may
68+
lead to unexpected behaviour and breaking changes when updating EventBus due to reliance on internal implementation
69+
details.
70+
71+
For use-cases where you need to debug issues in production or are unable to use the annotation processor, you can enable
72+
strict runtime checks by setting the `eventbus.api.strictRuntimeChecks` system property to `true` when launching your
73+
application. This will enable more exhaustive runtime checks for most API usage, including bulk registration of
74+
listeners and EventBus creation, but not field declarations.
75+
76+
Alternatively, you can selectively enable strict runtime checks for specific subsets of the API by using the
77+
`eventbus.api.strictRegistrationChecks` and `eventbus.api.strictBusCreationChecks` system properties for bulk
78+
registration and EventBus creation, respectively. This may be useful if you only intend on adding listeners to an
79+
existing event made by another library, where you don't need strict checks for the creation of its associated EventBus
80+
due to it being outside your control.
81+
5782
## Contributing
5883
One of the main goals of EventBus is performance. As such, any changes should be benchmarked with the `jmh` Gradle task
5984
to help ensure that there are no unintended performance regressions. If you are unsure how to do this or would like

eventbus-test/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,22 @@ license {
1919
dependencies {
2020
testImplementation rootProject
2121
testImplementation projects.eventbusTestJar
22+
testImplementation projects.eventbusValidator
2223
testImplementation libs.junit.api
2324
testImplementation libs.jspecify.annotations
25+
testImplementation libs.compile.testing
26+
testImplementation libs.jetbrains.annotations
2427
testRuntimeOnly libs.bundles.junit.runtime
2528
}
2629

2730
extraJavaModuleInfo {
2831
failOnMissingModuleInfo = false
32+
automaticModule(libs.compile.testing, "compile.testing")
2933
}
3034

3135
tasks.named('test', Test) {
3236
useJUnitPlatform()
37+
systemProperties = ['eventbus.api.strictBusCreationChecks': true]
3338
}
3439

3540
tasks.register('testAll', AggregateTest) {

eventbus-test/src/test/java/module-info.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
*/
55
open module net.minecraftforge.eventbus.test {
66
requires net.minecraftforge.eventbus;
7+
requires net.minecraftforge.eventbus.validator;
78
requires org.junit.jupiter.api;
89
requires org.jspecify;
10+
requires java.compiler;
11+
requires compile.testing;
12+
requires org.jetbrains.annotations;
13+
requires net.minecraftforge.eventbus.testjars;
914

1015
exports net.minecraftforge.eventbus.test;
16+
exports net.minecraftforge.eventbus.test.compiletime;
1117
}

eventbus-test/src/test/java/net/minecraftforge/eventbus/test/BulkEventListenerTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public static void monitoringListener(TestEvent event) {
288288
monitoringCalled = true;
289289
}
290290

291-
@SubscribeEvent
291+
@SubscribeEvent(priority = Priority.MONITOR)
292292
public static void cancellationAwareMonitoringListener(CancellableTestEvent event, boolean wasCancelled) {
293293
cancellationAwareMonitoringCalled = true;
294294
}
@@ -653,7 +653,7 @@ static void wrongParamCountStatic(TestEvent event, boolean wrong) {}
653653
}
654654

655655
/**
656-
* Tests that strict bulk registeration on a class with a non-monitoring priority on a cancellation-aware monitoring event listener method throws an exception.
656+
* Tests that strict bulk registration on a class with a non-monitoring priority on a cancellation-aware monitoring event listener method throws an exception.
657657
*/
658658
@Test
659659
public void testStrictBulkRegistrationValidationWrongPriorityMonitoring() {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.eventbus.test.compiletime;
6+
7+
import com.google.testing.compile.Compilation;
8+
import com.google.testing.compile.Compiler;
9+
import com.google.testing.compile.JavaFileObjects;
10+
import net.minecraftforge.eventbus.api.bus.EventBus;
11+
import net.minecraftforge.eventbus.testjar.events.EventWithData;
12+
import net.minecraftforge.eventbus.validator.EventBusValidator;
13+
import net.minecraftforge.eventbus.validator.EventTypeValidator;
14+
import net.minecraftforge.eventbus.validator.SubscribeEventValidator;
15+
import org.intellij.lang.annotations.Language;
16+
import org.jspecify.annotations.NullMarked;
17+
18+
import java.io.File;
19+
import java.net.URISyntaxException;
20+
import java.nio.file.Path;
21+
import java.util.List;
22+
23+
final class CompileTestHelper {
24+
private CompileTestHelper() {}
25+
26+
private static final List<File> CLASSPATH;
27+
28+
@Language("Java")
29+
static final String SOURCE_PREFIX = """
30+
import net.minecraftforge.eventbus.api.bus.*;
31+
import net.minecraftforge.eventbus.api.event.*;
32+
import net.minecraftforge.eventbus.api.event.characteristic.*;
33+
import net.minecraftforge.eventbus.api.listener.*;
34+
35+
import net.minecraftforge.eventbus.testjar.events.*;
36+
37+
import org.jspecify.annotations.NullMarked;
38+
import org.jspecify.annotations.Nullable;
39+
40+
@NullMarked
41+
""";
42+
43+
static {
44+
try {
45+
CLASSPATH = List.of(
46+
getClassPath(EventBus.class).toFile(),
47+
getClassPath(EventWithData.class).toFile(),
48+
getClassPath(NullMarked.class).toFile()
49+
);
50+
} catch (URISyntaxException e) {
51+
throw new RuntimeException(e);
52+
}
53+
}
54+
55+
private static Path getClassPath(Class<?> clazz) throws URISyntaxException {
56+
return Path.of(clazz
57+
.getProtectionDomain()
58+
.getCodeSource()
59+
.getLocation()
60+
.toURI()
61+
);
62+
}
63+
64+
static Compilation compile(@Language(value = "Java", prefix = SOURCE_PREFIX) String sourceCode) {
65+
return compileWithoutDefaultPrefix(SOURCE_PREFIX + sourceCode);
66+
}
67+
68+
static Compilation compileWithoutDefaultPrefix(@Language(value = "Java") String sourceCode) {
69+
return Compiler.javac()
70+
.withProcessors(new EventBusValidator(), new EventTypeValidator(), new SubscribeEventValidator())
71+
.withClasspath(CLASSPATH)
72+
.compile(JavaFileObjects.forSourceString("", sourceCode));
73+
}
74+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.eventbus.test.compiletime;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import static com.google.testing.compile.CompilationSubject.assertThat;
10+
import static net.minecraftforge.eventbus.test.compiletime.CompileTestHelper.compile;
11+
12+
public class EventBusValidatorTests {
13+
/**
14+
* Tests that compile-time validation emits a warning for EventBus fields that are not final.
15+
*/
16+
@Test
17+
public void testBusFieldModifiers() {
18+
var compilation = compile("""
19+
record RecordTestEvent() implements RecordEvent {
20+
static EventBus<RecordTestEvent> BUS = EventBus.create(RecordTestEvent.class);
21+
}
22+
""");
23+
assertThat(compilation).hadWarningContaining("should be final");
24+
}
25+
26+
/**
27+
* Tests that compile-time validation emits a warning for EventBus fields that are not of the correct type.
28+
*/
29+
@Test
30+
public void testBusFieldType() {
31+
var compilation = compile("""
32+
record CancellableEvent() implements Cancellable, RecordEvent {
33+
static final EventBus<CancellableEvent> BUS = EventBus.create(CancellableEvent.class);
34+
}
35+
""");
36+
assertThat(compilation).hadWarningContaining("should be CancellableEventBus");
37+
}
38+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) Forge Development LLC
3+
* SPDX-License-Identifier: LGPL-2.1-only
4+
*/
5+
package net.minecraftforge.eventbus.test.compiletime;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import static com.google.testing.compile.CompilationSubject.assertThat;
10+
import static net.minecraftforge.eventbus.test.compiletime.CompileTestHelper.compile;
11+
12+
public class EventTypeValidatorTests {
13+
/**
14+
* Tests that compile-time validation throws an error for non-record classes that implement RecordEvent.
15+
*/
16+
@Test
17+
public void testRecordEventValidation() {
18+
var compilation = compile("final class ClassTestEvent implements RecordEvent {}");
19+
assertThat(compilation).hadErrorContaining("implements RecordEvent but is not a record class");
20+
21+
compilation = compile("record RecordTestEvent() implements RecordEvent {}");
22+
assertThat(compilation).succeededWithoutWarnings();
23+
}
24+
25+
/**
26+
* Tests that compile-time validation throws an error for MonitorAware on classes that do not extend MutableEvent.
27+
*/
28+
@Test
29+
public void testMonitorAwareValidation() {
30+
var compilation = compile("record RecordTestEvent() implements RecordEvent, MonitorAware {}");
31+
assertThat(compilation).hadErrorContaining("implements MonitorAware but is not a mutable event");
32+
33+
compilation = compile("final class ClassTestEvent extends MutableEvent implements MonitorAware {}");
34+
assertThat(compilation).succeededWithoutWarnings();
35+
}
36+
37+
/**
38+
* Tests that compile-time validation throws an error for InheritableEvent on classes that are not inheritable.
39+
*/
40+
@Test
41+
public void testInheritableEventValidation() {
42+
var compilation = compile("final class ClassTestEvent implements InheritableEvent {}");
43+
assertThat(compilation).hadErrorContaining("directly implements InheritableEvent but is not inheritable - extend MutableEvent instead");
44+
45+
compilation = compile("record RecordTestEvent() implements InheritableEvent {}");
46+
assertThat(compilation).hadErrorContaining("directly implements InheritableEvent but is not inheritable - implement RecordEvent instead");
47+
48+
compilation = compile("final class ClassTestEvent extends MutableEvent {}");
49+
assertThat(compilation).succeededWithoutWarnings();
50+
}
51+
}

0 commit comments

Comments
 (0)