Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
41e9fae
feat(framework): add class reader
thisisalexandercook Nov 18, 2025
715a4fd
feat(fraomework): introduce filter interface
thisisalexandercook Nov 18, 2025
759402e
feat(framework): add a prototype Filter implementation - ClassListFilter
thisisalexandercook Nov 18, 2025
ef15d4a
refactor(framework): make Filter extend Predicate
thisisalexandercook Nov 18, 2025
8ecb938
refactor(framework): update class reader to use generic Filter
thisisalexandercook Nov 18, 2025
f04d36b
doc: add project notes
thisisalexandercook Nov 18, 2025
4b52083
refactor(framework): turn ClassReader into RuntimeTransformer
thisisalexandercook Nov 19, 2025
843485c
feat(framework): introduce RuntimeInstrumenter abstract class with co…
thisisalexandercook Nov 20, 2025
c851b0d
chore: spotless formatting
thisisalexandercook Nov 20, 2025
ff8663f
feat(framework): add FrameworkSafetyFilter to ignore JDK and framewor…
thisisalexandercook Nov 20, 2025
35fa6d9
feat(framework): add field write instrumentation
thisisalexandercook Nov 24, 2025
6d890ef
feat(framework): add field read and return instrumentation
thisisalexandercook Nov 24, 2025
c5b2cac
feat(framework.core): introduce abstract RuntimeChecker
thisisalexandercook Nov 25, 2025
27bf550
feat(framework.util): concrete utility checker implementation for deb…
thisisalexandercook Nov 25, 2025
3d26575
refactor(framework.agent): update agent to use debugging checker
thisisalexandercook Nov 25, 2025
ed5cf4d
refactor(framework.agent): Update RuntimeTransformer to use new check…
thisisalexandercook Nov 25, 2025
fc930c4
feat(framework.runtime): introduce an abstract RuntimeVerifier and Vi…
thisisalexandercook Nov 25, 2025
88c1eda
chore(git): update .gitignore
thisisalexandercook Nov 25, 2025
e2fe8f9
build: introduce checker subproject
thisisalexandercook Nov 26, 2025
1a9307c
feat(checker): add nullness checker
thisisalexandercook Nov 27, 2025
b9a1574
feat(framework): add field write checks
thisisalexandercook Nov 28, 2025
1f53216
feat(framework): add return checks
thisisalexandercook Nov 28, 2025
c895dac
feat(framework): add field read
thisisalexandercook Dec 1, 2025
9f56cf6
refactor(framework): rename generateParamCheck to generateParameterCheck
thisisalexandercook Dec 1, 2025
ab961f6
refactor(framework): remove fully qualified name usage
thisisalexandercook Dec 1, 2025
df51bae
feat(framework): introduce dynamic dispatch (WIP)
thisisalexandercook Dec 1, 2025
13ccfc4
feat(framework): method invocation implementation
thisisalexandercook Dec 1, 2025
94f8b07
feat(framework): introduce policy abstraction
thisisalexandercook Dec 2, 2025
758da07
feat(framework): introduce global policy
thisisalexandercook Dec 17, 2025
2b580e0
refactor(framework): policy and resolution refactor
thisisalexandercook Dec 17, 2025
bf14183
feat(framework): add an AnnotatedFor annotation with runtime retention
thisisalexandercook Dec 17, 2025
5227643
feat(framework): add AnnotatedFor scanning
thisisalexandercook Dec 17, 2025
1d7a5c3
feat(framework): introduce array store and load checks
thisisalexandercook Dec 18, 2025
40203e8
feat(test): introduce test suite
thisisalexandercook Dec 18, 2025
79b78f9
feat(test-util): introduce test suite
thisisalexandercook Dec 19, 2025
3d8278a
test: add field test cases
thisisalexandercook Dec 19, 2025
7437e21
test: add bridge tests
thisisalexandercook Dec 22, 2025
a97e23c
refactor: move checked reflection from instrumenter to policy
thisisalexandercook Dec 22, 2025
835d46f
chore: comment cleanup
thisisalexandercook Dec 22, 2025
2b1dbfe
test: add nullable override
thisisalexandercook Dec 22, 2025
9723804
feat(framework): support nullable in an unchecked override
thisisalexandercook Dec 22, 2025
c0a5ed7
chore: error prone cleanup
thisisalexandercook Dec 22, 2025
44ea6c3
chore: debug cleanup
thisisalexandercook Dec 22, 2025
054e878
doc: update runtime checker javadoc
thisisalexandercook Dec 22, 2025
8b6037d
fix(framework): handle global vs. annotated for mode properly
thisisalexandercook Dec 22, 2025
3d41e60
chore: comment cleanup
thisisalexandercook Dec 22, 2025
bf99fbd
fix(test): hack to select correct jars in test harness
thisisalexandercook Dec 23, 2025
8492d35
test: add mrore invoke test cases
thisisalexandercook Dec 23, 2025
7e16a6f
docs: update README and add examples
thisisalexandercook Dec 23, 2025
c799839
fix: README formatting
thisisalexandercook Dec 23, 2025
85ab419
Merge remote-tracking branch 'upstream/main' into development
thisisalexandercook Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,22 @@

# Ignore Gradle build output directory
build

# Example Classfiles
examples/global-policy/global/
examples/standard-policy/standard/

# Eclipse
.metadata
.classpath
.project
.externalToolBuilders
.settings
**/*.factorypath

# Environment setup
.direnv/
.envrc

# bins
framework/bin/
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

103 changes: 103 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#+TITLE: Runtime Framework
* About

A Java Agent-based framework designed to enforce pluggable type systems at runtime. By instrumenting bytecode at load-time, the framework ensures that type contracts (such as @NonNull) are respected at the boundary between annotated and unannotated code. This provides a traceable safety net when integrating code verified by tools like the [[https://github.com/eisop/checker-framework][EISOP Checker Framework]] with legacy or unchecked external libraries.

** Project Structure

- ~framework/~: The core agent and instrumentation logic.
- ~agent~: The Java Agent entry point and transformer.
- ~core~: The main instrumentation engine
- ~policy~: Defines /when/ checks are injected.
- Currently, there are two defined policies: a *Standard Policy* and a *Global Policy*.
- The **Standard Policy** instruments only code that has been marked as "Checked".
- The **Global Policy** extends this to instrument Unchecked code as well.
- ~resolution~: Handles class hierarchy analysis for bridge methods.
- ~filter~: Controls which classes are instrumented.
- ~runtime~: The runtime library that instrumented code calls.
- ~checker/~: Concrete implementations of type systems (currently Nullness).
- ~test-utils/~: Shared testing infrastructure for writing integration tests.

** Requirements

This project requires JDK25+.

** Status

This project is in early development and will still go through many breaking changes regarding API and configuration. Its overall design goal is to be flexible enough to support any pluggable type system but is currently focused on Nullness.

* Building the Project

This project uses Gradle 9.0. To build the agent and runtime libraries:

#+begin_src bash
./gradlew build
#+end_src

Note: This will also run the test suite located under ~checker/src/test/java/~ (executing integration tests via ~NullnessDirectoryTest~).

This will generate the artifacts in ~build/dist/~:
- ~framework.jar~ (The Java Agent)
- ~checker.jar~ (The Runtime Nullness Checker)
- ~test-utils.jar~ (Testing helpers)
- ~checker-qual.jar~ (Annotations)

* Usage

To use the agent, you need to launch your Java application with the ~-javaagent~ flag and configure the classpath.

** 1. Standard Policy Example

There are two examples under ~examples/~ that demonstrate basic usage. These examples catch exceptions to print success messages instead of crashing, allowing you to see multiple violations in one run while still using the exception handler.

The **Standard Policy** example demonstrates instrumentation on only checked code, where checked code is defined as code within the scope of an ~AnnotatedFor~ annotation. This policy is limited insofar as it will not instrument code outside of an ~AnnotatedFor~ scope, leaving field wrties from unchecked to checked code and the checked parent, unchecked child relationship unaccounted for. However, limiting checks to only a checked scope could be prefered in some situations.

To compile the standard policy example:

#+BEGIN_SRC bash
javac -cp 'build/dist/*' -d examples/standard-policy/ examples/standard-policy/*.java
#+END_SRC

To run the example:

#+BEGIN_SRC bash
java \
-javaagent:build/dist/framework.jar \
-Druntime.checker=io.github.eisop.runtimeframework.checker.nullness.NullnessRuntimeChecker \
-Druntime.trustAnnotatedFor=true \
-cp 'examples/standard-policy:build/dist/*' \
standard.StandardDemo
#+END_SRC


** 2. Global Policy Example

The global policy instruments both checked and unchecked code. It applies all the same checks present in the standard policy, but now field writes from unchecked -> checked and Checked Parent Unchecked child relationships can be handled accordingly.

To compile the global policy example:

#+BEGIN_SRC bash
javac -cp 'build/dist/*' -d examples/global-policy/ examples/global-policy/*.java
#+END_SRC

To run the example:

#+begin_src bash
java \
-javaagent:build/dist/framework.jar \
-Druntime.global=true \
-Druntime.checker=io.github.eisop.runtimeframework.checker.nullness.NullnessRuntimeChecker \
-Druntime.trustAnnotatedFor=true \
-cp 'examples/global-policy:build/dist/*' \
global.GlobalDemo
#+end_src

** Configuration Properties

| Property | Description | Example |
|-----------------------------+-------------------------------------------------------------------------+--------------------------------------------------|
| ~runtime.checker~ | Fully qualified class name of the ~RuntimeChecker~ implementation. | ~...checker.nullness.NullnessRuntimeChecker~ |
| ~runtime.classes~ | Comma-separated list of classes to treat as "Checked" (Safe). | ~com.app.Main,com.app.Utils~ |
| ~runtime.global~ | ~true~ to enable Global Policy (scans all classes for external writes). | ~true~ |
| ~runtime.trustAnnotatedFor~ | ~true~ to automatically treat classes with ~@AnnotatedFor~ as Checked. | ~true~ |
| ~runtime.handler~ | ~ViolationHandler~ class to use (defaults to Throwing). | ~io.github.eisop.testutils.TestViolationHandler~ |
90 changes: 74 additions & 16 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,43 +1,101 @@
plugins {
id 'base'
id 'net.ltgt.errorprone' version '4.3.0' apply false
id 'com.diffplug.spotless' version '8.1.0' apply false
id 'java'
}

subprojects {
allprojects {
repositories {
mavenCentral()
mavenCentral()
}
// TODO: better spotless integration to support groovy etc...

apply plugin: 'com.diffplug.spotless'
spotless {
groovyGradle {
target '**/*.gradle'
targetExclude 'build/**', '.gradle/**', '.direnv/**'
greclipse()
leadingTabsToSpaces(4)
trimTrailingWhitespace()
endWithNewline()
}
}

plugins.withId('java') {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}

spotless {
java {
googleJavaFormat('1.32.0')
spotless {
java {
googleJavaFormat('1.32.0')
}
}

apply plugin: 'net.ltgt.errorprone'
dependencies {
errorprone("com.google.errorprone:error_prone_core:2.44.0")
}

tasks.withType(Test).configureEach {
// Pass the absolute path of the 'dist' folder to the test JVM
useJUnitPlatform()
systemProperty 'agent.dist.dir', rootProject.layout.buildDirectory.dir("dist").get().asFile.absolutePath

// Ensure artifacts are built before running tests
dependsOn rootProject.tasks.named('copyToDist')

// Allow stdout to show up in Gradle logs
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
}
}
}

allprojects {
apply plugin: 'eclipse'
eclipse.classpath {
defaultOutputDir = file("build/default")
file.whenMerged { cp ->
defaultOutputDir = file("build/default")
file.whenMerged { cp ->
cp.entries.forEach { cpe ->
if (cpe instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder) {
if (cpe instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder) {
cpe.output = cpe.output.replace "bin/", "build/classes/java/"
}
if (cpe instanceof org.gradle.plugins.ide.eclipse.model.Output) {
}
if (cpe instanceof org.gradle.plugins.ide.eclipse.model.Output) {
cpe.path = cpe.path.replace "bin/", "build/"
}
}
}
}
}
}
}

task copyToDist(type: Copy) {
// FIX: Force this task to run every time (disable incremental build check)
outputs.upToDateWhen { false }

// Use the Layout API to get a provider for the destination directory.
// This is safe to capture for the Configuration Cache.
def distDir = layout.buildDirectory.dir("dist")
into distDir

// FIX: Flatten the lists so the Copy task sees the actual files, not a List of Lists
from {
subprojects.collect { it.tasks.withType(Jar) }.flatten()
}
from {
subprojects.findAll { it.plugins.hasPlugin('java') || it.plugins.hasPlugin('java-library') }
.collect { it.configurations.getByName('runtimeClasspath') }
.flatten()
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE

// Log so we know it ran
doFirst {
// Use the captured provider (.get().asFile) instead of '$buildDir'
println ">> Copying artifacts to ${distDir.get().asFile}"
}
}

build.dependsOn copyToDist
26 changes: 26 additions & 0 deletions checker/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
id 'java'
}


java {
toolchain {
languageVersion = JavaLanguageVersion.of(25) // Target JDK 25
}
}

ext {
versions = [
eisopVersion: '3.49.3-eisop1'
]
}

dependencies {
implementation project(':framework')
implementation "io.github.eisop:checker-qual:${versions.eisopVersion}"

testImplementation platform('org.junit:junit-bom:5.10.0')
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation project(':test-utils')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.eisop.runtimeframework.checker.nullness;

import io.github.eisop.runtimeframework.core.TargetAnnotation;
import java.lang.annotation.Annotation;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.TypeKind;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
import org.checkerframework.checker.nullness.qual.NonNull;

public class NonNullTarget implements TargetAnnotation {

private static final ClassDesc VERIFIER = ClassDesc.of(NullnessRuntimeVerifier.class.getName());
private static final String METHOD = "checkNotNull";
private static final MethodTypeDesc DESC =
MethodTypeDesc.ofDescriptor("(Ljava/lang/Object;Ljava/lang/String;)V");

@Override
public Class<? extends Annotation> annotationType() {
return NonNull.class;
}

@Override
public void check(CodeBuilder b, TypeKind type, String diagnosticName) {
if (type == TypeKind.REFERENCE) {
b.ldc(diagnosticName + " must be NonNull");
b.invokestatic(VERIFIER, METHOD, DESC);
} else {
if (type.slotSize() == 1) b.pop();
else b.pop2();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.eisop.runtimeframework.checker.nullness;

import io.github.eisop.runtimeframework.core.AnnotationInstrumenter;
import io.github.eisop.runtimeframework.core.OptOutAnnotation;
import io.github.eisop.runtimeframework.core.RuntimeChecker;
import io.github.eisop.runtimeframework.core.RuntimeInstrumenter;
import io.github.eisop.runtimeframework.filter.ClassInfo;
import io.github.eisop.runtimeframework.filter.Filter;
import io.github.eisop.runtimeframework.policy.EnforcementPolicy;
import io.github.eisop.runtimeframework.resolution.HierarchyResolver;
import io.github.eisop.runtimeframework.resolution.ReflectionHierarchyResolver;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;

public class NullnessRuntimeChecker extends RuntimeChecker {

@Override
public String getName() {
return "nullness";
}

@Override
public RuntimeInstrumenter getInstrumenter(Filter<ClassInfo> filter) {
EnforcementPolicy policy =
createPolicy(
List.of(new NonNullTarget()), List.of(new OptOutAnnotation(Nullable.class)), filter);

HierarchyResolver resolver =
new ReflectionHierarchyResolver(
className -> filter.test(new ClassInfo(className.replace('.', '/'), null, null)));

return new AnnotationInstrumenter(policy, resolver, filter);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.eisop.runtimeframework.checker.nullness;

import io.github.eisop.runtimeframework.runtime.RuntimeVerifier;

/**
* The static trampoline for Nullness checks.
*
* <p>The {@link NullnessRuntimeInstrumenter} generates {@code invokestatic} calls to the methods in
* this class. These methods perform the actual runtime validation and report violations to the
* central {@link RuntimeVerifier}.
*/
public class NullnessRuntimeVerifier extends RuntimeVerifier {

/**
* Verifies that the given object is not null.
*
* @param o The object to check
* @param message The error message to report if the object is null
*/
public static void checkNotNull(Object o, String message) {
if (o == null) {
reportViolation("Nullness", message);
}
}
}
Loading