diff --git a/.idea/misc.xml b/.idea/misc.xml index d0a36e3..2093826 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index eb46c70..0000000 --- a/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -rootProject.name = 'spotbugs-jspecify-plugin' -includeBuild '../jspecify' -includeBuild '../gradle-modules-plugin' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..6ff1071 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "spotbugs-jspecify-plugin" +includeBuild("../jspecify") +includeBuild("../gradle-modules-plugin") diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/NeedlessAnnotationDetector.java b/src/main/java/com/github/spotbugs/jspecify/nullness/NeedlessAnnotationDetector.java index 227f0fc..191ec6e 100644 --- a/src/main/java/com/github/spotbugs/jspecify/nullness/NeedlessAnnotationDetector.java +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/NeedlessAnnotationDetector.java @@ -15,58 +15,40 @@ */ package com.github.spotbugs.jspecify.nullness; +import com.github.spotbugs.jspecify.nullness.asm.*; import edu.umd.cs.findbugs.BugInstance; import edu.umd.cs.findbugs.BugReporter; import edu.umd.cs.findbugs.Priorities; import edu.umd.cs.findbugs.asm.ClassNodeDetector; import edu.umd.cs.findbugs.ba.XClass; -import edu.umd.cs.findbugs.classfile.CheckedAnalysisException; -import edu.umd.cs.findbugs.classfile.ClassDescriptor; -import edu.umd.cs.findbugs.classfile.FieldDescriptor; -import edu.umd.cs.findbugs.classfile.Global; -import edu.umd.cs.findbugs.classfile.MethodDescriptor; +import edu.umd.cs.findbugs.classfile.*; import edu.umd.cs.findbugs.classfile.engine.asm.FindBugsASM; -import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName; import java.lang.invoke.MethodHandles; import java.lang.reflect.Modifier; -import java.util.Set; +import java.util.Objects; +import org.apache.bcel.Const; import org.jspecify.nullness.NullMarked; import org.jspecify.nullness.Nullable; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.TypeReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class NeedlessAnnotationDetector extends ClassNodeDetector { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private final Set INTRINSICALLY_NOT_NULLABLE_TYPES = - Set.of( - Type.BOOLEAN_TYPE, - Type.BYTE_TYPE, - Type.CHAR_TYPE, - Type.INT_TYPE, - Type.LONG_TYPE, - Type.FLOAT_TYPE, - Type.DOUBLE_TYPE); - @Nullable private Nullness nullness; - @SlashedClassName @Nullable private ClassDescriptor classDescriptor; + @Nullable private ClassDescriptor classDescriptor; public NeedlessAnnotationDetector(BugReporter bugReporter) { super(bugReporter); } + /** Return true if given type can be null. */ private boolean canBeNull(Type type) { - final XClass clazz; - try { - clazz = Global.getAnalysisCache().getClassAnalysis(XClass.class, classDescriptor); - } catch (CheckedAnalysisException e) { - bugReporter.reportMissingClass(classDescriptor); - return false; - } - return !INTRINSICALLY_NOT_NULLABLE_TYPES.contains(type) - && !clazz.getSuperclassDescriptor().matches(Enum.class); + return type.getSort() > Type.DOUBLE; } @Override @@ -95,9 +77,48 @@ public MethodVisitor visitMethod( return new MethodVisitor( FindBugsASM.ASM_VERSION, methodDescriptor, + signature, nullness == null ? Nullness.NO_EXPLICIT_CONFIG : nullness); } + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String descriptor, boolean visible) { + TypeReference typeRefObj = new TypeReference(typeRef); + switch (typeRefObj.getSort()) { + case TypeReference.CLASS_EXTENDS: + if (Nullness.from(descriptor).isPresent() && typePath == null) { + try { + int index = typeRefObj.getSuperTypeIndex(); + ClassDescriptor annotated; + if (index == -1) { + // TODO improve bug description to tell the problem intuitive + annotated = classDescriptor.getXClass().getSuperclassDescriptor(); + log.warn( + "The superclass, {}, is wrongly annotated with a nullness annotation in the class {}", + annotated, + classDescriptor); + } else { + // TODO improve bug description to tell the problem intuitive + annotated = classDescriptor.getXClass().getInterfaceDescriptorList()[index]; + log.warn( + "One of interfaces, {}, is w with a nullness annotation in the class {}", + annotated, + classDescriptor); + } + bugReporter.reportBug( + new BugInstance( + "JSPECIFY_NULLNESS_INTRINSICALLY_NOT_NULLABLE", Priorities.HIGH_PRIORITY) + .addType(annotated) + .addClass(classDescriptor)); + } catch (CheckedAnalysisException e) { + log.error("Could not get XClass from " + classDescriptor, e); + } + } + } + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { Nullness.from(descriptor) @@ -126,15 +147,36 @@ class FieldVisitor extends org.objectweb.asm.FieldVisitor { this.fieldDescriptor = fieldDescriptor; } + /** Return true if visiting firld is an Enum field like {@code enum Foo {FOO;} } */ + private boolean isEnumField() { + if (!fieldDescriptor.isStatic()) { + return false; + } + + try { + XClass clazz = Global.getAnalysisCache().getClassAnalysis(XClass.class, classDescriptor); + if (!clazz.getSuperclassDescriptor().matches(Enum.class)) { + return false; + } + } catch (CheckedAnalysisException e) { + bugReporter.reportMissingClass(classDescriptor); + return false; + } + + return Objects.equals( + Type.getType(fieldDescriptor.getSignature()), + Type.getType(classDescriptor.getSignature())); + } + @Override public void visitEnd() { super.visitEnd(); Nullness nullnessOfReturnedValue = nullness == null ? defaultNullness : nullness; Type type = Type.getType(fieldDescriptor.getSignature()); - if (!canBeNull(type) + if ((isEnumField() || !canBeNull(type)) && nullnessOfReturnedValue.isSetExplicitly() && nullnessOfReturnedValue != Nullness.NOT_NULL) { - log.info("{} is annotated as nullable, but {} cannot be null", fieldDescriptor, type); + log.debug("{} is annotated as nullable, but {} cannot be null", fieldDescriptor, type); bugReporter.reportBug( new BugInstance( "JSPECIFY_NULLNESS_INTRINSICALLY_NOT_NULLABLE", Priorities.HIGH_PRIORITY) @@ -168,29 +210,116 @@ class MethodVisitor extends org.objectweb.asm.MethodVisitor { private final MethodDescriptor methodDescriptor; /** Default nullness in the current scope. */ private final Nullness defaultNullness; + /** The signature of this method, could be null if the method is not generic. */ + @Nullable private final String signature; /** Nullness specified by the annotation on this method's return value. */ @Nullable private Nullness nullness; - MethodVisitor(int api, MethodDescriptor methodDescriptor, Nullness defaultNullness) { + MethodVisitor( + int api, MethodDescriptor methodDescriptor, String signature, Nullness defaultNullness) { super(api); + this.signature = signature; this.defaultNullness = defaultNullness; this.methodDescriptor = methodDescriptor; } @Override public void visitParameter(String name, int access) { - System.err.printf("visitParameter: %s%s%n", methodDescriptor.getSignature(), name); + log.debug("visitParameter: signature {}, name {}", methodDescriptor.getSignature(), name); super.visitParameter(name, access); } + private Type getAnnotated(TypeReference typeRef, TypePath typePath) { + TypeArgument tempType = null; + switch (typeRef.getSort()) { + case TypeReference.METHOD_RETURN: + return Type.getReturnType(methodDescriptor.getSignature()); + + case TypeReference.METHOD_FORMAL_PARAMETER: + int typeParamIndex = typeRef.getTypeParameterIndex(); + if (typePath == null) { + // means the type of parameter is directly annotated, like `@Nullness int` + return Type.getArgumentTypes(methodDescriptor.getSignature())[typeParamIndex]; + } + log.debug("method is {}, signature is {}", methodDescriptor, signature); + ParameterType param = + ParameterTypeFinder.create(api, signature, methodDescriptor) + .getParameterType(typeParamIndex); + + int length = typePath.getLength(); + for (int i = 0; i < length; ++i) { + switch (typePath.getStep(i)) { + case TypePath.TYPE_ARGUMENT: + int index = typePath.getStepArgument(i); + tempType = param.getTypeArguments().get(index); + break; + + case TypePath.ARRAY_ELEMENT: + // means an element of array is annotated, like `@Nullness int[]` + // TODO support nested array like int[][] + if (tempType instanceof TypeArgumentWithType) { + return ((TypeArgumentWithType) tempType).getType().getElementType(); + } + + Type arrayType = + Type.getArgumentTypes(methodDescriptor.getSignature())[typeParamIndex]; + return arrayType.getElementType(); + } + } + // fall through + default: + log.debug( + ">> sort {}, TypeArgumentIndex {}, TypeParameterIndex {}, TypeParameterBoundIndex {}, typePath {}", + typeRef.getSort(), + typeRef.getTypeArgumentIndex(), + typeRef.getTypeParameterIndex(), + typeRef.getTypeParameterBoundIndex(), + typePath); + } + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation( + int typeRef, TypePath typePath, String descriptor, boolean visible) { + TypeReference typeRefObj = new TypeReference(typeRef); + if (typeRefObj.getSort() == TypeReference.METHOD_RECEIVER + || (Const.CONSTRUCTOR_NAME.equals(methodDescriptor.getName()) + && typeRefObj.getSort() == TypeReference.METHOD_RETURN)) { + // TODO make bug description intuitive + if (Nullness.from(descriptor).map(Nullness::isSetExplicitly).orElse(Boolean.FALSE)) { + bugReporter.reportBug( + new BugInstance( + "JSPECIFY_NULLNESS_INTRINSICALLY_NOT_NULLABLE", Priorities.HIGH_PRIORITY) + .addType(classDescriptor.getSignature()) + .addClassAndMethod(methodDescriptor)); + } + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + Type annotated = getAnnotated(typeRefObj, typePath); + if (annotated != null && !canBeNull(annotated) && Nullness.from(descriptor).isPresent()) { + bugReporter.reportBug( + new BugInstance( + "JSPECIFY_NULLNESS_INTRINSICALLY_NOT_NULLABLE", Priorities.HIGH_PRIORITY) + .addType(annotated.getDescriptor()) + .addClassAndMethod(methodDescriptor) + .addParameterAnnotation( + typeRefObj.getTypeParameterIndex(), "the annotated parameter")); + } + return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); + } + @Override public AnnotationVisitor visitParameterAnnotation( int parameter, String descriptor, boolean visible) { Type[] types = Type.getArgumentTypes(methodDescriptor.getSignature()); Type type = types[parameter]; - System.err.printf( - "visitParameterAnnotation: %s method parameter (%d) is type %s and annotated with %s%n", - methodDescriptor, parameter, type, descriptor); + log.debug( + "visitParameterAnnotation: {} method parameter ({}) is type {} and annotated with {}", + methodDescriptor, + parameter, + type, + descriptor); return null; } @@ -202,8 +331,8 @@ public void visitEnd() { if (!canBeNull(returnType) && nullnessOfReturnedValue.isSetExplicitly() && nullnessOfReturnedValue != Nullness.NOT_NULL) { - System.err.printf( - "%s is annotated as nullable, but %s cannot be null%n", methodDescriptor, returnType); + log.debug( + "{} is annotated as nullable, but {} cannot be null", methodDescriptor, returnType); bugReporter.reportBug( new BugInstance( "JSPECIFY_NULLNESS_INTRINSICALLY_NOT_NULLABLE", Priorities.HIGH_PRIORITY) diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/Nullness.java b/src/main/java/com/github/spotbugs/jspecify/nullness/Nullness.java index f305b68..7aaf107 100644 --- a/src/main/java/com/github/spotbugs/jspecify/nullness/Nullness.java +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/Nullness.java @@ -61,6 +61,10 @@ public static Optional from(@Nullable String descriptor) { case "org/jspecify/nullness/Nullable": case "org.jspecify.nullness.Nullable": return Optional.of(NULLABLE); + case "Lorg/jspecify/nullness/NullnessUnspecified;": + case "org/jspecify/nullness/NullnessUnspecified": + case "org.jspecify.nullness.NullnessUnspecified": + return Optional.of(UNKNOWN); default: return Optional.empty(); } diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/ReturnUnexpectedNullDetector.java b/src/main/java/com/github/spotbugs/jspecify/nullness/ReturnUnexpectedNullDetector.java index 231da40..7f8494e 100644 --- a/src/main/java/com/github/spotbugs/jspecify/nullness/ReturnUnexpectedNullDetector.java +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/ReturnUnexpectedNullDetector.java @@ -64,21 +64,21 @@ boolean isTargetMethod() { @Override public void afterOpcode(int code) { - switch (code) { - case Const.INVOKEINTERFACE: - case Const.INVOKESPECIAL: - case Const.INVOKESTATIC: - case Const.INVOKEVIRTUAL: - XMethod methodOperand = getXMethodOperand(); - NullnessDatabase database = Global.getAnalysisCache().getDatabase(NullnessDatabase.class); - Optional optional = - database.findNullnessOf(getXClassOperand(), methodOperand, Global.getAnalysisCache()); - super.afterOpcode(code); - optional.ifPresent(nullness -> stack.getStackItem(0).setUserValue(nullness)); - return; - // constructor has no returned value - default: - super.afterOpcode(code); + super.afterOpcode(code); + + if (code != Const.INVOKEINTERFACE + && code != Const.INVOKESPECIAL + && code != Const.INVOKESTATIC + && code != Const.INVOKEVIRTUAL) { + return; + } + + XMethod methodOperand = getXMethodOperand(); + NullnessDatabase database = Global.getAnalysisCache().getDatabase(NullnessDatabase.class); + Optional optional = + database.findNullnessOf(getXClassOperand(), methodOperand, Global.getAnalysisCache()); + if ((methodOperand != null && methodOperand.isReturnTypeReferenceType()) && !stack.isTop()) { + optional.ifPresent(nullness -> stack.getStackItem(0).setUserValue(nullness)); } } } diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterType.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterType.java new file mode 100644 index 0000000..88d91e7 --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterType.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.objectweb.asm.Type; + +public final class ParameterType { + private final List typeArguments; + private final Type type; + + ParameterType(Type type, List typeArguments) { + this.type = Objects.requireNonNull(type); + this.typeArguments = Collections.unmodifiableList(typeArguments); + } + + public List getTypeArguments() { + return typeArguments; + } + + public Type getType() { + return type; + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinder.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinder.java new file mode 100644 index 0000000..652bc4a --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinder.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import edu.umd.cs.findbugs.classfile.MethodDescriptor; +import java.util.Collections; +import java.util.Objects; +import org.jspecify.nullness.Nullable; +import org.objectweb.asm.Type; + +public abstract class ParameterTypeFinder { + public static ParameterTypeFinder create( + int api, @Nullable String signature, MethodDescriptor methodDescriptor) { + if (signature == null) { + return new BasedOnMethodDescriptor(methodDescriptor); + } else { + return new BasedOnSignature(new SignatureParser(api, signature)); + } + } + + public abstract ParameterType getParameterType(int argument); + + static class BasedOnSignature extends ParameterTypeFinder { + private final SignatureParser signatureParser; + + BasedOnSignature(SignatureParser signatureParser) { + this.signatureParser = Objects.requireNonNull(signatureParser); + } + + @Override + public ParameterType getParameterType(int argument) { + return signatureParser.getParameterType(argument); + } + } + + static class BasedOnMethodDescriptor extends ParameterTypeFinder { + private final Type[] argumentTypes; + + BasedOnMethodDescriptor(String signature) { + this.argumentTypes = Type.getArgumentTypes(Objects.requireNonNull(signature)); + } + + BasedOnMethodDescriptor(MethodDescriptor methodDescriptor) { + this(Objects.requireNonNull(methodDescriptor).getSignature()); + } + + @Override + public ParameterType getParameterType(int argument) { + return new ParameterType(argumentTypes[argument], Collections.emptyList()); + } + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParser.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParser.java new file mode 100644 index 0000000..0c3fa78 --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParser.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.lang.invoke.MethodHandles; +import java.util.*; +import java.util.function.Consumer; +import org.jspecify.nullness.Nullable; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A helper class that parses signature (in context of the ASM), to split them into {@link + * org.objectweb.asm.Type} instances. It is similar to the {@code + * edu.umd.cs.findbugs.ba.SignatureParser} provided by SpotBugs core, but provides more features + * especially the generics support. + * + * @see edu.umd.cs.findbugs.ba.SignatureParser + */ +class SignatureParser { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final MySignatureVisitor visitor; + + SignatureParser(int api, String signature) { + Objects.requireNonNull(signature); + log.debug("analysing signature {}", signature); + SignatureReader reader = new SignatureReader(signature); + visitor = new MySignatureVisitor(api); + reader.accept(visitor); + } + + public Optional getReturnedType() { + return Optional.ofNullable(visitor.returnType); + } + + public ParameterType getParameterType(int argument) { + assert visitor.parameterTypes.size() == visitor.typeArguments.size(); + if (0 <= argument && argument < visitor.parameterTypes.size()) { + TypeArgument typeArgument = visitor.parameterTypes.get(argument); + if (typeArgument instanceof TypeArgumentWithType) { + TypeArgumentWithType withType = (TypeArgumentWithType) typeArgument; + return new ParameterType(withType.getType(), visitor.typeArguments.get(argument)); + } else if (typeArgument instanceof TypeArgumentWithTypeVariable) { + // TODO treat type variable properly + TypeArgumentWithTypeVariable withTypeVariable = (TypeArgumentWithTypeVariable) typeArgument; + return new ParameterType( + Type.getObjectType(withTypeVariable.getTypeVariable()), + visitor.typeArguments.get(argument)); + } + } + throw new IndexOutOfBoundsException( + String.format( + "The target method has only %d arguments but requested to get %dth parameter", + visitor.parameterTypes.size(), argument)); + } + + public Optional getFormalTypeParameter() { + return Optional.ofNullable(visitor.formalTypeParameter); + } + + private static final class MySignatureVisitor extends SignatureVisitor { + @Nullable private String formalTypeParameter; + @Nullable private TypeArgument returnType; + /** + * List of name of parameter types, e.g. {@code + * "CaptureConvertedUnionNullToOtherUnionNull$Lib"}. + */ + private final List parameterTypes = new ArrayList<>(); + + private final List> typeArguments = new ArrayList<>(); + + MySignatureVisitor(int api) { + super(api); + } + + @Override + public SignatureVisitor visitReturnType() { + return new TypeFinder(api, type -> this.returnType = type); + } + + @Override + public SignatureVisitor visitParameterType() { + typeArguments.add(new ArrayList<>()); + + return new TypeFinder( + api, + new Consumer<>() { + private boolean isFirst = true; + + @Override + public void accept(TypeArgument typeArgument) { + if (isFirst) { + parameterTypes.add(typeArgument); + isFirst = false; + } else { + getLastTypeArguments().add(typeArgument); + } + } + }); + } + + private List getLastTypeArguments() { + return typeArguments.get(typeArguments.size() - 1); + } + + @Override + public void visitFormalTypeParameter(String name) { + formalTypeParameter = name; + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new TypeFinder(api, type -> getLastTypeArguments().add(type)); + } + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgument.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgument.java new file mode 100644 index 0000000..11da536 --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgument.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.util.function.Function; + +public abstract class TypeArgument { + private final char wildcard; + + TypeArgument(char wildcard) { + this.wildcard = wildcard; + } + + /** + * Returns '=' for the {@code instanceof}, '+' for {@code extends}, and '-' for {@code super} type + * argument. + * + * @see JVMS + * ยง4.7.9.1. Signatures + */ + public char getWildcard() { + return wildcard; + } + + abstract TypeArgument replaceWildcard(char wildcard); + + abstract TypeArgument replaceType(Function replace); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TypeArgument that = (TypeArgument) o; + + return wildcard == that.wildcard; + } + + @Override + public int hashCode() { + return wildcard; + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithType.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithType.java new file mode 100644 index 0000000..a614f3c --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithType.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.util.Objects; +import java.util.function.Function; +import org.objectweb.asm.Type; + +public final class TypeArgumentWithType extends TypeArgument { + private final Type type; + + public TypeArgumentWithType(char wildcard, String name) { + super(wildcard); + this.type = Type.getType(Objects.requireNonNull(name)); + } + + TypeArgumentWithType(char wildcard, Type type) { + super(wildcard); + this.type = type; + } + + public Type getType() { + return type; + } + + @Override + TypeArgument replaceWildcard(char wildcard) { + return new TypeArgumentWithType(wildcard, type); + } + + @Override + TypeArgument replaceType(Function replace) { + Type newType = Type.getType(replace.apply(type.getDescriptor())); + return new TypeArgumentWithType(getWildcard(), newType); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TypeArgumentWithType that = (TypeArgumentWithType) o; + + return Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return type != null ? type.hashCode() : 0; + } + + @Override + public String toString() { + return "TypeArgumentWithType{" + "wildcard=" + getWildcard() + ", type=" + type + '}'; + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithTypeVariable.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithTypeVariable.java new file mode 100644 index 0000000..288bd2e --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeArgumentWithTypeVariable.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.util.Objects; +import java.util.function.Function; + +public final class TypeArgumentWithTypeVariable extends TypeArgument { + private final String typeVariable; + + TypeArgumentWithTypeVariable(char wildcard, String typeVariable) { + super(wildcard); + this.typeVariable = Objects.requireNonNull(typeVariable); + } + + public String getTypeVariable() { + return typeVariable; + } + + @Override + TypeArgument replaceWildcard(char wildcard) { + return new TypeArgumentWithTypeVariable(wildcard, typeVariable); + } + + @Override + TypeArgument replaceType(Function replace) { + return new TypeArgumentWithTypeVariable(getWildcard(), replace.apply(typeVariable)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + TypeArgumentWithTypeVariable that = (TypeArgumentWithTypeVariable) o; + + return Objects.equals(typeVariable, that.typeVariable); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (typeVariable != null ? typeVariable.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "TypeArgumentWithTypeVariable{" + + "wildcard=" + + getWildcard() + + ", typeVariable='" + + typeVariable + + '\'' + + '}'; + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeFinder.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeFinder.java new file mode 100644 index 0000000..7826953 --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/TypeFinder.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import java.util.Objects; +import java.util.function.Consumer; +import org.jspecify.nullness.NullMarked; +import org.jspecify.nullness.Nullable; +import org.objectweb.asm.signature.SignatureVisitor; + +/** + * A helper class that find type in signature of returned type, parameter type, etc. It is designed + * to use as returned value of methods in {@link SignatureVisitor}. + */ +@NullMarked +final class TypeFinder extends SignatureVisitor { + private final Consumer<@Nullable TypeArgument> callback; + + TypeFinder(int api, Consumer<@Nullable TypeArgument> callback) { + super(api); + this.callback = Objects.requireNonNull(callback); + } + + @Override + public void visitBaseType(char descriptor) { + callback.accept(new TypeArgumentWithType('?', Character.toString(descriptor))); + } + + @Override + public void visitInnerClassType(String name) { + String innerName = "L" + name + ";"; + callback.accept(new TypeArgumentWithType('?', innerName)); + } + + @Override + public void visitClassType(String name) { + String innerName = "L" + name + ";"; + callback.accept(new TypeArgumentWithType('?', innerName)); + } + + @Override + public void visitTypeVariable(String name) { + callback.accept(new TypeArgumentWithTypeVariable('?', name)); + } + + @Override + public SignatureVisitor visitArrayType() { + return new TypeFinder(api, type -> callback.accept(type.replaceType(t -> "[" + t))); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new TypeFinder( + api, typeArgument -> callback.accept(typeArgument.replaceWildcard(wildcard))); + } + + @Override + public void visitTypeArgument() { + // TODO treat type argument with a wildcard in a special way? + callback.accept(new TypeArgumentWithType('*', "Ljava/lang/Object;")); + } +} diff --git a/src/main/java/com/github/spotbugs/jspecify/nullness/asm/package-info.java b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/package-info.java new file mode 100644 index 0000000..f740d3b --- /dev/null +++ b/src/main/java/com/github/spotbugs/jspecify/nullness/asm/package-info.java @@ -0,0 +1,8 @@ +/** + * A package serving helper classes that helps to integrate ASM code written in event-oriented style + * with logics written in OOP. + */ +@NullMarked +package com.github.spotbugs.jspecify.nullness.asm; + +import org.jspecify.nullness.NullMarked; diff --git a/src/main/resources/messages.xml b/src/main/resources/messages.xml index 7b8a6ea..4abbc2c 100644 --- a/src/main/resources/messages.xml +++ b/src/main/resources/messages.xml @@ -26,7 +26,7 @@
Nullness annotation used for types that cannot be null.

+

Nullness annotation used for a type ({0}) that cannot be null.

]]>
diff --git a/src/test/java/com/github/spotbugs/jspecify/TestWithSample.java b/src/test/java/com/github/spotbugs/jspecify/TestWithSample.java index efc81f0..c4670cf 100644 --- a/src/test/java/com/github/spotbugs/jspecify/TestWithSample.java +++ b/src/test/java/com/github/spotbugs/jspecify/TestWithSample.java @@ -53,7 +53,20 @@ class TestWithSample { .toAbsolutePath() .normalize(), Paths.get( - System.getenv("user.home"), + System.getProperty("user.home"), + ".gradle", + "caches", + "modules-2", + "files-2.1", + "com.google.code.findbugs", + "jsr305", + "3.0.2", + "25ea2e8b0c338a877313bd4672d3fe056ea78f0d", + "jsr305-3.0.2.jar") + .toAbsolutePath() + .normalize(), + Paths.get( + System.getProperty("user.home"), ".gradle", "caches", "modules-2", @@ -74,7 +87,7 @@ private static File[] listSamples() { return Arrays.stream(samples.toFile().listFiles()) .filter(File::isFile) .filter(file -> file.getName().endsWith(".java")) - .toArray(size -> new File[size]); + .toArray(File[]::new); } @ParameterizedTest(name = "Compile and run analysis on {0}") @@ -105,7 +118,8 @@ private void compile( File javaFile, JavaCompiler compiler, StandardJavaFileManager javaFileManager) { Iterable javaFileObjects = javaFileManager.getJavaFileObjects(javaFile); - String classpath = JAR.stream().map(Path::toString).collect(Collectors.joining(",")); + String classpath = + JAR.stream().map(Path::toString).collect(Collectors.joining(File.pathSeparator)); Boolean success = compiler .getTask( @@ -116,6 +130,8 @@ private void compile( null, javaFileObjects) .call(); - Assertions.assertTrue(success); + if (!success) { + Assertions.fail("Compilation failed unexpectedly"); + } } } diff --git a/src/test/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinderTest.java b/src/test/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinderTest.java new file mode 100644 index 0000000..f21308c --- /dev/null +++ b/src/test/java/com/github/spotbugs/jspecify/nullness/asm/ParameterTypeFinderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import edu.umd.cs.findbugs.classfile.engine.asm.FindBugsASM; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ParameterTypeFinderTest { + @Test + void complexCase1() { + String signature = + "(LCaptureConvertedTypeVariableBounded$ImplicitlyObjectBounded.Inner<+Ljava/lang/Object;>;)Ljava/lang/Object;"; + ParameterTypeFinder.BasedOnSignature finder = + new ParameterTypeFinder.BasedOnSignature( + new SignatureParser(FindBugsASM.ASM_VERSION, signature)); + Assertions.assertEquals( + new TypeArgumentWithType('+', "Ljava/lang/Object;"), + finder.getParameterType(0).getTypeArguments().get(0)); + } +} diff --git a/src/test/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParserTest.java b/src/test/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParserTest.java new file mode 100644 index 0000000..64fbb9c --- /dev/null +++ b/src/test/java/com/github/spotbugs/jspecify/nullness/asm/SignatureParserTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021-2022 The SpotBugs team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.spotbugs.jspecify.nullness.asm; + +import edu.umd.cs.findbugs.classfile.engine.asm.FindBugsASM; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; + +class SignatureParserTest { + @Nested + static class ParseReturnValueTest { + @Test + void voidMethod() { + SignatureParser parser = new SignatureParser(FindBugsASM.ASM_VERSION, "()V"); + Assertions.assertEquals( + Optional.of(new TypeArgumentWithType('?', Type.VOID_TYPE)), parser.getReturnedType()); + } + + @Test + void primitiveMethod() { + SignatureParser parser = new SignatureParser(FindBugsASM.ASM_VERSION, "()I"); + Assertions.assertEquals( + Optional.of(new TypeArgumentWithType('?', Type.INT_TYPE)), parser.getReturnedType()); + } + } + + @Nested + static class ParseParameterTypeTest { + @Test + void noParameterType() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(Ljava/lang/Object;)V"); + Assertions.assertTrue(parser.getParameterType(0).getTypeArguments().isEmpty()); + } + + @Test + void singleParameterType() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(Ljava/util/Set;)V"); + TypeArgumentWithType typeArgument = + (TypeArgumentWithType) parser.getParameterType(0).getTypeArguments().get(0); + Assertions.assertEquals(Type.getType("Ljava/lang/Object;"), typeArgument.getType()); + Assertions.assertEquals('=', typeArgument.getWildcard()); + } + + @Test + void doubleParameterType() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(Ljava/util/Set;)V"); + List typeArguments = parser.getParameterType(0).getTypeArguments(); + Assertions.assertEquals(2, typeArguments.size()); + Assertions.assertEquals( + "T", ((TypeArgumentWithTypeVariable) typeArguments.get(0)).getTypeVariable()); + } + + @Test + void singleParameterTypeWithWildcard() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(LAnnotatedWildcard<*>;)V"); + Assertions.assertEquals( + Type.getType("LAnnotatedWildcard;"), parser.getParameterType(0).getType()); + + TypeArgumentWithType typeArgument = + (TypeArgumentWithType) parser.getParameterType(0).getTypeArguments().get(0); + Assertions.assertEquals('*', typeArgument.getWildcard()); + } + + @Test + void singleParameterTypeWithExtends() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(Ljava/util/Set<+Ljava/lang/Object;>;)V"); + TypeArgumentWithType typeArgument = + (TypeArgumentWithType) parser.getParameterType(0).getTypeArguments().get(0); + Assertions.assertEquals(Type.getType("Ljava/lang/Object;"), typeArgument.getType()); + Assertions.assertEquals('+', typeArgument.getWildcard()); + } + + @Test + void singleParameterTypeWithSuper() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(Ljava/util/Set<-Ljava/lang/Object;>;)V"); + TypeArgumentWithType typeArgument = + (TypeArgumentWithType) parser.getParameterType(0).getTypeArguments().get(0); + Assertions.assertEquals(Type.getType("Ljava/lang/Object;"), typeArgument.getType()); + Assertions.assertEquals('-', typeArgument.getWildcard()); + } + } + + @Test + void complexCase1() { + SignatureParser parser = + new SignatureParser( + FindBugsASM.ASM_VERSION, + "(LMultiplePathsToTypeVariable$TBounded;)Ljava/lang/Object;"); + List typeArguments = parser.getParameterType(0).getTypeArguments(); + Assertions.assertEquals(2, typeArguments.size()); + Assertions.assertEquals( + "T", ((TypeArgumentWithTypeVariable) typeArguments.get(0)).getTypeVariable()); + Assertions.assertEquals('=', typeArguments.get(0).getWildcard()); + Assertions.assertEquals( + "T", ((TypeArgumentWithTypeVariable) typeArguments.get(0)).getTypeVariable()); + Assertions.assertEquals('+', typeArguments.get(1).getWildcard()); + } + + @Test + void complexCase2() { + SignatureParser parser = + new SignatureParser( + FindBugsASM.ASM_VERSION, + "(LNotNullMarkedAnnotatedInnerOfParameterized.Nested;LNotNullMarkedAnnotatedInnerOfParameterized<*>.Nested;LNotNullMarkedAnnotatedInnerOfParameterized<*>.Nested;LNotNullMarkedAnnotatedInnerOfParameterized<*>.Nested.DoublyNested;LNotNullMarkedAnnotatedInnerOfParameterized<*>.Nested.DoublyNested;LNotNullMarkedAnnotatedInnerOfParameterized<*>.Nested.DoublyNested;LNotNullMarkedAnnotatedInnerOfParameterized$Lib.Nested.DoublyNested;>;LNotNullMarkedAnnotatedInnerOfParameterized$Lib.Nested.DoublyNested;>;LNotNullMarkedAnnotatedInnerOfParameterized$Lib.Nested.DoublyNested;>;)V"); + } + + @Test + void complexCase3() { + SignatureParser parser = + new SignatureParser(FindBugsASM.ASM_VERSION, "(LArraySameType$Lib<[Ljava/lang/Object;>;)V"); + TypeArgumentWithType typeArgument = + (TypeArgumentWithType) parser.getParameterType(0).getTypeArguments().get(0); + Assertions.assertEquals( + Type.getType("Ljava/lang/Object;"), typeArgument.getType().getElementType()); + } +} diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..2115a2f --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.execution.parallel.enabled = true