From cbfd0c63ea9db675cde2bb7ecac10f6d51f6e328 Mon Sep 17 00:00:00 2001 From: Alex Good Date: Sat, 30 Aug 2025 23:13:12 +0100 Subject: [PATCH] Add linter step to ensure LoadLibrary.initialize is called --- lib/build.gradle.kts | 80 +++++++++++++++++++ .../main/java/org/automerge/CommitResult.java | 4 + lib/src/main/java/org/automerge/Cursor.java | 4 + lib/src/main/java/org/automerge/Document.java | 7 +- lib/src/main/java/org/automerge/ObjectId.java | 4 + lib/src/main/java/org/automerge/PatchLog.java | 4 + .../main/java/org/automerge/SyncState.java | 4 + .../java/org/automerge/TransactionImpl.java | 4 + 8 files changed, 108 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 6f88cdf..0c15b5e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -99,6 +99,86 @@ testing { } } +// Register the custom linter task that checks for LoadLibrary.initialize() in static blocks +val checkLoadLibraryInitializer = tasks.register("checkLoadLibraryInitializer") { + description = "Checks that all classes calling AutomergeSys have LoadLibrary.initialize() in static block" + group = "verification" + + // Make the task depend on Java source files + inputs.files(fileTree("src/main/java") { + include("**/*.java") + }) + + doLast { + val violations = mutableListOf() + val exemptClasses = listOf("AutomergeSys.java", "LoadLibrary.java") + + fileTree("src/main/java") { + include("**/*.java") + }.forEach { file -> + val fileName = file.name + + // Skip exempt classes + if (fileName in exemptClasses) { + return@forEach + } + + val content = file.readText() + + // Check if file calls AutomergeSys + // Use simple contains() since we're just looking for the class name followed by a dot + val callsAutomergeSys = content.contains("AutomergeSys.") + + if (callsAutomergeSys) { + // Check if it has the static initializer with LoadLibrary.initialize() + // Look for the pattern: static { ... LoadLibrary.initialize() ... } + val hasStaticInit = Regex("""static\s*\{[^}]*LoadLibrary\.initialize\(\)""", RegexOption.DOT_MATCHES_ALL) + .containsMatchIn(content) + + if (!hasStaticInit) { + val classMatch = Regex("""(?:public\s+)?(?:abstract\s+)?class\s+(\w+)""").find(content) + val className = classMatch?.groupValues?.get(1) ?: fileName.removeSuffix(".java") + val calledClass = "AutomergeSys" + val relativePath = file.relativeTo(projectDir).path + violations.add("$relativePath: $className calls $calledClass but missing static initializer") + } + } + } + + if (violations.isNotEmpty()) { + val message = buildString { + appendLine() + appendLine("=" .repeat(80)) + appendLine("LoadLibrary.initialize() Static Initializer Check FAILED") + appendLine("=".repeat(80)) + appendLine() + appendLine("The following classes call AutomergeSys but are missing") + appendLine("a static initializer that calls LoadLibrary.initialize():") + appendLine() + violations.forEach { violation -> + appendLine(" ❌ $violation") + } + appendLine() + appendLine("To fix this, add the following static block to each class:") + appendLine() + appendLine(" static {") + appendLine(" LoadLibrary.initialize();") + appendLine(" }") + appendLine() + appendLine("=".repeat(80)) + } + throw GradleException(message) + } + + logger.lifecycle("✓ All classes that call AutomergeSys have LoadLibrary.initialize() in static block") + } +} + +// Make 'check' depend on our linter +tasks.named("check") { + dependsOn(checkLoadLibraryInitializer) +} + // Create a separate test task that runs the same tests with Java 8 runtime tasks.register("testJava8") { description = "Runs tests with Java 8 runtime to verify backward compatibility" diff --git a/lib/src/main/java/org/automerge/CommitResult.java b/lib/src/main/java/org/automerge/CommitResult.java index ec50cf6..e50628a 100644 --- a/lib/src/main/java/org/automerge/CommitResult.java +++ b/lib/src/main/java/org/automerge/CommitResult.java @@ -6,6 +6,10 @@ class CommitResult { private Optional hash; private AutomergeSys.PatchLogPointer patchLog; + static { + LoadLibrary.initialize(); + } + protected CommitResult(Optional hash, AutomergeSys.PatchLogPointer patchLog) { this.hash = hash; this.patchLog = patchLog; diff --git a/lib/src/main/java/org/automerge/Cursor.java b/lib/src/main/java/org/automerge/Cursor.java index 5eb27d1..cafbc6c 100644 --- a/lib/src/main/java/org/automerge/Cursor.java +++ b/lib/src/main/java/org/automerge/Cursor.java @@ -19,6 +19,10 @@ public class Cursor { private byte[] raw; + static { + LoadLibrary.initialize(); + } + /** * Parse the output of {@link toBytes()} * diff --git a/lib/src/main/java/org/automerge/Document.java b/lib/src/main/java/org/automerge/Document.java index eaaf089..568fd09 100644 --- a/lib/src/main/java/org/automerge/Document.java +++ b/lib/src/main/java/org/automerge/Document.java @@ -68,6 +68,10 @@ * many times it may be worth reusing actor IDs. */ public class Document implements Read { + static { + LoadLibrary.initialize(); + } + private Optional pointer; // Keep actor ID here so we a) don't have to keep passing it across the JNI // boundary and b) can access it when a transaction is in progress @@ -80,7 +84,6 @@ public class Document implements Read { /** Create a new document with a random actor ID */ public Document() { - LoadLibrary.initialize(); this.pointer = Optional.of(AutomergeSys.createDoc()); this.actorId = AutomergeSys.getActorId(this.pointer.get()); this.transactionPtr = Optional.empty(); @@ -93,14 +96,12 @@ public Document() { * the actor ID to use for this document */ public Document(byte[] actorId) { - LoadLibrary.initialize(); this.actorId = actorId; this.pointer = Optional.of(AutomergeSys.createDocWithActor(actorId)); this.transactionPtr = Optional.empty(); } private Document(DocPointer pointer) { - LoadLibrary.initialize(); this.pointer = Optional.of(pointer); this.actorId = AutomergeSys.getActorId(this.pointer.get()); this.transactionPtr = Optional.empty(); diff --git a/lib/src/main/java/org/automerge/ObjectId.java b/lib/src/main/java/org/automerge/ObjectId.java index 3e60f4c..62e701e 100644 --- a/lib/src/main/java/org/automerge/ObjectId.java +++ b/lib/src/main/java/org/automerge/ObjectId.java @@ -11,6 +11,10 @@ public class ObjectId { private byte[] raw; + static { + LoadLibrary.initialize(); + } + public static ObjectId ROOT; static { diff --git a/lib/src/main/java/org/automerge/PatchLog.java b/lib/src/main/java/org/automerge/PatchLog.java index 819c510..ff6864a 100644 --- a/lib/src/main/java/org/automerge/PatchLog.java +++ b/lib/src/main/java/org/automerge/PatchLog.java @@ -8,6 +8,10 @@ public class PatchLog { private Optional pointer; + static { + LoadLibrary.initialize(); + } + public PatchLog() { pointer = Optional.of(AutomergeSys.createPatchLog()); } diff --git a/lib/src/main/java/org/automerge/SyncState.java b/lib/src/main/java/org/automerge/SyncState.java index b2947c8..bdb61cf 100644 --- a/lib/src/main/java/org/automerge/SyncState.java +++ b/lib/src/main/java/org/automerge/SyncState.java @@ -29,6 +29,10 @@ public class SyncState { private Optional pointer; + static { + LoadLibrary.initialize(); + } + private SyncState(AutomergeSys.SyncStatePointer pointer) { this.pointer = Optional.of(pointer); } diff --git a/lib/src/main/java/org/automerge/TransactionImpl.java b/lib/src/main/java/org/automerge/TransactionImpl.java index b5f90d6..d8305ed 100644 --- a/lib/src/main/java/org/automerge/TransactionImpl.java +++ b/lib/src/main/java/org/automerge/TransactionImpl.java @@ -12,6 +12,10 @@ public class TransactionImpl implements Transaction { private Document doc; private Optional> finish; + static { + LoadLibrary.initialize(); + } + protected TransactionImpl(Document doc, AutomergeSys.TransactionPointer pointer) { this.pointer = Optional.of(pointer); this.doc = doc;