Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.eisop.runtimeframework.checker.nullness;

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

/**
Expand All @@ -18,8 +19,19 @@ public class NullnessRuntimeVerifier extends RuntimeVerifier {
* @param message The error message to report if the object is null
*/
public static void checkNotNull(Object o, String message) {
checkNotNull(o, message, AttributionKind.LOCAL);
}

/**
* Verifies that the given object is not null, with specific attribution.
*
* @param o The object to check
* @param message The error message to report if the object is null
* @param attribution The attribution strategy
*/
public static void checkNotNull(Object o, String message, AttributionKind attribution) {
if (o == null) {
reportViolation("Nullness", message);
reportViolation("Nullness", message, attribution);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.eisop.runtimeframework.checker.nullness;

import io.github.eisop.runtimeframework.core.RuntimeVerifier;
import io.github.eisop.runtimeframework.runtime.AttributionKind;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.TypeKind;
import java.lang.constant.ClassDesc;
Expand All @@ -9,15 +10,48 @@
public class NullnessVerifier implements RuntimeVerifier {

private static final ClassDesc VERIFIER = ClassDesc.of(NullnessRuntimeVerifier.class.getName());
private static final String METHOD = "checkNotNull";
private static final MethodTypeDesc DESC =
private static final ClassDesc ATTRIBUTION_KIND =
ClassDesc.of(AttributionKind.class.getName());

private static final String METHOD_DEFAULT = "checkNotNull";
private static final MethodTypeDesc DESC_DEFAULT =
MethodTypeDesc.ofDescriptor("(Ljava/lang/Object;Ljava/lang/String;)V");

private static final String METHOD_ATTRIBUTED = "checkNotNull";
private static final MethodTypeDesc DESC_ATTRIBUTED =
MethodTypeDesc.ofDescriptor(
"(Ljava/lang/Object;Ljava/lang/String;Lio/github/eisop/runtimeframework/runtime/AttributionKind;)V");

private final AttributionKind attribution;

public NullnessVerifier() {
this(AttributionKind.LOCAL);
}

public NullnessVerifier(AttributionKind attribution) {
this.attribution = attribution;
}

@Override
public RuntimeVerifier withAttribution(AttributionKind kind) {
if (this.attribution == kind) return this;
return new NullnessVerifier(kind);
}

@Override
public void generateCheck(CodeBuilder b, TypeKind type, String diagnosticName) {
if (type == TypeKind.REFERENCE) {
b.ldc(diagnosticName + " must be NonNull");
b.invokestatic(VERIFIER, METHOD, DESC);

if (attribution == AttributionKind.LOCAL) {
b.invokestatic(VERIFIER, METHOD_DEFAULT, DESC_DEFAULT);
} else {
b.getstatic(
ATTRIBUTION_KIND,
attribution.name(),
ClassDesc.ofDescriptor("Lio/github/eisop/runtimeframework/runtime/AttributionKind;"));
b.invokestatic(VERIFIER, METHOD_ATTRIBUTED, DESC_ATTRIBUTED);
}
} else {
if (type.slotSize() == 1) b.pop();
else b.pop2();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// :: error: (Parameter 0 in inherited method dangerousAction must be NonNull)
// :: error: (Parameter 0 in inherited method protectedAction must be NonNull)

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import io.github.eisop.runtimeframework.qual.AnnotatedFor;
Expand All @@ -11,28 +8,34 @@ public class InheritanceBridgeTest extends UncheckedParent {
public static void main(String[] args) {
InheritanceBridgeTest test = new InheritanceBridgeTest();
test.dangerousAction("safe");

// :: error: (Parameter 0 in inherited method dangerousAction must be NonNull)
test.dangerousAction(null);

test.overrideMe("safe", "safe");
test.overrideMe(null, "unsafe");
test.overrideMe("safe", "null");
test.overrideMe("safe", "safe");

// :: error: (Parameter 0 must be NonNull)
test.overrideMe(null, "unsafe");

test.overrideMe("safe", "null");

test.protectedAction("safe");

test.protectedAction("safe");
// :: error: (Parameter 0 in inherited method protectedAction must be NonNull)
test.protectedAction(null);

test.finalAction("safe");
test.finalAction("safe");
test.finalAction(null);
// cannot bridge final methods, no error here
// cannot bridge final methods, no error here

String unsafe = test.returnAction();
String unsafe = test.returnAction();
// :: error: (Local Variable Assignment (Slot 2) must be NonNull)

@Nullable String again = test.returnAction();
@Nullable String again = test.returnAction();
}

@Override
public void overrideMe(@NonNull String inputA, @Nullable String inputB) {
// :: error: (Parameter 0 must be NonNull)
System.out.println("safe version of this method" + inputA + inputB);
System.out.println("safe version of this method" + inputA + inputB);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@
public class UncheckedToCheckedInstance {

public void checkedMethod(@NonNull String input) {
// :: error: (Parameter 0 must be NonNull)
}

public void nullableCheckedMethod(@Nullable String input) {
}

public void mixedCheckedMethod(@Nullable String input, @NonNull String anotherInput) {
// :: error: (Parameter 1 must be NonNull)
}

static class UncheckedCaller {
public static void invoke(UncheckedToCheckedInstance target) {
// :: error: (Parameter 0 must be NonNull)
target.checkedMethod(null);
target.nullableCheckedMethod(null);
target.mixedCheckedMethod(null, null);
target.nullableCheckedMethod(null);
// :: error: (Parameter 1 must be NonNull)
target.mixedCheckedMethod(null, null);
}
}

public static void main(String[] args) {
UncheckedToCheckedInstance target = new UncheckedToCheckedInstance();
UncheckedCaller.invoke(target);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@
public class UncheckedToCheckedStatic {

public static void staticCheckedMethod(@NonNull String input) {
// :: error: (Parameter 0 must be NonNull)
}

public static void staticCheckedNullableMethod(@Nullable String input) {
}

public static void staticCheckedMixedMethod(@Nullable String input, @NonNull String anotherInput) {
// :: error: (Parameter 1 must be NonNull)
}

static class UncheckedCaller {
public static void invoke() {
// :: error: (Parameter 0 must be NonNull)
UncheckedToCheckedStatic.staticCheckedMethod(null);
UncheckedToCheckedStatic.staticCheckedNullableMethod(null);
// :: error: (Parameter 1 must be NonNull)
UncheckedToCheckedStatic.staticCheckedMixedMethod(null, null);
}
}

public static void main(String[] args) {
UncheckedCaller.invoke();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
@AnnotatedFor("nullness")
public class Constructors {
public static void main(String[] args) {
// :: error: (Parameter 0 must be NonNull)
new Constructors(null);

// :: error: (Parameter 0 must be NonNull)
new Constructors(null, "ignore");
}

public Constructors(String s) {
// :: error: (Parameter 0 must be NonNull)
}

public Constructors(@NonNull String a, String b) {
// :: error: (Parameter 0 must be NonNull)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,31 @@ public class MixedMethods {

public static void main(String[] args) {
// 1. Explicit NonNull
// :: error: (Parameter 0 must be NonNull)
checkExplicit(null);

// 2. Implicit NonNull (Strict Default)
// :: error: (Parameter 0 must be NonNull)
checkImplicit(null);

// 3. Explicit Nullable (Should NOT error)
checkNullable(null);

// 3. Explicit Nullable (Should NOT error)
// 4. Multiple Parameters
// :: error: (Parameter 1 must be NonNull)
// :: error: (Parameter 2 must be NonNull)
checkMultiple(null,null,null,null);
}

public static void checkExplicit(@NonNull String s) {
// :: error: (Parameter 0 must be NonNull)
}

public static void checkImplicit(String s) {
// :: error: (Parameter 0 must be NonNull)
}

public static void checkNullable(@Nullable String s) {
// No error expected here
}

public static void checkMultiple(@Nullable String s, String q, String r, @Nullable String v) {
// :: error: (Parameter 1 must be NonNull)
// :: error: (Parameter 2 must be NonNull)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.eisop.runtimeframework.core;

import io.github.eisop.runtimeframework.runtime.AttributionKind;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.TypeKind;

Expand All @@ -25,4 +26,14 @@ public interface RuntimeVerifier {
* error messages.
*/
void generateCheck(CodeBuilder b, TypeKind type, String diagnosticName);

/**
* Returns a verifier that attributes the violation according to the given strategy.
*
* @param kind The attribution strategy.
* @return A verifier with the specified attribution.
*/
default RuntimeVerifier withAttribution(AttributionKind kind) {
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.github.eisop.runtimeframework.core.ValidationKind;
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.runtime.AttributionKind;
import io.github.eisop.runtimeframework.resolution.ParentMethod;
import java.lang.classfile.Annotation;
import java.lang.classfile.Attributes;
Expand Down Expand Up @@ -58,7 +60,8 @@ protected RuntimeVerifier resolveVerifier(List<Annotation> annotations) {
public RuntimeVerifier getParameterCheck(MethodModel method, int paramIndex, TypeKind type) {
if (type != TypeKind.REFERENCE) return null;
List<Annotation> annos = getMethodParamAnnotations(method, paramIndex);
return resolveVerifier(annos);
RuntimeVerifier verifier = resolveVerifier(annos);
return (verifier != null) ? verifier.withAttribution(AttributionKind.CALLER) : null;
}

@Override
Expand Down Expand Up @@ -178,7 +181,7 @@ public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int pa
List<Annotation> annos = getMethodParamAnnotations(method, paramIndex);

RuntimeVerifier verifier = resolveVerifier(annos);
if (verifier != null) return verifier;
if (verifier != null) return verifier.withAttribution(AttributionKind.CALLER);

// Check default
var paramTypes = method.methodTypeSymbol().parameterList();
Expand All @@ -198,7 +201,7 @@ public RuntimeVerifier getBridgeParameterCheck(ParentMethod parentMethod, int pa
if (!isExplicitNoop) {
TypeSystemConfiguration.ConfigEntry defaultEntry = configuration.getDefault();
if (defaultEntry != null && defaultEntry.kind() == ValidationKind.ENFORCE) {
return defaultEntry.verifier();
return defaultEntry.verifier().withAttribution(AttributionKind.CALLER);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.eisop.runtimeframework.runtime;

/** Defines how a violation should be attributed in the stack trace. */
public enum AttributionKind {
/**
* The violation occurred in the current method.
*
* <p>Example: A method returns null but promised @NonNull. The blame is on the method itself.
*/
LOCAL,

/**
* The violation occurred at the call site of the current method.
*
* <p>Example: A method received null as an argument but requires @NonNull. The blame is on the
* caller.
*/
CALLER
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,26 @@ public LoggingViolationHandler(PrintStream out) {
}

@Override
public void handleViolation(String checkerName, String message) {
out.printf("[RuntimeFramework - %s] %s%n", checkerName, message);
public void handleViolation(String checkerName, String message, AttributionKind attribution) {
StackTraceElement source = findSource(attribution);
String location =
(source != null) ? source.getFileName() + ":" + source.getLineNumber() : "Unknown:0";

out.printf("[RuntimeFramework - %s] (%s) %s%n", checkerName, location, message);
}

private StackTraceElement findSource(AttributionKind attribution) {
return StackWalker.getInstance()
.walk(
stream ->
stream
// Skip the runtime framework infrastructure
.filter(f -> !f.getClassName().startsWith("io.github.eisop.runtimeframework"))
// Skip the method that triggered the violation if we are attributing to the
// CALLER
.skip(attribution == AttributionKind.CALLER ? 1 : 0)
.findFirst()
.map(StackWalker.StackFrame::toStackTraceElement)
.orElse(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public static void setViolationHandler(ViolationHandler newHandler) {

/** Reports a violation to the current handler. */
protected static void reportViolation(String checkerName, String message) {
handler.handleViolation(checkerName, message);
reportViolation(checkerName, message, AttributionKind.LOCAL);
}

/** Reports a violation to the current handler with specific attribution. */
protected static void reportViolation(
String checkerName, String message, AttributionKind attribution) {
handler.handleViolation(checkerName, message, attribution);
}
}
Loading
Loading