Skip to content

Commit

Permalink
feat: analyze bytecode of methods to determine return type nullability
Browse files Browse the repository at this point in the history
  • Loading branch information
sebthom committed Oct 2, 2024
1 parent 7c45311 commit 0aead54
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 79 deletions.
11 changes: 9 additions & 2 deletions eea-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ SPDX-FileContributor: Sebastian Thomschke (https://sebthom.de), Vegard IT GmbH (
SPDX-License-Identifier: EPL-2.0
SPDX-ArtifactOfProjectHomePage: https://github.com/vegardit/no-npe
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -53,7 +55,12 @@ SPDX-ArtifactOfProjectHomePage: https://github.com/vegardit/no-npe
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.176</version>
<version>4.8.177</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>9.7</version>
</dependency>

<!-- test dependencies -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
import com.vegardit.no_npe.eea_generator.EEAFile.ClassMember;
import com.vegardit.no_npe.eea_generator.EEAFile.SaveOption;
import com.vegardit.no_npe.eea_generator.EEAFile.ValueWithComment;
import com.vegardit.no_npe.eea_generator.internal.BytecodeAnalyzer;
import com.vegardit.no_npe.eea_generator.internal.ClassGraphUtils;
import com.vegardit.no_npe.eea_generator.internal.ClassGraphUtils.MethodReturnKind;
import com.vegardit.no_npe.eea_generator.internal.Props;

import io.github.classgraph.ClassGraph;
Expand Down Expand Up @@ -218,7 +221,7 @@ public static void main(final String... args) throws Exception {
}

protected static ValueWithComment computeAnnotatedSignature(final EEAFile.ClassMember member, final ClassInfo classInfo,
final ClassMemberInfo memberInfo) {
final ClassMemberInfo memberInfo, final BytecodeAnalyzer bytecodeAnalyzer) {

final var templates = new ArrayList<EEAFile>();
if (isThrowable(classInfo)) {
Expand All @@ -238,73 +241,91 @@ protected static ValueWithComment computeAnnotatedSignature(final EEAFile.ClassM
if (memberInfo instanceof MethodInfo) {
final MethodInfo methodInfo = (MethodInfo) memberInfo;

/*
* mark the return value of builder methods as @NonNull.
*/
if (classInfo.getName().endsWith("Builder") //
&& !methodInfo.isStatic() // non-static
&& methodInfo.isPublic() //
&& methodInfo.getTypeDescriptor().getResultType() instanceof ClassRefTypeSignature //
&& (methodInfo.getName().equals("build") && methodInfo.getParameterInfo().length == 0 //
|| Objects.equals(((ClassRefTypeSignature) methodInfo.getTypeDescriptor().getResultType()).getClassInfo(), classInfo)))
// (...)Lcom/example/MyBuilder -> (...)L1com/example/MyBuilder;
return new ValueWithComment(insert(member.originalSignature.value, member.originalSignature.value.lastIndexOf(")") + 2, "1"),
"");

/*
* mark the parameter of Comparable#compareTo(Object) as @NonNull.
*/
if (classInfo.implementsInterface("java.lang.Comparable") //
&& !methodInfo.isStatic() // non-static
&& member.originalSignature.value.endsWith(")I") // returns Integer
&& methodInfo.isPublic() //
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor() instanceof ClassRefTypeSignature)
// (Lcom/example/Entity;)I -> (L1com/example/Entity;)I
return new ValueWithComment(insert(member.originalSignature.value, 2, "1"), "");

/*
* mark the parameter of single-parameter void methods as @NonNull,
* if the class name matches "*Listener" and the parameter type name matches "*Event"
*/
if (classInfo.isInterface() //
&& classInfo.getName().endsWith("Listener") //
&& !methodInfo.isStatic() // non-static
&& member.originalSignature.value.endsWith(")V") // returns void
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor().toString().endsWith("Event"))
// (Ljava/lang/String;)V -> (L1java/lang/String;)V
return new ValueWithComment(insert(member.originalSignature.value, 2, "1"), "");

/*
* mark the parameter of single-parameter methods as @NonNull
* with signature matching: void (add|remove)*Listener(*Listener)
*/
if (!methodInfo.isStatic() // non-static
&& (methodInfo.getName().startsWith("add") || methodInfo.getName().startsWith("remove")) //
&& methodInfo.getName().endsWith("Listener") //
&& member.originalSignature.value.endsWith(")V") // returns void
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor().toString().endsWith("Listener"))
return new ValueWithComment( //
member.originalSignature.value.startsWith("(") //
// (Lcom/example/MyListener;)V -> (L1com/example/MyListener;)V
// (TT;)V -> (T1T;)V
? insert(member.originalSignature.value, 2, "1") //
// <T::Lcom/example/MyListener;>(TT;)V --> <1T::Lcom/example/MyListener;>(TT;)V
: insert(member.originalSignature.value, 1, "1"), //
"");

if (hasObjectReturnType(member)) { // returns non-void
if (hasNullableAnnotation(methodInfo.getAnnotationInfo()))
final var returnKind = ClassGraphUtils.getMethodReturnKind(methodInfo);
if (returnKind == MethodReturnKind.ARRAY || returnKind == MethodReturnKind.OBJECT) {

final var returnTypeNullability = bytecodeAnalyzer.determineMethodReturnTypeNullability(methodInfo);
/*
* mark the return value of a method as nullable if the byte code analysis of the method body determines it returns null values
* or the method is annotated with a known nullable annotation.
*/
if (returnTypeNullability.isNullable() //
|| hasNullableAnnotation(methodInfo.getAnnotationInfo()))
// ()Ljava/lang/String -> ()L0java/lang/String;
return new ValueWithComment(insert(member.originalSignature.value, member.originalSignature.value.lastIndexOf(")") + 2, "0"),
"");

if (hasNonNullAnnotation(methodInfo.getAnnotationInfo()))
/*
* mark the return value of a method as non-null if the method is annotated with a non-null annotation
* or has a method name starting with "create".
*/
if (returnTypeNullability.isNonNull() //
|| hasNonNullAnnotation(methodInfo.getAnnotationInfo()) //
|| methodInfo.getName().startsWith("create"))
// ()Ljava/lang/String -> ()L1java/lang/String;
// create...(...)LLcom/example/Entity -> create...(...)L1Lcom/example/Entity;
return new ValueWithComment(insert(member.originalSignature.value, member.originalSignature.value.lastIndexOf(")") + 2, "1"),
"");

/*
* mark the return value of builder methods as @NonNull.
*/
if (classInfo.getName().endsWith("Builder") //
&& !methodInfo.isStatic() // non-static
&& methodInfo.isPublic() //
&& methodInfo.getTypeDescriptor().getResultType() instanceof ClassRefTypeSignature //
&& (methodInfo.getName().equals("build") && methodInfo.getParameterInfo().length == 0 //
|| Objects.equals(((ClassRefTypeSignature) methodInfo.getTypeDescriptor().getResultType()).getClassInfo(),
classInfo)))
// (...)Lcom/example/MyBuilder -> (...)L1com/example/MyBuilder;
return new ValueWithComment(insert(member.originalSignature.value, member.originalSignature.value.lastIndexOf(")") + 2, "1"),
"");

} else {

/*
* mark the parameter of Comparable#compareTo(Object) as @NonNull.
*/
if (classInfo.implementsInterface("java.lang.Comparable") //
&& !methodInfo.isStatic() // non-static
&& member.originalSignature.value.endsWith(")I") // returns Integer
&& methodInfo.isPublic() //
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor() instanceof ClassRefTypeSignature)
// (Lcom/example/Entity;)I -> (L1com/example/Entity;)I
return new ValueWithComment(insert(member.originalSignature.value, 2, "1"), "");

/*
* mark the parameter of single-parameter void methods as @NonNull,
* if the class name matches "*Listener" and the parameter type name matches "*Event"
*/
if (classInfo.isInterface() //
&& classInfo.getName().endsWith("Listener") //
&& !methodInfo.isStatic() // non-static
&& member.originalSignature.value.endsWith(")V") // returns void
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor().toString().endsWith("Event"))
// (Ljava/lang/String;)V -> (L1java/lang/String;)V
return new ValueWithComment(insert(member.originalSignature.value, 2, "1"), "");

/*
* mark the parameter of single-parameter methods as @NonNull
* with signature matching: void (add|remove)*Listener(*Listener)
*/
if (!methodInfo.isStatic() // non-static
&& (methodInfo.getName().startsWith("add") || methodInfo.getName().startsWith("remove")) //
&& methodInfo.getName().endsWith("Listener") //
&& member.originalSignature.value.endsWith(")V") // returns void
&& methodInfo.getParameterInfo().length == 1 // only 1 parameter
&& methodInfo.getParameterInfo()[0].getTypeDescriptor().toString().endsWith("Listener"))
return new ValueWithComment( //
member.originalSignature.value.startsWith("(") //
// (Lcom/example/MyListener;)V -> (L1com/example/MyListener;)V
// (TT;)V -> (T1T;)V
? insert(member.originalSignature.value, 2, "1") //
// <T::Lcom/example/MyListener;>(TT;)V --> <1T::Lcom/example/MyListener;>(TT;)V
: insert(member.originalSignature.value, 1, "1"), //
"");
}
}

Expand All @@ -324,14 +345,6 @@ protected static ValueWithComment computeAnnotatedSignature(final EEAFile.ClassM
return new ValueWithComment(member.originalSignature.value);
}

protected static boolean hasObjectReturnType(final EEAFile.ClassMember member) {
final String sig = member.originalSignature.value;
// object return type: (Ljava/lang/String;)Ljava/lang/String; or (Ljava/lang/String;)TT;
// void return type: (Ljava/lang/String;)V
// primitive return type: (Ljava/lang/String;)B
return sig.charAt(sig.length() - 2) != ')';
}

protected static EEAFile computeEEAFile(final ClassInfo classInfo) {
LOG.log(Level.DEBUG, "Scanning class [{0}]...", classInfo.getName());

Expand Down Expand Up @@ -390,6 +403,8 @@ protected static EEAFile computeEEAFile(final ClassInfo classInfo) {
}
eeaFile.addEmptyLine();

final var bytecodeAnalyzer = new BytecodeAnalyzer(classInfo);

// static fields
for (final FieldInfo f : getStaticFields(fields)) {
if (classInfo.isEnum()) {
Expand All @@ -400,28 +415,28 @@ protected static EEAFile computeEEAFile(final ClassInfo classInfo) {
}

final var member = eeaFile.addMember(f.getName(), f.getTypeSignatureOrTypeDescriptorStr()); // CHECKSTYLE:IGNORE .*
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, f);
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, f, bytecodeAnalyzer);
}
eeaFile.addEmptyLine();

// static methods
for (final MethodInfo m : getStaticMethods(methods)) {
final var member = eeaFile.addMember(m.getName(), m.getTypeSignatureOrTypeDescriptorStr());
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, m);
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, m, bytecodeAnalyzer);
}
eeaFile.addEmptyLine();

// instance fields
for (final FieldInfo f : getInstanceFields(fields)) {
final var member = eeaFile.addMember(f.getName(), f.getTypeSignatureOrTypeDescriptorStr()); // CHECKSTYLE:IGNORE .*
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, f);
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, f, bytecodeAnalyzer);
}
eeaFile.addEmptyLine();

// instance methods
for (final MethodInfo m : getInstanceMethods(methods)) {
final var member = eeaFile.addMember(m.getName(), m.getTypeSignatureOrTypeDescriptorStr()); // CHECKSTYLE:IGNORE .*
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, m);
member.annotatedSignature = computeAnnotatedSignature(member, classInfo, m, bytecodeAnalyzer);
}
return eeaFile;
}
Expand Down
Loading

0 comments on commit 0aead54

Please sign in to comment.