diff --git a/build-logic/base/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/build-logic/base/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index b12310c05..f61fd99a1 100644 --- a/build-logic/base/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/build-logic/base/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -89,6 +89,7 @@ tasks.withType(JavaCompile::class.java).configureEach { option("UnusedMethod:ExemptingMethodAnnotations", "dan200.computercraft.api.lua.LuaFunction") check("NullAway", CheckSeverity.ERROR) + check("JSpecifyUnrecognizedAnnotationLocation", CheckSeverity.ERROR) option( "NullAway:AnnotatedPackages", listOf("dan200.computercraft", "cc.tweaked", "net.fabricmc.fabric.api").joinToString(","), @@ -97,6 +98,8 @@ tasks.withType(JavaCompile::class.java).configureEach { option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull") option("NullAway:CheckOptionalEmptiness") option("NullAway:AcknowledgeRestrictiveAnnotations") + option("NullAway:JSpecifyMode") + option("NullAway:JSpecifyExperimental") excludedPaths.set(".*/jmh_generated/.*") } diff --git a/build-logic/base/src/main/kotlin/cc-tweaked.published-mod.gradle.kts b/build-logic/base/src/main/kotlin/cc-tweaked.published-mod.gradle.kts index a11c391e3..9a954dcd3 100644 --- a/build-logic/base/src/main/kotlin/cc-tweaked.published-mod.gradle.kts +++ b/build-logic/base/src/main/kotlin/cc-tweaked.published-mod.gradle.kts @@ -54,6 +54,10 @@ val verifyClassesMatch = tasks.register("verifyClassesMatch") check("MissingOverride", CheckSeverity.OFF) check("LoaderOverride", CheckSeverity.DEFAULT) check("MissingLoaderOverride", CheckSeverity.DEFAULT) + + // TODO: Enable JSpecify in the common classes. Unfortunately there's several bugs blocking us right now. + option("NullAway:JSpecifyMode", false) + option("NullAway:JSpecifyExperimental", false) } source = minecraftEmbeddedProjectResolved.map { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 371e2ef90..415fb0d95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ illuaminate = "0.1.0-83-g1131f68" lwjgl = "3.4.3" minotaur = "2.9.0" modDevGradle = "2.0.147" -nullAway = "0.14.0" +nullAway = "0.14.2" shadow = "9.6.1" spotless = "8.10.4" teavm = "0.16.0" diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/media/PrintoutContents.java b/projects/common-api/src/main/java/dan200/computercraft/api/media/PrintoutContents.java index 8284cdd62..2572de67a 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/media/PrintoutContents.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/media/PrintoutContents.java @@ -15,7 +15,6 @@ import java.util.stream.Stream; * * @since 1.115 */ -@Nullable public interface PrintoutContents { /** * Get the (possibly empty) title for this printout. diff --git a/projects/common/build.gradle.kts b/projects/common/build.gradle.kts index 38f79b0be..c0dc2b6fb 100644 --- a/projects/common/build.gradle.kts +++ b/projects/common/build.gradle.kts @@ -8,6 +8,7 @@ import cc.tweaked.gradle.MergeTrees import cc.tweaked.gradle.annotationProcessorEverywhere import cc.tweaked.vanillaextract.configurations.Capabilities.clientClasses import cc.tweaked.vanillaextract.configurations.Capabilities.commonClasses +import net.ltgt.gradle.errorprone.errorprone plugins { `java-test-fixtures` @@ -70,6 +71,14 @@ dependencies { "cctJavadoc"(libs.cctJavadoc) } +// TODO: Enable JSpecify in the common classes. Unfortunately there's several bugs blocking us right now. +tasks.compileJava { + options.errorprone { + option("NullAway:JSpecifyMode", false) + option("NullAway:JSpecifyExperimental", false) + } +} + illuaminate { version = libs.versions.illuaminate } diff --git a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java index 0c58aef5c..f611dc696 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java +++ b/projects/common/src/client/java/dan200/computercraft/client/ClientRegistry.java @@ -190,7 +190,7 @@ public final class ClientRegistry { return layer == 0 ? ((IColouredItem) stack.getItem()).getColour(stack) : 0xFFFFFF; } - public static void registerShaders(ResourceProvider resources, BiConsumer> load) throws IOException { + public static void registerShaders(ResourceProvider resources, BiConsumer> load) throws IOException { RenderTypes.registerShaders(resources, (name, create, onLoaded) -> { ShaderInstance shader; try { diff --git a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java index 9acff17a8..d348cc41b 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java +++ b/projects/common/src/client/java/dan200/computercraft/client/gui/AbstractComputerScreen.java @@ -183,7 +183,8 @@ public abstract class AbstractComputerScreen ext return; } - var name = file.getFileName().toString(); + var fileName = file.getFileName(); + var name = fileName == null ? "unknown" : fileName.toString(); if (name.length() > UploadFileMessage.MAX_FILE_NAME) { alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.name_too_long")); return; diff --git a/projects/common/src/client/java/dan200/computercraft/client/integration/ShaderMod.java b/projects/common/src/client/java/dan200/computercraft/client/integration/ShaderMod.java index 9c6a236ad..6a1d64847 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/integration/ShaderMod.java +++ b/projects/common/src/client/java/dan200/computercraft/client/integration/ShaderMod.java @@ -7,6 +7,7 @@ package dan200.computercraft.client.integration; import dan200.computercraft.client.render.RenderTypes; import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer; import dan200.computercraft.client.render.vbo.DirectVertexBuffer; +import dan200.computercraft.core.util.Nullability; import java.nio.ByteBuffer; import java.util.Optional; @@ -48,10 +49,10 @@ public class ShaderMod { } private static final class Storage { - static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class) + static final ShaderMod INSTANCE = Nullability.assertNonNull(ServiceLoader.load(Provider.class) .stream() .flatMap(x -> x.get().get().stream()) .findFirst() - .orElseGet(ShaderMod::new); + .orElseGet(ShaderMod::new)); } } diff --git a/projects/common/src/datagen/java/dan200/computercraft/data/PrettyDataProvider.java b/projects/common/src/datagen/java/dan200/computercraft/data/PrettyDataProvider.java index 83913fa4f..c2f3e59ad 100644 --- a/projects/common/src/datagen/java/dan200/computercraft/data/PrettyDataProvider.java +++ b/projects/common/src/datagen/java/dan200/computercraft/data/PrettyDataProvider.java @@ -7,6 +7,7 @@ package dan200.computercraft.data; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.shared.util.PrettyJsonWriter; import net.minecraft.data.CachedOutput; import net.minecraft.data.DataProvider; @@ -38,7 +39,7 @@ public record PrettyDataProvider(T provider) implements @Override public void writeIfNeeded(Path path, byte[] bytes, HashCode hashCode) throws IOException { - var name = path.getFileName().toString(); + var name = Nullability.assertNonNull(path.getFileName()).toString(); if (name.endsWith(".json") || name.endsWith(".mcmeta")) { bytes = PrettyJsonWriter.reformat(bytes); hashCode = HASH_FUNCTION.hashBytes(bytes); diff --git a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java index 87994d1e8..941452f04 100644 --- a/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java +++ b/projects/common/src/main/java/dan200/computercraft/impl/network/wired/NodeSet.java @@ -5,9 +5,9 @@ package dan200.computercraft.impl.network.wired; import dan200.computercraft.api.network.wired.WiredNode; +import dan200.computercraft.core.util.Nullability; import org.jspecify.annotations.Nullable; -import java.util.Objects; /** * A disjoint-set/union-find of {@link WiredNodeImpl}s. @@ -95,6 +95,6 @@ class NodeSet { * @return The associated network. */ WiredNetworkImpl network() { - return Objects.requireNonNull(find().network); + return Nullability.assertNonNull(find().network); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index ce1cae72a..7fd8f80c1 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -17,6 +17,7 @@ import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.api.upgrades.UpgradeBase; import dan200.computercraft.api.upgrades.UpgradeData; import dan200.computercraft.core.util.Colour; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.UpgradeManager; @@ -123,7 +124,6 @@ import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Comparator; -import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -480,7 +480,7 @@ public final class ModRegistry { ComputerCraftAPI.registerAPIFactory(computer -> { var turtle = computer.getComponent(ComputerComponents.TURTLE); - var metrics = Objects.requireNonNull(computer.getComponent(ServerComputer.METRICS)); + var metrics = Nullability.assertNonNull(computer.getComponent(ServerComputer.METRICS)); return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle); }); diff --git a/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java b/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java index 257d86b30..1e1257a73 100644 --- a/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java +++ b/projects/common/src/test/java/dan200/computercraft/shared/details/ItemDetailsTest.java @@ -60,6 +60,6 @@ class ItemDetailsTest { @SuppressWarnings("unchecked") private static Matcher> containsEntryWith(String key, Matcher value) { - return CustomMatchers.containsEntryWith(key, (Matcher) value); + return CustomMatchers.containsEntryWith(key, (Matcher) value); } } diff --git a/projects/common/src/testMod/java/dan200/computercraft/export/ImageRenderer.java b/projects/common/src/testMod/java/dan200/computercraft/export/ImageRenderer.java index ea3d8bc7e..6321ccaf4 100644 --- a/projects/common/src/testMod/java/dan200/computercraft/export/ImageRenderer.java +++ b/projects/common/src/testMod/java/dan200/computercraft/export/ImageRenderer.java @@ -9,6 +9,7 @@ import com.mojang.blaze3d.platform.Lighting; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.VertexSorting; +import dan200.computercraft.core.util.Nullability; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.FogRenderer; import org.joml.Matrix4f; @@ -33,7 +34,7 @@ public class ImageRenderer implements AutoCloseable { } public void captureRender(Path output, Runnable render) throws IOException { - Files.createDirectories(output.getParent()); + Files.createDirectories(Nullability.assertNonNull(output.getParent())); framebuffer.setClearColor(0, 0, 0, 0); framebuffer.clear(Minecraft.ON_OSX); diff --git a/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/NbtUtilsMixin.java b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/NbtUtilsMixin.java index 4e7a3015f..6532396b9 100644 --- a/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/NbtUtilsMixin.java +++ b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/NbtUtilsMixin.java @@ -4,6 +4,7 @@ package dan200.computercraft.mixin.gametest; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.gametest.core.TestHooks; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtUtils; @@ -12,8 +13,6 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.Objects; - /** * Patches {@link NbtUtils#structureToSnbt(CompoundTag)} to remove air blocks from the structure file. This * significantly reduces the size of our generated templates. @@ -27,7 +26,7 @@ abstract class NbtUtilsMixin { @SuppressWarnings("unused") private static void structureToSnbt(CompoundTag tag, CallbackInfoReturnable ci) { // Load in the structure, strip out air, then save it back again. - var structure = Objects.requireNonNull(TestHooks.getStructureManager()).readStructure(tag); + var structure = Nullability.assertNonNull(TestHooks.getStructureManager()).readStructure(tag); var palette = ((StructureTemplateAccessor) structure).getPalettes().get(0); palette.blocks().removeIf(x -> x.state().isAir()); var newTag = structure.save(new CompoundTag()); diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java index 9d5eb1b80..b856813ae 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -74,7 +74,7 @@ public interface IArguments { * @see #get(int) To get a single argument. */ default @Nullable Object[] getAll() throws LuaException { - var result = new Object[count()]; + var result = new @Nullable Object[count()]; for (var i = 0; i < result.length; i++) result[i] = get(i); return result; } diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java index ecc091565..502d0df85 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaTable.java @@ -458,7 +458,7 @@ public interface LuaTable extends Map { } @Override - default V remove(Object o) { + default V remove(@Nullable Object o) { throw new UnsupportedOperationException("Cannot modify LuaTable"); } diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java index 523ba84e6..f4c589b72 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -8,6 +8,7 @@ import org.jspecify.annotations.Nullable; import java.nio.ByteBuffer; import java.util.Map; +import java.util.Objects; /** * Various utility functions for operating with Lua values. @@ -159,7 +160,7 @@ public final class LuaValues { * @throws LuaException If this is not a known enum value. */ public static > T checkEnum(int index, Class klass, String value) throws LuaException { - for (var possibility : klass.getEnumConstants()) { + for (var possibility : Objects.requireNonNull(klass.getEnumConstants())) { if (possibility.name().equalsIgnoreCase(value)) return possibility; } diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java index 1ca8adce1..2a9591f94 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java @@ -57,7 +57,7 @@ public final class ObjectArguments implements IArguments { } @Override - public Object[] getAll() { + public @Nullable Object[] getAll() { return args.toArray(); } } diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java index dc0e45f5c..4a4ceab81 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java @@ -17,6 +17,7 @@ import java.util.Set; public class ObjectLuaTable implements LuaTable { private final Map map; + @SuppressWarnings("NullAway") public ObjectLuaTable(Map map) { this.map = Collections.unmodifiableMap(map); } @@ -32,18 +33,17 @@ public class ObjectLuaTable implements LuaTable { } @Override - public boolean containsKey(Object o) { + public boolean containsKey(@Nullable Object o) { return map.containsKey(o); } @Override - public boolean containsValue(Object o) { + public boolean containsValue(@Nullable Object o) { return map.containsKey(o); } - @Nullable @Override - public Object get(Object o) { + public @Nullable Object get(@Nullable Object o) { return map.get(o); } @@ -63,7 +63,7 @@ public class ObjectLuaTable implements LuaTable { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { return this == o || o instanceof Map otherMap && map.equals(otherMap); } diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 6640be901..b68718648 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -8,6 +8,7 @@ import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.StringUtil; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -135,7 +136,7 @@ public class OSAPI implements ILuaAPI { * @param args The parameters of the event. * @cc.tparam string name The name of the event to queue. * @cc.param ... The parameters of the event. These can be any primitive type (boolean, number, string) as well as - * tables. Other types (like functions), as well as metatables, will not be preserved. + * tables. Other types (like functions), as well as metatables, will not be preserved. * @cc.see os.pullEvent To pull the event queued */ @LuaFunction @@ -408,7 +409,6 @@ public class OSAPI implements ILuaAPI { *
{@code
      * os.date("%A %d %B %Y") -- See the reference above!
      * }
- * * @cc.usage Convert a timestamp to a table. *
{@code
      * os.date("!*t", 1242534247)
@@ -430,7 +430,7 @@ public class OSAPI implements ILuaAPI {
     @LuaFunction
     public final Object date(Optional formatA, Optional timeA) throws LuaException {
         var format = formatA.orElse("%c");
-        long time = timeA.orElseGet(() -> Instant.now().getEpochSecond());
+        long time = Nullability.assertNonNull(timeA.orElseGet(() -> Instant.now().getEpochSecond()));
 
         var instant = Instant.ofEpochSecond(time);
         ZonedDateTime date;
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
index 48ac044da..9f7e34b2b 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java
@@ -187,7 +187,7 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
 
     private final IAPIEnvironment environment;
     private final MethodSupplier peripheralMethods;
-    private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
+    private final @Nullable PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
     private boolean running;
 
     public PeripheralAPI(IAPIEnvironment environment, MethodSupplier peripheralMethods) {
diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java
index 96d266ac1..d4aa978f3 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressRule.java
@@ -71,6 +71,8 @@ public final class AddressRule {
 
         var port = socketAddress.getPort();
         var address = socketAddress.getAddress();
+        if (address == null) throw new IllegalStateException("Socket address must be resolved");
+
         var ipv4Address = address instanceof Inet6Address inet6 && InetAddresses.is6to4Address(inet6)
             ? InetAddresses.get6to4IPv4Address(inet6) : null;
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java
index 8b5b92cef..0e675df7c 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/asm/Generator.java
@@ -11,6 +11,7 @@ import com.google.common.primitives.Primitives;
 import com.google.common.reflect.TypeToken;
 import dan200.computercraft.api.lua.*;
 import dan200.computercraft.core.methods.LuaMethod;
+import dan200.computercraft.core.util.Nullability;
 import org.jspecify.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -179,7 +180,7 @@ final class Generator {
         }
 
         // unsafe can only be used on the computer thread, so reject it for mainThread functions.
-        var annotation = method.getAnnotation(LuaFunction.class);
+        var annotation = Nullability.assertNonNull(method.getAnnotation(LuaFunction.class));
         if (annotation.unsafe() && annotation.mainThread()) {
             LOG.error("Lua Method {}.{} cannot use unsafe and mainThread.", method.getDeclaringClass().getName(), method.getName());
             return false;
@@ -231,7 +232,7 @@ final class Generator {
     private Optional build(Method method, MethodHandle handle, List parameters) {
         LOG.debug("Generating method wrapper for {}.{}.", method.getDeclaringClass().getName(), method.getName());
 
-        var annotation = method.getAnnotation(LuaFunction.class);
+        var annotation = Nullability.assertNonNull(method.getAnnotation(LuaFunction.class));
         var wrappedHandle = buildMethodHandle(method, handle, parameters, annotation.unsafe());
         if (wrappedHandle == null) return Optional.empty();
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java b/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java
index 3509dfc4a..b4201c134 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/asm/IntCache.java
@@ -4,12 +4,14 @@
 
 package dan200.computercraft.core.asm;
 
+import org.jspecify.annotations.Nullable;
+
 import java.util.Arrays;
 import java.util.function.IntFunction;
 
 final class IntCache {
     private final IntFunction factory;
-    private volatile Object[] cache = new Object[16];
+    private volatile @Nullable Object[] cache = new Object[16];
 
     IntCache(IntFunction factory) {
         this.factory = factory;
diff --git a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java
index aa773ddde..e00205cc6 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/asm/MethodSupplierImpl.java
@@ -30,7 +30,7 @@ final class MethodSupplierImpl implements MethodSupplier {
     private final List genericMethods;
     private final Generator generator;
     private final IntCache dynamic;
-    private final Function dynamicMethods;
+    private final Function dynamicMethods;
 
     private final ClassValue>> classCache = new ClassValue<>() {
         private final Function, List>> getter = catching(MethodSupplierImpl.this::getMethodsImpl, List.of());
@@ -45,7 +45,7 @@ final class MethodSupplierImpl implements MethodSupplier {
         List genericMethods,
         Generator generator,
         IntCache dynamic,
-        Function dynamicMethods
+        Function dynamicMethods
     ) {
         this.genericMethods = genericMethods;
         this.generator = generator;
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java b/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
index a87903e41..295c7628c 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/Environment.java
@@ -31,7 +31,7 @@ public final class Environment implements IAPIEnvironment {
     private final ComputerEnvironment environment;
     private final MetricsObserver metrics;
 
-    private final IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT];
+    private final @Nullable IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT];
     private @Nullable IPeripheralChangeListener peripheralListener = null;
 
     private final Int2ObjectMap timers = new Int2ObjectOpenHashMap<>();
diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java b/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
index 370b74e44..ff01bf5a6 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/computer/computerthread/ComputerThread.java
@@ -120,7 +120,7 @@ public final class ComputerThread implements ComputerScheduler {
      * The array of current workers, and their owning threads.
      */
     @GuardedBy("threadLock")
-    private final WorkerThread[] workers;
+    private final @Nullable WorkerThread[] workers;
 
     /**
      * The number of workers in {@link #workers}.
@@ -187,7 +187,7 @@ public final class ComputerThread implements ComputerScheduler {
     }
 
     @SuppressWarnings("GuardedBy")
-    private WorkerThread[] workersReadOnly() {
+    private @Nullable WorkerThread[] workersReadOnly() {
         return workers;
     }
 
@@ -204,7 +204,9 @@ public final class ComputerThread implements ComputerScheduler {
         try {
             LOG.trace("Possibly spawning a worker or monitor.");
 
-            if (monitor == null || !monitor.isAlive()) (monitor = monitorFactory.newThread(new Monitor())).start();
+            if (monitor == null || !monitor.isAlive()) {
+                (monitor = Objects.requireNonNull(monitorFactory.newThread(new Monitor()), "Cannot create monitor thread")).start();
+            }
             if (idleWorkers.get() == 0 || workerCount < workers.length) {
                 for (var i = 0; i < workers.length; i++) {
                     if (workers[i] == null) {
@@ -575,7 +577,7 @@ public final class ComputerThread implements ComputerScheduler {
         /**
          * The computer we're currently running.
          */
-        final AtomicReference currentExecutor = new AtomicReference<>(null);
+        final AtomicReference<@Nullable ExecutorImpl> currentExecutor = new AtomicReference<>(null);
 
         /**
          * The last time we reported a stack trace, used to avoid spamming the logs.
@@ -584,7 +586,7 @@ public final class ComputerThread implements ComputerScheduler {
 
         WorkerThread(int index) {
             this.index = index;
-            owner = workerFactory.newThread(this);
+            owner = Objects.requireNonNull(workerFactory.newThread(this), "Cannot create thread");
         }
 
         @Override
diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java
index 38568d1e2..58a9b7831 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileMount.java
@@ -8,6 +8,7 @@ import com.google.common.base.Joiner;
 import dan200.computercraft.api.filesystem.FileAttributes;
 import dan200.computercraft.api.filesystem.FileOperationException;
 import dan200.computercraft.api.filesystem.Mount;
+import dan200.computercraft.core.util.Nullability;
 
 import java.io.IOException;
 import java.nio.channels.SeekableByteChannel;
@@ -58,7 +59,7 @@ public class FileMount implements Mount {
         if (path.isEmpty() && !created()) return;
 
         try (var stream = Files.newDirectoryStream(resolvePath(path))) {
-            stream.forEach(x -> contents.add(x.getFileName().toString()));
+            stream.forEach(x -> contents.add(Nullability.assertNonNull(x.getFileName()).toString()));
         } catch (IOException e) {
             throw remapException(path, e);
         }
diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
index c0a3adad8..9a1b49759 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java
@@ -381,7 +381,7 @@ public class FileSystem {
 
             if (part.equals("..")) {
                 // .. can cancel out the last folder entered
-                if (outputParts.isEmpty() || outputParts.peekLast().equals("..")) {
+                if (outputParts.isEmpty() || outputParts.getLast().equals("..")) {
                     outputParts.addLast("..");
                 } else {
                     outputParts.removeLast();
diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java
index fc4cd1d93..d80ddf92b 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/JarMount.java
@@ -67,6 +67,8 @@ public final class JarMount extends ArchiveMount implements
         if (file.zipEntry == null) throw new FileOperationException(path, NO_SUCH_FILE);
 
         try (var stream = zip.getInputStream(file.zipEntry)) {
+            if (stream == null) throw new IllegalStateException("Zip entry must exist");
+
             return stream.readAllBytes();
         } catch (IOException e) {
             // Mask other IO exceptions as a non-existent file.
diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java
index b9f0946d2..9a7d9c62b 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/MemoryMount.java
@@ -200,7 +200,7 @@ public final class MemoryMount extends AbstractInMemoryMount queue = new ArrayDeque<>();
-        queue.add(root);
+        queue.add(Nullability.assertNonNull(root));
 
         long size = 0;
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java b/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java
index 8086196b7..8bd789f06 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/filesystem/WritableFileMount.java
@@ -61,13 +61,22 @@ public class WritableFileMount extends FileMount implements WritableMount {
         return capacity - MINIMUM_FILE_SIZE;
     }
 
+    private File getParentFile(File file) {
+        if (file.equals(rootFile)) throw new IllegalArgumentException("Cannot navigate above root file");
+
+        var parent = file.getParentFile();
+        if (parent == null) throw new IllegalStateException("File has no parent");
+
+        return parent;
+    }
+
     @Override
     public boolean isReadOnly(String path) {
         var file = resolveFile(path);
         while (true) {
             if (file.exists()) return !file.canWrite();
             if (file.equals(rootFile)) return false;
-            file = file.getParentFile();
+            file = getParentFile(file);
         }
     }
 
@@ -81,10 +90,10 @@ public class WritableFileMount extends FileMount implements WritableMount {
         }
 
         var dirsToCreate = 1;
-        var parent = file.getParentFile();
+        var parent = getParentFile(file);
         while (!parent.exists()) {
             ++dirsToCreate;
-            parent = parent.getParentFile();
+            parent = getParentFile(parent);
         }
 
         if (getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE) {
@@ -112,8 +121,8 @@ public class WritableFileMount extends FileMount implements WritableMount {
         // Empty directories first
         if (file.isDirectory()) {
             var children = file.list();
-            for (var aChildren : children) {
-                deleteRecursively(new File(file, aChildren));
+            if (children != null) {
+                for (var aChildren : children) deleteRecursively(new File(file, aChildren));
             }
         }
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
index e937039e6..ea904785f 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
@@ -313,7 +313,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 
     static @Nullable Object[] toObjects(Varargs values) {
         var count = values.count();
-        var objects = new Object[count];
+        var objects = new @Nullable Object[count];
         for (var i = 0; i < count; i++) objects[i] = toObject(values.arg(i + 1), null);
         return objects;
     }
diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/TableImpl.java b/projects/core/src/main/java/dan200/computercraft/core/lua/TableImpl.java
index 5497d16c0..e9d42e700 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/lua/TableImpl.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/lua/TableImpl.java
@@ -6,6 +6,7 @@ package dan200.computercraft.core.lua;
 
 import dan200.computercraft.api.lua.LuaException;
 import dan200.computercraft.api.lua.LuaValues;
+import dan200.computercraft.core.util.Nullability;
 import org.jetbrains.annotations.VisibleForTesting;
 import org.jspecify.annotations.Nullable;
 import org.squiddev.cobalt.*;
@@ -59,7 +60,7 @@ class TableImpl implements dan200.computercraft.api.lua.LuaTable
         }
     }
 
-    private LuaValue getImpl(Object o) {
+    private LuaValue getImpl(@Nullable Object o) {
         checkValid();
         var value = convertValue(o);
         return value == null ? Constants.NIL : table.rawget(value);
@@ -82,13 +83,13 @@ class TableImpl implements dan200.computercraft.api.lua.LuaTable
     }
 
     @Override
-    public boolean containsKey(Object o) {
+    public boolean containsKey(@Nullable Object o) {
         return !getImpl(o).isNil();
     }
 
     @Nullable
     @Override
-    public Object get(Object o) {
+    public Object get(@Nullable Object o) {
         return CobaltLuaMachine.toObject(getImpl(o), null);
     }
 
@@ -102,12 +103,12 @@ class TableImpl implements dan200.computercraft.api.lua.LuaTable
         checkValid();
         if (backingMap != null) return backingMap;
         return backingMap = Collections.unmodifiableMap(
-            Objects.requireNonNull((Map) CobaltLuaMachine.toObject(table, null))
+            (Map) Nullability.assertNonNull(CobaltLuaMachine.toObject(table, null))
         );
     }
 
     @Override
-    public boolean containsValue(Object o) {
+    public boolean containsValue(@Nullable Object o) {
         return getBackingMap().containsKey(o);
     }
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java
index 554c45d10..25a5cf1a0 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/lua/VarargArguments.java
@@ -231,11 +231,10 @@ final class VarargArguments implements IArguments {
     }
 
     @SuppressWarnings("ArrayRecordComponent")
-    private record ArraySlice(T[] array, int offset) {
+    private record ArraySlice(@Nullable T[] array, int offset) {
         // FIXME: We should be able to remove the @Nullables if we update NullAway.
 
-        @Nullable
-        T get(int index) {
+        @Nullable T get(int index) {
             return array[offset + index];
         }
 
diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java b/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java
index 0dd438e6a..3383cdc71 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/util/LuaUtil.java
@@ -16,7 +16,7 @@ public class LuaUtil {
     private static final Set EMPTY_SET = Set.of();
     private static final Map EMPTY_MAP = Map.of();
 
-    public static Object[] consArray(Object value, Collection rest) {
+    public static Object[] consArray(Object value, Collection rest) {
         if (rest.isEmpty()) return new Object[]{ value };
 
         // I'm not proud of this code.
diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/ThreadUtils.java b/projects/core/src/main/java/dan200/computercraft/core/util/ThreadUtils.java
index 39261cfd6..2018796a1 100644
--- a/projects/core/src/main/java/dan200/computercraft/core/util/ThreadUtils.java
+++ b/projects/core/src/main/java/dan200/computercraft/core/util/ThreadUtils.java
@@ -70,11 +70,12 @@ public final class ThreadUtils {
      * @see #factory(String)
      */
     public static ThreadFactoryBuilder builder(String name) {
-        var group = new ThreadGroup(baseGroup, baseGroup.getName() + "-" + name);
+        var fullName = baseGroup.getName() + "-" + name;
+        var group = new ThreadGroup(baseGroup, fullName);
         return new ThreadFactoryBuilder()
             .setDaemon(true)
-            .setNameFormat(group.getName().replace("%", "%%") + "-%d")
-            .setUncaughtExceptionHandler((t, e) -> LOG.error("Exception in thread " + t.getName(), e))
+            .setNameFormat(fullName.replace("%", "%%") + "-%d")
+            .setUncaughtExceptionHandler((t, e) -> LOG.error("Exception in thread {}", t.getName(), e))
             .setThreadFactory(x -> new Thread(group, x));
     }
 
diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java
index c10e8c692..5ebdd971c 100644
--- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java
+++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/ContramapMatcher.java
@@ -6,6 +6,7 @@ package dan200.computercraft.test.core;
 
 import org.hamcrest.FeatureMatcher;
 import org.hamcrest.Matcher;
+import org.jspecify.annotations.Nullable;
 
 import java.util.function.Function;
 
@@ -16,7 +17,7 @@ import java.util.function.Function;
  * @param  The type of the object to be matched.
  * @param  The type of the projection/field to be matched.
  */
-public final class ContramapMatcher extends FeatureMatcher {
+public final class ContramapMatcher extends FeatureMatcher {
     private final Function convert;
 
     public ContramapMatcher(String desc, Function convert, Matcher matcher) {
@@ -29,11 +30,11 @@ public final class ContramapMatcher extends FeatureMatcher {
         return convert.apply(actual);
     }
 
-    public static  Matcher contramap(Matcher matcher, String desc, Function convert) {
+    public static  Matcher contramap(Matcher matcher, String desc, Function convert) {
         return new ContramapMatcher<>(desc, convert, matcher);
     }
 
-    public static  Matcher contramap(Matcher matcher, Function convert) {
+    public static  Matcher contramap(Matcher matcher, Function convert) {
         return new ContramapMatcher<>("-f(_)->", convert, matcher);
     }
 }
diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java
index 8841bbf39..17e05c2cb 100644
--- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java
+++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/CustomMatchers.java
@@ -6,6 +6,7 @@ package dan200.computercraft.test.core;
 
 import org.hamcrest.Matcher;
 import org.hamcrest.StringDescription;
+import org.jspecify.annotations.Nullable;
 
 import java.util.List;
 import java.util.Map;
@@ -53,7 +54,7 @@ public class CustomMatchers {
      * @param    The type of values in the map.
      * @return A matcher that projects out of a map.
      */
-    public static  Matcher> containsEntryWith(K key, Matcher value) {
+    public static  Matcher> containsEntryWith(K key, Matcher value) {
         return ContramapMatcher.contramap(value, new StringDescription().appendValue(key).toString(), x -> x.get(key));
     }
 }
diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java
index b5e2e0053..d54564519 100644
--- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java
+++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java
@@ -69,7 +69,7 @@ final class StructuralEqualities {
         }
     }
 
-    static final class FeatureEquality implements StructuralEquality {
+    static final class FeatureEquality implements StructuralEquality {
         private final String desc;
         private final Function get;
         private final StructuralEquality inner;
@@ -100,7 +100,9 @@ final class StructuralEqualities {
         }
     }
 
-    record ListEquality(StructuralEquality equality) implements StructuralEquality> {
+    record ListEquality(
+        StructuralEquality equality
+    ) implements StructuralEquality> {
         @Override
         public boolean equals(List left, List right) {
             if (left.size() != right.size()) return false;
@@ -111,6 +113,7 @@ final class StructuralEqualities {
         }
 
         @Override
+        @SuppressWarnings("NullAway") // FIXME: See https://github.com/uber/NullAway/issues/1727
         public void describe(Description description, List object) {
             description.appendText("[");
             var separator = false;
@@ -123,7 +126,7 @@ final class StructuralEqualities {
         }
     }
 
-    static final class EqualityMatcher extends TypeSafeMatcher {
+    static final class EqualityMatcher extends TypeSafeMatcher<@Nullable T> {
         private final StructuralEquality equality;
         private final T equalTo;
 
diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java
index 5045ebf43..61cd419cc 100644
--- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java
+++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java
@@ -6,6 +6,7 @@ package dan200.computercraft.test.core;
 
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
+import org.jspecify.annotations.Nullable;
 
 import java.lang.reflect.Field;
 import java.util.List;
@@ -18,7 +19,7 @@ import java.util.function.Function;
  *
  * @param  The type of the value to check.
  */
-public interface StructuralEquality {
+public interface StructuralEquality {
     /**
      * Check if two non-null values are equal.
      *
@@ -52,7 +53,7 @@ public interface StructuralEquality {
      * @param expected The expected value.
      * @return A matcher which checks if its input is equal to the given value.
      */
-    default Matcher asMatcher(Class klass, T expected) {
+    default Matcher<@Nullable T> asMatcher(Class klass, T expected) {
         return new StructuralEqualities.EqualityMatcher<>(klass, this, expected);
     }
 
@@ -117,8 +118,7 @@ public interface StructuralEquality {
      * @param        The type of the field's.
      * @return The newly created {@link StructuralEquality} object.
      */
-    @SuppressWarnings("unchecked")
-    static  StructuralEquality field(Class klass, String fieldName, StructuralEquality inner) {
+    static  StructuralEquality field(Class klass, String fieldName, StructuralEquality inner) {
         Field field;
         try {
             field = klass.getDeclaredField(fieldName);
@@ -127,13 +127,18 @@ public interface StructuralEquality {
             throw new IllegalArgumentException("Cannot find field", e);
         }
 
-        return new StructuralEqualities.FeatureEquality<>(fieldName, x -> {
+        return new StructuralEqualities.FeatureEquality<>(fieldName, makeFieldReader(field), inner);
+    }
+
+    @SuppressWarnings({ "unchecked", "NullAway" })
+    private static  Function makeFieldReader(Field field) {
+        return x -> {
             try {
                 return (U) field.get(x);
             } catch (ReflectiveOperationException e) {
                 throw new IllegalStateException("Cannot read field", e);
             }
-        }, inner);
+        };
     }
 
     /**
diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java
index 5c2695112..bd9393dd5 100644
--- a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java
+++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/computer/BasicEnvironment.java
@@ -13,6 +13,7 @@ import dan200.computercraft.core.filesystem.FileMount;
 import dan200.computercraft.core.filesystem.JarMount;
 import dan200.computercraft.core.filesystem.MemoryMount;
 import dan200.computercraft.core.metrics.MetricsObserver;
+import dan200.computercraft.core.util.Nullability;
 import org.jspecify.annotations.Nullable;
 
 import java.io.File;
@@ -75,7 +76,7 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment
 
     @Override
     public @Nullable InputStream getLuaBios() {
-        return ComputerContext.class.getClassLoader().getResourceAsStream("data/computercraft/lua/bios.lua");
+        return Nullability.assertNonNull(ComputerContext.class.getClassLoader()).getResourceAsStream("data/computercraft/lua/bios.lua");
     }
 
     public static Mount createMount(Class klass, String path, String fallback) {
@@ -104,7 +105,13 @@ public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment
     }
 
     private static File getContainingFile(Class klass) {
-        var path = klass.getProtectionDomain().getCodeSource().getLocation().getPath();
+        var source = klass.getProtectionDomain().getCodeSource();
+        if (source == null) throw new IllegalStateException("Cannot find owning Jar");
+
+        var location = source.getLocation();
+        if (location == null) throw new IllegalStateException("Cannot find owning Jar");
+
+        var path = location.getPath();
         var bangIndex = path.indexOf("!");
 
         // Plain old file, so step up from dan200.computercraft.
diff --git a/projects/fabric-api/src/main/java/dan200/computercraft/api/media/MediaLookup.java b/projects/fabric-api/src/main/java/dan200/computercraft/api/media/MediaLookup.java
index 0692744d9..88a9bbafa 100644
--- a/projects/fabric-api/src/main/java/dan200/computercraft/api/media/MediaLookup.java
+++ b/projects/fabric-api/src/main/java/dan200/computercraft/api/media/MediaLookup.java
@@ -21,11 +21,13 @@ import org.jspecify.annotations.Nullable;
 public final class MediaLookup {
     public static final ResourceLocation ID = new ResourceLocation(ComputerCraftAPI.MOD_ID, "media");
 
+    @SuppressWarnings("NullAway") // FIXME(26.3): Can be removed when FAPI fixes their annotation
     private static final ItemApiLookup lookup = ItemApiLookup.get(ID, IMedia.class, Void.class);
 
     private MediaLookup() {
     }
 
+    @SuppressWarnings("NullAway") // FIXME(26.3): Can be removed when FAPI fixes their annotation
     public static ItemApiLookup get() {
         return lookup;
     }
diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/integration/rei/UpgradeDisplayGenerator.java b/projects/fabric/src/client/java/dan200/computercraft/client/integration/rei/UpgradeDisplayGenerator.java
index 09d278bf4..d0df39a01 100644
--- a/projects/fabric/src/client/java/dan200/computercraft/client/integration/rei/UpgradeDisplayGenerator.java
+++ b/projects/fabric/src/client/java/dan200/computercraft/client/integration/rei/UpgradeDisplayGenerator.java
@@ -4,6 +4,7 @@
 
 package dan200.computercraft.client.integration.rei;
 
+import dan200.computercraft.core.util.Nullability;
 import dan200.computercraft.shared.integration.UpgradeRecipeGenerator;
 import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator;
 import me.shedaniel.rei.api.common.entry.EntryStack;
@@ -17,7 +18,9 @@ import java.util.Optional;
  * Provides custom recipe and usage hints for pocket/turtle upgrades.
  */
 class UpgradeDisplayGenerator implements DynamicDisplayGenerator> {
-    private final UpgradeRecipeGenerator> resolver = new UpgradeRecipeGenerator<>(DefaultCraftingDisplay::of);
+    private final UpgradeRecipeGenerator> resolver = new UpgradeRecipeGenerator<>(
+        r -> Nullability.assertNonNull(DefaultCraftingDisplay.of(r))
+    );
 
     @Override
     public Optional>> getRecipeFor(EntryStack entry) {
diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/model/FoiledModel.java b/projects/fabric/src/client/java/dan200/computercraft/client/model/FoiledModel.java
index 34313ebb5..85bac2efb 100644
--- a/projects/fabric/src/client/java/dan200/computercraft/client/model/FoiledModel.java
+++ b/projects/fabric/src/client/java/dan200/computercraft/client/model/FoiledModel.java
@@ -53,7 +53,7 @@ public final class FoiledModel extends ForwardingBakedModel implements RenderCon
     }
 
     @Override
-    public boolean equals(Object obj) {
+    public boolean equals(@Nullable Object obj) {
         return this == obj || (obj instanceof FoiledModel other && wrapped.equals(other.wrapped));
     }
 
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
index f4d163561..a99585eb6 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java
@@ -141,6 +141,7 @@ public class ComputerCraft {
         }
     }
 
+    @SuppressWarnings("NullAway") // FIXME(26.3): Can be removed when FAPI fixes their annotation
     private record ItemComponentImpl(
         ItemApiLookup lookup
     ) implements ModRegistry.ItemComponent {
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
index ebe9ffea8..185550279 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/peripheral/generic/methods/InventoryMethods.java
@@ -41,7 +41,7 @@ public final class InventoryMethods extends AbstractInventoryMethods extractContainerImpl(Level level, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, @Nullable Direction direction) {
         var internal = ItemStorage.SIDED.find(level, pos, state, blockEntity, null);
         if (internal != null) return internal;
diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java
index 2765b17c9..da143d25e 100644
--- a/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java
+++ b/projects/fabric/src/main/java/dan200/computercraft/shared/platform/FabricConfigFile.java
@@ -22,6 +22,7 @@ import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -52,7 +53,9 @@ public final class FabricConfigFile extends ConfigFile {
             .build();
 
         try {
-            Files.createDirectories(path.getParent());
+            var parent = path.getParent();
+            if (parent != null) Files.createDirectories(parent);
+
             FileWatcher.defaultInstance().addWatch(config.getNioPath(), this::loadConfig);
         } catch (IOException e) {
             LOG.error("Failed to load config from {}.", path, e);
@@ -153,7 +156,8 @@ public final class FabricConfigFile extends ConfigFile {
             var path = getPath(name);
             spec.define(path, defaultValue, o -> o != null && o != NullObject.NULL_OBJECT && EnumGetMethod.NAME_IGNORECASE.validate(o, defaultValue.getDeclaringClass()));
 
-            var suffix = "Allowed Values: " + Arrays.stream(defaultValue.getDeclaringClass().getEnumConstants()).map(Enum::name).collect(Collectors.joining(", "));
+            var suffix = "Allowed Values: " + Arrays.stream(Objects.requireNonNull(defaultValue.getDeclaringClass().getEnumConstants()))
+                .map(Enum::name).collect(Collectors.joining(", "));
             return defineValue(name, takeComment(), suffix, defaultValue, (c, p, d) -> c.getEnumOrElse(p, d, EnumGetMethod.NAME_IGNORECASE));
         }
 
diff --git a/projects/forge-api/src/main/java/dan200/computercraft/api/ForgeComputerCraftAPI.java b/projects/forge-api/src/main/java/dan200/computercraft/api/ForgeComputerCraftAPI.java
index 2a4b1dc0e..80d2fa58e 100644
--- a/projects/forge-api/src/main/java/dan200/computercraft/api/ForgeComputerCraftAPI.java
+++ b/projects/forge-api/src/main/java/dan200/computercraft/api/ForgeComputerCraftAPI.java
@@ -39,7 +39,7 @@ public final class ForgeComputerCraftAPI {
      * @param capability The capability to register.
      * @see GenericSource
      */
-    public static void registerGenericCapability(Capability capability) {
+    public static void registerGenericCapability(Capability capability) {
         getInstance().registerGenericCapability(capability);
     }
 
diff --git a/projects/forge-api/src/main/java/dan200/computercraft/impl/ComputerCraftAPIForgeService.java b/projects/forge-api/src/main/java/dan200/computercraft/impl/ComputerCraftAPIForgeService.java
index a9dd5e774..af7ba7a58 100644
--- a/projects/forge-api/src/main/java/dan200/computercraft/impl/ComputerCraftAPIForgeService.java
+++ b/projects/forge-api/src/main/java/dan200/computercraft/impl/ComputerCraftAPIForgeService.java
@@ -31,7 +31,7 @@ public interface ComputerCraftAPIForgeService extends ComputerCraftAPIService {
 
     void registerPeripheralProvider(IPeripheralProvider provider);
 
-    void registerGenericCapability(Capability capability);
+    void registerGenericCapability(Capability capability);
 
     LazyOptional getWiredElementAt(BlockGetter world, BlockPos pos, Direction side);
 
diff --git a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
index cd6977a35..dd6f0c468 100644
--- a/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
+++ b/projects/forge/src/client/java/dan200/computercraft/client/ForgeClientRegistry.java
@@ -62,7 +62,7 @@ public final class ForgeClientRegistry {
 
     @SubscribeEvent
     public static void registerShaders(RegisterShadersEvent event) throws IOException {
-        ClientRegistry.registerShaders(event.getResourceProvider(), event::registerShader);
+        ClientRegistry.registerShaders(event.getResourceProvider(), (s, c) -> event.registerShader(s, c));
     }
 
     @SubscribeEvent
diff --git a/projects/forge/src/client/java/dan200/computercraft/client/model/FoiledModel.java b/projects/forge/src/client/java/dan200/computercraft/client/model/FoiledModel.java
index 7f71173ed..b22cc60ca 100644
--- a/projects/forge/src/client/java/dan200/computercraft/client/model/FoiledModel.java
+++ b/projects/forge/src/client/java/dan200/computercraft/client/model/FoiledModel.java
@@ -39,7 +39,7 @@ public final class FoiledModel extends BakedModelWrapper {
     }
 
     @Override
-    public boolean equals(Object obj) {
+    public boolean equals(@Nullable Object obj) {
         return this == obj || (obj instanceof FoiledModel other && originalModel.equals(other.originalModel));
     }
 
diff --git a/projects/forge/src/client/java/dan200/computercraft/client/platform/ClientPlatformHelperImpl.java b/projects/forge/src/client/java/dan200/computercraft/client/platform/ClientPlatformHelperImpl.java
index 86fb1fe4c..81782bcc7 100644
--- a/projects/forge/src/client/java/dan200/computercraft/client/platform/ClientPlatformHelperImpl.java
+++ b/projects/forge/src/client/java/dan200/computercraft/client/platform/ClientPlatformHelperImpl.java
@@ -31,7 +31,7 @@ import java.util.Arrays;
 @AutoService(dan200.computercraft.impl.client.ClientPlatformHelper.class)
 public class ClientPlatformHelperImpl implements ClientPlatformHelper {
     private static final RandomSource random = RandomSource.create(0);
-    private static final Direction[] directions = Arrays.copyOf(Direction.values(), 7);
+    private static final @Nullable Direction[] directions = Arrays.copyOf(Direction.values(), 7);
 
     @Override
     public BakedModel getModel(ModelManager manager, ResourceLocation location) {
diff --git a/projects/forge/src/main/java/dan200/computercraft/ComputerCraft.java b/projects/forge/src/main/java/dan200/computercraft/ComputerCraft.java
index 88a04e80e..0db03c85c 100644
--- a/projects/forge/src/main/java/dan200/computercraft/ComputerCraft.java
+++ b/projects/forge/src/main/java/dan200/computercraft/ComputerCraft.java
@@ -43,10 +43,14 @@ import net.minecraftforge.fml.event.config.ModConfigEvent;
 import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
 import net.minecraftforge.registries.NewRegistryEvent;
 import net.minecraftforge.registries.RegistryBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Mod(ComputerCraftAPI.MOD_ID)
 @Mod.EventBusSubscriber(modid = ComputerCraftAPI.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
 public final class ComputerCraft {
+    private static final Logger LOG = LoggerFactory.getLogger(ComputerCraft.class);
+
     public ComputerCraft() {
         ModRegistry.register();
 
@@ -135,8 +139,11 @@ public final class ComputerCraft {
                 Pack.Position.BOTTOM,
                 PackSource.FEATURE
             );
-
-            event.addRepositorySource(add -> add.accept(pack));
+            if (pack == null) {
+                LOG.error("Cannot find " + TurtleStorage.DATA_PACK_NAME + " datapack");
+            } else {
+                event.addRepositorySource(add -> add.accept(pack));
+            }
         }
     }
 }
diff --git a/projects/forge/src/main/java/dan200/computercraft/impl/ComputerCraftAPIImpl.java b/projects/forge/src/main/java/dan200/computercraft/impl/ComputerCraftAPIImpl.java
index e2905f089..4f591265c 100644
--- a/projects/forge/src/main/java/dan200/computercraft/impl/ComputerCraftAPIImpl.java
+++ b/projects/forge/src/main/java/dan200/computercraft/impl/ComputerCraftAPIImpl.java
@@ -43,7 +43,7 @@ public final class ComputerCraftAPIImpl extends AbstractComputerCraftAPI impleme
     }
 
     @Override
-    public void registerGenericCapability(Capability capability) {
+    public void registerGenericCapability(Capability capability) {
         Peripherals.registerGenericCapability(capability);
     }
 
diff --git a/projects/forge/src/main/java/dan200/computercraft/impl/Peripherals.java b/projects/forge/src/main/java/dan200/computercraft/impl/Peripherals.java
index 25a904450..5f2a88c43 100644
--- a/projects/forge/src/main/java/dan200/computercraft/impl/Peripherals.java
+++ b/projects/forge/src/main/java/dan200/computercraft/impl/Peripherals.java
@@ -59,14 +59,13 @@ public final class Peripherals {
      * @param         The type of the capability we look up.
      */
     private record CapabilityLookup(Capability capability) implements ComponentLookup {
-        @Nullable
         @Override
-        public T find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, InvalidateCallback invalidate) {
+        public @Nullable T find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, InvalidateCallback invalidate) {
             return CapabilityUtil.unwrap(CapabilityUtil.getCapability(blockEntity, this.capability(), side), invalidate);
         }
     }
 
-    public static void registerGenericCapability(Capability capability) {
+    public static void registerGenericCapability(Capability capability) {
         Objects.requireNonNull(capability, "Capability cannot be null");
         registerGenericLookup(new CapabilityLookup<>(capability));
     }
diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/util/CapabilityProvider.java b/projects/forge/src/main/java/dan200/computercraft/shared/util/CapabilityProvider.java
index b28d61adf..247f86c9d 100644
--- a/projects/forge/src/main/java/dan200/computercraft/shared/util/CapabilityProvider.java
+++ b/projects/forge/src/main/java/dan200/computercraft/shared/util/CapabilityProvider.java
@@ -29,17 +29,17 @@ import java.util.function.Supplier;
  */
 public final class CapabilityProvider implements ICapabilityProvider {
     private final Capability cap;
-    private final Supplier supplier;
+    private final Supplier<@Nullable T> supplier;
     private final BooleanSupplier isRemoved;
     private @Nullable LazyOptional instance;
 
-    private CapabilityProvider(Capability cap, Supplier supplier, BooleanSupplier isRemoved) {
+    private CapabilityProvider(Capability cap, Supplier<@Nullable T> supplier, BooleanSupplier isRemoved) {
         this.cap = Objects.requireNonNull(cap, "Capability cannot be null");
         this.supplier = supplier;
         this.isRemoved = isRemoved;
     }
 
-    public static  CapabilityProvider attach(AttachCapabilitiesEvent event, ResourceLocation id, Capability cap, Supplier instance) {
+    public static  CapabilityProvider attach(AttachCapabilitiesEvent event, ResourceLocation id, Capability cap, Supplier<@Nullable T> instance) {
         BooleanSupplier isRemoved
             = event.getObject() instanceof BlockEntity be ? be::isRemoved
             : event.getObject() instanceof Entity entity ? entity::isRemoved
diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/GLObjects.java b/projects/standalone/src/main/java/cc/tweaked/standalone/GLObjects.java
index 48d01ebdc..c6d3cc478 100644
--- a/projects/standalone/src/main/java/cc/tweaked/standalone/GLObjects.java
+++ b/projects/standalone/src/main/java/cc/tweaked/standalone/GLObjects.java
@@ -4,12 +4,14 @@
 
 package cc.tweaked.standalone;
 
+import dan200.computercraft.core.util.Nullability;
 import org.lwjgl.BufferUtils;
 import org.lwjgl.opengl.GL45C;
 
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayDeque;
 import java.util.Deque;
@@ -79,6 +81,12 @@ public class GLObjects implements AutoCloseable {
         return texture;
     }
 
+    private InputStream getResource(String path) throws IOException {
+        var stream = Nullability.assertNonNull(getClass().getClassLoader()).getResourceAsStream(path);
+        if (stream == null) throw new IOException("Cannot find " + path);
+        return stream;
+    }
+
     /**
      * Create a texture, loading it from an image.
      *
@@ -88,8 +96,7 @@ public class GLObjects implements AutoCloseable {
      */
     public int loadTexture(String path) throws IOException {
         BufferedImage image;
-        try (var stream = getClass().getClassLoader().getResourceAsStream(path)) {
-            if (stream == null) throw new IOException("Cannot find " + path);
+        try (var stream = getResource(path)) {
             image = ImageIO.read(stream);
         }
 
@@ -122,8 +129,7 @@ public class GLObjects implements AutoCloseable {
      */
     public int compileShader(int type, String path) throws IOException {
         String contents;
-        try (var stream = getClass().getClassLoader().getResourceAsStream(path)) {
-            if (stream == null) throw new IOException("Could not load shader " + path);
+        try (var stream = getResource(path)) {
             contents = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
         }
 
diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java
index 4ca32a5db..94b662203 100644
--- a/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java
+++ b/projects/standalone/src/main/java/cc/tweaked/standalone/InputState.java
@@ -127,7 +127,8 @@ public class InputState {
                 continue;
             }
 
-            files.add(new TransferredFile(path.getFileName().toString(), new ArrayByteChannel(contents)));
+            var name = path.getFileName();
+            files.add(new TransferredFile(name == null ? "unknown" : name.toString(), new ArrayByteChannel(contents)));
         }
 
         if (!files.isEmpty()) computer.queueEvent(TransferredFiles.EVENT, new Object[]{ new TransferredFiles(files) });
diff --git a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java
index ea219e6a4..b652a3bf4 100644
--- a/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java
+++ b/projects/standalone/src/main/java/cc/tweaked/standalone/Main.java
@@ -74,7 +74,9 @@ public class Main {
             var matcher = TermSize.PATTERN.matcher(value);
             if (!matcher.matches()) throw new ParseException("'" + value + "' is not a valid terminal size.");
 
-            return new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
+            var width = Integer.parseInt(Objects.requireNonNull(matcher.group(1)));
+            var height = Integer.parseInt(Objects.requireNonNull(matcher.group(2)));
+            return new TermSize(width, height);
         }
     }
 
@@ -85,7 +87,9 @@ public class Main {
             var matcher = MountPaths.PATTERN.matcher(value);
             if (!matcher.matches()) throw new ParseException("'" + value + "' is not a mount spec.");
 
-            return new MountPaths(parsePath(matcher.group(1)), matcher.group(2));
+            var src = parsePath(Objects.requireNonNull(matcher.group(1)));
+            var dest = Objects.requireNonNull(matcher.group(2));
+            return new MountPaths(src, dest);
         }
     }
 
diff --git a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
index 224527cf8..c43c1661a 100644
--- a/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
+++ b/projects/web/src/builder/java/cc/tweaked/web/builder/Builder.java
@@ -23,6 +23,7 @@ import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.BiConsumer;
 import java.util.stream.Stream;
 
@@ -37,7 +38,7 @@ public class Builder {
             var input = getPath(scope, "cct.input");
             var classpath = getPath(scope, "cct.classpath");
             var output = getFile("cct.output");
-            var version = System.getProperty("cct.version");
+            var version = Objects.requireNonNull(System.getProperty("cct.version"), "cct.version must be present");
             var minify = Boolean.parseBoolean(System.getProperty("cct.minify", "true"));
 
             buildClasses(input, classpath, output, minify);
@@ -73,7 +74,7 @@ public class Builder {
         run(output, remapper, TeaVMTargetType.JAVASCRIPT, TeaVMOptimizationLevel.ADVANCED, minify);
         run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.SIMPLE, minify);
 
-        try (var runtime = Builder.class.getClassLoader().getResourceAsStream("org/teavm/backend/wasm/wasm-gc-module-runtime.js")) {
+        try (var runtime = Objects.requireNonNull(Builder.class.getClassLoader()).getResourceAsStream("org/teavm/backend/wasm/wasm-gc-module-runtime.js")) {
             if (runtime == null) throw new IllegalStateException("Cannot find WASM runtime");
             Files.copy(runtime, output.resolve("wasm-gc-runtime.js"), StandardCopyOption.REPLACE_EXISTING);
         }
@@ -143,7 +144,9 @@ public class Builder {
     }
 
     private static List getPath(CloseScope scope, String name) {
-        return Arrays.stream(System.getProperty(name).split(File.pathSeparator))
+        var path = System.getProperty(name);
+        if (path == null) throw new NullPointerException(name + " must be given");
+        return Arrays.stream(path.split(File.pathSeparator))
             .map(Path::of)
             .filter(Files::exists)
             .map(file -> {
@@ -157,6 +160,8 @@ public class Builder {
     }
 
     private static Path getFile(String name) {
-        return Path.of(System.getProperty(name));
+        var path = System.getProperty(name);
+        if (path == null) throw new NullPointerException(name + " must be given");
+        return Path.of(path);
     }
 }
diff --git a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
index bfa32d85e..f229f633b 100644
--- a/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
+++ b/projects/web/src/main/java/cc/tweaked/web/js/JavascriptConv.java
@@ -14,7 +14,10 @@ import org.teavm.jso.core.JSString;
 /**
  * Utility methods for converting between Java and Javascript representations.
  */
-public class JavascriptConv {
+public final class JavascriptConv {
+    private JavascriptConv() {
+    }
+
     /**
      * Convert an array of Javascript values to an equivalent array of Java values.
      *
@@ -24,7 +27,7 @@ public class JavascriptConv {
     @Contract("null -> null; !null -> !null")
     public static @Nullable Object @Nullable [] toJava(@Nullable JSObject @Nullable [] value) {
         if (value == null) return null;
-        var out = new Object[value.length];
+        var out = new @Nullable Object[value.length];
         for (var i = 0; i < value.length; i++) out[i] = toJava(value[i]);
         return out;
     }
diff --git a/projects/web/src/main/java/cc/tweaked/web/methods/StaticGenerator.java b/projects/web/src/main/java/cc/tweaked/web/methods/StaticGenerator.java
index a83b32e59..d31bcdfec 100644
--- a/projects/web/src/main/java/cc/tweaked/web/methods/StaticGenerator.java
+++ b/projects/web/src/main/java/cc/tweaked/web/methods/StaticGenerator.java
@@ -12,6 +12,7 @@ import com.google.common.reflect.TypeToken;
 import dan200.computercraft.api.lua.*;
 import dan200.computercraft.core.asm.Reflect;
 import dan200.computercraft.core.methods.LuaMethod;
+import dan200.computercraft.core.util.Nullability;
 import org.jspecify.annotations.Nullable;
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.MethodVisitor;
@@ -106,7 +107,7 @@ public final class StaticGenerator {
             }
         }
 
-        var annotation = method.getAnnotation(LuaFunction.class);
+        var annotation = Nullability.assertNonNull(method.getAnnotation(LuaFunction.class));
         if (annotation.unsafe() && annotation.mainThread()) {
             System.err.printf("Lua Method %s cannot use unsafe and mainThread.\n", name);
             return Optional.empty();