diff --git a/java/java.hints/external/binaries-list b/java/java.hints/external/binaries-list new file mode 100644 index 000000000000..284f64e34250 --- /dev/null +++ b/java/java.hints/external/binaries-list @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +7425A601C1C7EC76645A78D22B8C6A627EDEE507 org.jspecify:jspecify:1.0.0 diff --git a/java/java.hints/nbproject/project.properties b/java/java.hints/nbproject/project.properties index df8658d15cbc..3f7b60149aa2 100644 --- a/java/java.hints/nbproject/project.properties +++ b/java/java.hints/nbproject/project.properties @@ -17,7 +17,7 @@ spec.version.base=1.116.0 -javac.release=17 +javac.release=21 nbroot=../.. jbrowse.external=${nbroot}/retouche @@ -59,7 +59,7 @@ requires.nb.javac=true test.runner=junit -test-unit-sys-prop.hints-tools.jar.location=${tools.jar} +test-unit-sys-prop.hints-jspecify.jar.location=${basedir}/external/jspecify-1.0.0.jar test.config.batch1.includes=\ **/*Test.class diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Bundle.properties b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Bundle.properties index eb396dcc3aca..a7f94dd7b0c3 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Bundle.properties +++ b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/Bundle.properties @@ -133,6 +133,7 @@ ERR_AssigningNullToNotNull=Assigning null to not-null variable ERR_PossibleAssigingNullToNotNull=Assigning possible null to not-null variable ERR_NULL_TO_NON_NULL_ARG=Passing null to not-null argument ERR_POSSIBLENULL_TO_NON_NULL_ARG=Passing possible null to not-null argument +ERR_TYPES_MISMATCH=Nullness states mismatch ERR_NotNullWouldBeNPE=Unnecessary test for null - a NullPointerException would already be thrown ERR_NotNull=Unnecessary test for null - the expression is never null ERR_ReturningNullFromNonNull=Returning null value from a method whose return type is not-null diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/bugs/NPECheck.java b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/NPECheck.java index 3beeb9bedd5a..0469d613d9e9 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/bugs/NPECheck.java +++ b/java/java.hints/src/org/netbeans/modules/java/hints/bugs/NPECheck.java @@ -26,24 +26,27 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; @@ -53,11 +56,11 @@ import org.netbeans.spi.editor.hints.ErrorDescription; import org.openide.util.NbBundle; -import static org.netbeans.modules.java.hints.bugs.NPECheck.State.*; import org.netbeans.modules.java.hints.errors.Utilities; import org.netbeans.spi.java.hints.*; import org.netbeans.spi.java.hints.Hint.Options; import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; /** * @@ -76,7 +79,9 @@ public class NPECheck { static final boolean DEF_UNBOXING_UNKNOWN_VALUES = true; @BooleanOption(displayName = "#LBL_NPECheck.UNBOXING_UNKNOWN_VALUES", tooltip = "#TP_NPECheck.UNBOXING_UNKNOWN_VALUES", defaultValue=DEF_UNBOXING_UNKNOWN_VALUES) static final String KEY_UNBOXING_UNKNOWN_VALUES = "unboxing-unknown"; // NOI18N - + + private static final State DEFAULT_STATE = new State(StateEnum.POSSIBLE_NULL); + @TriggerPatterns({ @TriggerPattern("$mods$ $type $var = $expr;"), @TriggerPattern("$var = $expr") @@ -89,18 +94,18 @@ public static ErrorDescription assignment(HintContext ctx) { } TreePath expr = ctx.getVariables().get("$expr"); - State r = computeExpressionsState(ctx).get(expr.getLeaf()); + StateEnum r = computeExpressionsState(ctx).getOrDefault(expr.getLeaf(), DEFAULT_STATE).thisTypeState; State elementState = getStateFromAnnotations(ctx.getInfo(), e); if (elementState != null && elementState.isNotNull()) { String key = null; - if (r == NULL || r == NULL_HYPOTHETICAL) { + if (r == StateEnum.NULL) { key = "ERR_AssigningNullToNotNull"; } - if (r == POSSIBLE_NULL_REPORT) { + if (r == StateEnum.POSSIBLE_NULL_REPORT) { key = "ERR_PossibleAssigingNullToNotNull"; } @@ -111,8 +116,7 @@ public static ErrorDescription assignment(HintContext ctx) { return null; } - - + @TriggerPatterns({ @TriggerPattern(value = "$expr ? $trueExpr : $falseExpr", constraints = { @ConstraintVariableType(variable = "$trueExpr", type = "double"), @@ -194,10 +198,10 @@ public static ErrorDescription unboxingConditional(HintContext ctx) { assert npPath != null; Map expressionsState = computeExpressionsState(ctx); - State s = expressionsState.get(npPath.getLeaf()); + StateEnum s = expressionsState.getOrDefault(npPath.getLeaf(), DEFAULT_STATE).thisTypeState; String k; - if (s == null || s == POSSIBLE_NULL) { + if (s == null || s == StateEnum.POSSIBLE_NULL || s == StateEnum.POSSIBLE_NULL_EXPLICIT_UNSPECIFIED) { boolean report = ctx.getPreferences().getBoolean(KEY_UNBOXING_UNKNOWN_VALUES, DEF_UNBOXING_UNKNOWN_VALUES); if (!report) { return null; @@ -205,18 +209,13 @@ public static ErrorDescription unboxingConditional(HintContext ctx) { k = "ERR_UnboxingPotentialNullValue"; // NOI18N } else switch (s) { case NULL: - case NULL_HYPOTHETICAL: k = "ERR_UnboxingNullValue"; // NOI18N break; case POSSIBLE_NULL_REPORT: - case POSSIBLE_NULL: - case INSTANCE_OF_FALSE: k = "ERR_UnboxingPotentialNullValue"; // NOI18N break; case NOT_NULL_BE_NPE: case NOT_NULL: - case NOT_NULL_HYPOTHETICAL: - case INSTANCE_OF_TRUE: return null; default: throw new AssertionError(s.name()); @@ -263,14 +262,14 @@ public static ErrorDescription switchExpression(HintContext ctx) { return null; } - State r = computeExpressionsState(ctx).get(select.getLeaf()); - if (r == NULL || r == NULL_HYPOTHETICAL) { + StateEnum r = computeExpressionsState(ctx).getOrDefault(select.getLeaf(), DEFAULT_STATE).thisTypeState; + if (r == StateEnum.NULL) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_DereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); } - if (r == State.POSSIBLE_NULL_REPORT || r == INSTANCE_OF_FALSE) { + if (r == StateEnum.POSSIBLE_NULL_REPORT) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_PossiblyDereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); @@ -284,14 +283,14 @@ public static ErrorDescription enhancedFor(HintContext ctx) { if (colExpr == null) { return null; } - State r = computeExpressionsState(ctx).get(colExpr.getLeaf()); - if (r == NULL || r == NULL_HYPOTHETICAL) { + StateEnum r = computeExpressionsState(ctx).getOrDefault(colExpr.getLeaf(), DEFAULT_STATE).thisTypeState; + if (r == StateEnum.NULL) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_DereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); } - if (r == State.POSSIBLE_NULL_REPORT || r == State.INSTANCE_OF_FALSE) { + if (r == StateEnum.POSSIBLE_NULL_REPORT) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_PossiblyDereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); @@ -302,15 +301,15 @@ public static ErrorDescription enhancedFor(HintContext ctx) { @TriggerPattern("$select.$variable") public static ErrorDescription memberSelect(HintContext ctx) { TreePath select = ctx.getVariables().get("$select"); - State r = computeExpressionsState(ctx).get(select.getLeaf()); + StateEnum r = computeExpressionsState(ctx).getOrDefault(select.getLeaf(), DEFAULT_STATE).thisTypeState; - if (r == NULL || r == NULL_HYPOTHETICAL) { + if (r == StateEnum.NULL) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_DereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); } - if (r == State.POSSIBLE_NULL_REPORT || r == State.INSTANCE_OF_FALSE) { + if (r == StateEnum.POSSIBLE_NULL_REPORT) { String displayName = NbBundle.getMessage(NPECheck.class, "ERR_PossiblyDereferencingNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); @@ -325,7 +324,7 @@ public static ErrorDescription memberSelect(HintContext ctx) { * */ public static boolean isSafeToDereference(CompilationInfo info, TreePath path) { - State r = computeExpressionsState(info, null).get(path.getLeaf()); + StateEnum r = computeExpressionsState(info, null).getOrDefault(path.getLeaf(), DEFAULT_STATE).thisTypeState; // copied from warning issued on redundant != null. return r != null && r.isNotNull(); } @@ -337,8 +336,8 @@ public static List methodInvocation(HintContext ctx) { Map expressionsState = computeExpressionsState(ctx); for (Tree param : mit.getArguments()) { - State r = expressionsState.get(param); - paramStates.add(r != null ? r : State.POSSIBLE_NULL); + State r = expressionsState.getOrDefault(param, DEFAULT_STATE); + paramStates.add(r); } Element e = ctx.getInfo().getTrees().getElement(ctx.getPath()); @@ -353,15 +352,18 @@ public static List methodInvocation(HintContext ctx) { List params = ee.getParameters(); for (VariableElement param : params) { - if (getStateFromAnnotations(ctx.getInfo(), param) == NOT_NULL && (!ee.isVarArgs() || param != params.get(params.size() - 1))) { - switch (paramStates.get(index)) { - case NULL: case NULL_HYPOTHETICAL: + State declaredState = getStateFromAnnotations(ctx.getInfo(), param); + if (!ee.isVarArgs() || param != params.get(params.size() - 1)) { + switch (statesMatch(declaredState, paramStates.get(index))) { + case TOP_LEVEL_NULL_TO_NONNULL: result.add(ErrorDescriptionFactory.forTree(ctx, mit.getArguments().get(index), NbBundle.getMessage(NPECheck.class, "ERR_NULL_TO_NON_NULL_ARG"))); break; - case POSSIBLE_NULL_REPORT: - case INSTANCE_OF_FALSE: + case TOP_LEVEL_POSSIBLE_NULL_TO_NONNULL: result.add(ErrorDescriptionFactory.forTree(ctx, mit.getArguments().get(index), NbBundle.getMessage(NPECheck.class, "ERR_POSSIBLENULL_TO_NON_NULL_ARG"))); break; + case MISMATCH: + result.add(ErrorDescriptionFactory.forTree(ctx, mit.getArguments().get(index), NbBundle.getMessage(NPECheck.class, "ERR_TYPES_MISMATCH"))); + break; } } index++; @@ -376,10 +378,10 @@ public static List methodInvocation(HintContext ctx) { }) public static ErrorDescription notNullTest(HintContext ctx) { TreePath variable = ctx.getVariables().get("$variable"); - State r = computeExpressionsState(ctx).get(variable.getLeaf()); + StateEnum r = computeExpressionsState(ctx).getOrDefault(variable.getLeaf(), DEFAULT_STATE).thisTypeState; if (r != null && r.isNotNull() && !ignore(ctx, false)) { - String displayName = NbBundle.getMessage(NPECheck.class, r == State.NOT_NULL_BE_NPE ? "ERR_NotNullWouldBeNPE" : "ERR_NotNull"); + String displayName = NbBundle.getMessage(NPECheck.class, r == StateEnum.NOT_NULL_BE_NPE ? "ERR_NotNullWouldBeNPE" : "ERR_NotNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); } @@ -393,10 +395,10 @@ public static ErrorDescription notNullTest(HintContext ctx) { }) public static ErrorDescription nullTest(HintContext ctx) { TreePath variable = ctx.getVariables().get("$variable"); - State r = computeExpressionsState(ctx).get(variable.getLeaf()); + StateEnum r = computeExpressionsState(ctx).getOrDefault(variable.getLeaf(), DEFAULT_STATE).thisTypeState; if (r != null && r.isNotNull() && !ignore(ctx, true)) { - String displayName = NbBundle.getMessage(NPECheck.class, r == State.NOT_NULL_BE_NPE ? "ERR_NotNullWouldBeNPE" : "ERR_NotNull"); + String displayName = NbBundle.getMessage(NPECheck.class, r == StateEnum.NOT_NULL_BE_NPE ? "ERR_NotNullWouldBeNPE" : "ERR_NotNull"); return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), displayName); } @@ -457,7 +459,7 @@ private static boolean hasNull(HintContext ctx, BinaryTree bt) { @TriggerPattern("return $expression;") public static ErrorDescription returnNull(HintContext ctx) { TreePath expression = ctx.getVariables().get("$expression"); - State returnState = computeExpressionsState(ctx).get(expression.getLeaf()); + StateEnum returnState = computeExpressionsState(ctx).getOrDefault(expression.getLeaf(), DEFAULT_STATE).thisTypeState; if (returnState == null) return null; @@ -484,11 +486,10 @@ public static ErrorDescription returnNull(HintContext ctx) { String key = null; switch (returnState) { - case NULL: case NULL_HYPOTHETICAL: + case NULL: if (expected.isNotNull()) key = "ERR_ReturningNullFromNonNull"; break; case POSSIBLE_NULL_REPORT: - case INSTANCE_OF_FALSE: if (expected.isNotNull()) key = "ERR_ReturningPossibleNullFromNonNull"; break; } @@ -501,6 +502,31 @@ public static ErrorDescription returnNull(HintContext ctx) { return null; } + @TriggerPattern("synchronized ($expression) { $statements$; }") + @Messages({ + "ERR_SynchronizingOnNull=Synchronizing on null", + "ERR_SynchronizingOnPossibleNull=Synchronizing on possible null", + }) + public static ErrorDescription synchronizedNull(HintContext ctx) { + TreePath expression = ctx.getVariables().get("$expression"); + StateEnum expressionState = computeExpressionsState(ctx).getOrDefault(expression.getLeaf(), DEFAULT_STATE).thisTypeState; + + if (expressionState == null) return null; + + String message = switch (expressionState) { + case NULL -> Bundle.ERR_SynchronizingOnNull(); + case POSSIBLE_NULL_REPORT -> + Bundle.ERR_SynchronizingOnPossibleNull(); + default -> null; + }; + + if (message != null) { + return ErrorDescriptionFactory.forName(ctx, expression, message); + } + + return null; + } + private static final Object KEY_EXPRESSION_STATE = new Object(); private static final Object KEY_CONDITIONAL_PARAMETER = new Object(); @@ -535,39 +561,181 @@ private static Map computeExpressionsState(HintContext ctx) { ctx.getInfo().putCachedValue(KEY_EXPRESSION_STATE, result, CompilationInfo.CacheClearPolicy.ON_TASK_END); return result; } - - private static State getStateFromAnnotations(CompilationInfo info, Element e) { - return getStateFromAnnotations(info, e, State.POSSIBLE_NULL); + + private static StateMatchResult statesMatch(State declaredState, State actualState) { + return statesMatch(declaredState, actualState, true); + } + + private static StateMatchResult statesMatch(State declaredState, State actualState, boolean topLevel) { + if (topLevel) { + if (declaredState.isNotNull()) { + switch (actualState.thisTypeState != null ? actualState.thisTypeState : StateEnum.POSSIBLE_NULL) { + case NULL: return StateMatchResult.TOP_LEVEL_NULL_TO_NONNULL; + case POSSIBLE_NULL_REPORT: return StateMatchResult.TOP_LEVEL_POSSIBLE_NULL_TO_NONNULL; + } + } + } else { + if (isSubstantial(declaredState) && isSubstantial(actualState)) { + if (declaredState.isNotNull() ^ actualState.isNotNull()) { + return StateMatchResult.MISMATCH; + } + } + } + + if (declaredState.typeParameters != null && + actualState.typeParameters != null && + declaredState.typeParameters.size() == actualState.typeParameters.size()) { + for (int i = 0; i < declaredState.typeParameters.size(); i++) { + if (statesMatch(declaredState.typeParameters.get(i), actualState.typeParameters.get(i), false) != StateMatchResult.MATCHES) { + return StateMatchResult.MISMATCH; + } + } + } + + if (declaredState.componentTypeState != null && + actualState.componentTypeState != null) { + if (statesMatch(declaredState.componentTypeState, actualState.componentTypeState, false) != StateMatchResult.MATCHES) { + return StateMatchResult.MISMATCH; + } + } + + return StateMatchResult.MATCHES; + } + + private static boolean isSubstantial(State s) { + return s != null && s.thisTypeState != null && + s.thisTypeState != StateEnum.POSSIBLE_NULL && + s.thisTypeState != StateEnum.POSSIBLE_NULL_EXPLICIT_UNSPECIFIED; + } + + private enum StateMatchResult { + MATCHES, + TOP_LEVEL_NULL_TO_NONNULL, + TOP_LEVEL_POSSIBLE_NULL_TO_NONNULL, + MISMATCH } private static final AnnotationMirrorGetter OVERRIDE_ANNOTATIONS = Lookup.getDefault().lookup(AnnotationMirrorGetter.class); - - private static State getStateFromAnnotations(CompilationInfo info, Element e, State def) { - if (e == null) return def; - + private static final Set LOCAL_VARIABLES = EnumSet.of(ElementKind.BINDING_VARIABLE, ElementKind.EXCEPTION_PARAMETER, ElementKind.LOCAL_VARIABLE, ElementKind.RESOURCE_VARIABLE); + + private static State getStateFromAnnotations(CompilationInfo info, Element e) { + if (e == null) return new State(StateEnum.POSSIBLE_NULL); + + StateEnum typeDefault = getDefaultState(e, StateEnum.POSSIBLE_NULL); + StateEnum declarationDefault = StateEnum.POSSIBLE_NULL; Iterable mirrors = OVERRIDE_ANNOTATIONS != null ? OVERRIDE_ANNOTATIONS.getAnnotationMirrors(info, e) : null; - + if (mirrors == null) mirrors = e.getAnnotationMirrors(); - + + State result; + + if (e.getKind().isVariable()) { + //XXX: + //- should include with OVERRIDE_ANNOTATIONS? + //- adjust default(!) + result = getStateFromAnnotations(info, e.asType(), x -> null, + LOCAL_VARIABLES.contains(e.getKind()) ? StateEnum.POSSIBLE_NULL : typeDefault, typeDefault); + } else if (e.getKind() == ElementKind.METHOD) { + result = getStateFromAnnotations(info, ((ExecutableType) e.asType()).getReturnType(), typeDefault); + } else { + result = new State(StateEnum.POSSIBLE_NULL); + } + + StateEnum fromDeclaration = getStateFromAnnotations(mirrors, declarationDefault); + + if (fromDeclaration != StateEnum.POSSIBLE_NULL) { + //TODO: correct? + result = result.setThisState(fromDeclaration); + } + + return result; + } + + private static State getStateFromAnnotations(CompilationInfo info, TypeMirror type, StateEnum fallbackState) { + return getStateFromAnnotations(info, type, ta -> null, fallbackState); + } + + private static State getStateFromAnnotations(CompilationInfo info, TypeMirror type, Function type2StateMapper, StateEnum fallbackState) { + return getStateFromAnnotations(info, type, type2StateMapper, fallbackState, fallbackState); + } + + private static State getStateFromAnnotations(CompilationInfo info, TypeMirror type, Function type2StateMapper, StateEnum topLevelFallbackState, StateEnum fallbackState) { + State state = type2StateMapper.apply(type); + + if (state != null) { + //TODO: should presumably merge with other aspects? + return state; + } + + StateEnum thisTypeState = getStateFromAnnotations(type.getAnnotationMirrors(), topLevelFallbackState); + List typeParameters = null; + State arrayComponentState = null; + + if (type.getKind() == TypeKind.DECLARED) { + DeclaredType dt = (DeclaredType) type; + + typeParameters = dt.getTypeArguments() + .stream() + .map(ta -> getStateFromAnnotations(info, ta, type2StateMapper, fallbackState)) + .toList(); + } else if (type.getKind() == TypeKind.ARRAY) { + ArrayType at = (ArrayType) type; + + arrayComponentState = getStateFromAnnotations(info, at.getComponentType(), type2StateMapper, fallbackState); + } + + return new State(thisTypeState, typeParameters, arrayComponentState); + } + + private static StateEnum getStateFromAnnotations(Iterable mirrors, StateEnum fallbackState) { for (AnnotationMirror am : mirrors) { String simpleName = ((TypeElement) am.getAnnotationType().asElement()).getSimpleName().toString(); if ("Nullable".equals(simpleName) || "NullAllowed".equals(simpleName)) { - return State.POSSIBLE_NULL_REPORT; + return StateEnum.POSSIBLE_NULL_REPORT; } if ("CheckForNull".equals(simpleName)) { - return State.POSSIBLE_NULL_REPORT; + return StateEnum.POSSIBLE_NULL_REPORT; } if ("NotNull".equals(simpleName) || "NonNull".equals(simpleName) || "Nonnull".equals(simpleName)) { - return State.NOT_NULL; + return StateEnum.NOT_NULL; + } + + String fqnName = ((TypeElement) am.getAnnotationType().asElement()).getQualifiedName().toString(); + + if ("org.jspecify.annotations.NullnessUnspecified".equals(fqnName)) { + return StateEnum.POSSIBLE_NULL_EXPLICIT_UNSPECIFIED; + } + } + return fallbackState; + } + + private static StateEnum getDefaultState(Element el, StateEnum fallbackState) { + while (el != null) { + for (AnnotationMirror am : el.getAnnotationMirrors()) { + String fqnName = ((TypeElement) am.getAnnotationType().asElement()).getQualifiedName().toString(); + + if ("org.jspecify.annotations.NullMarked".equals(fqnName)) { + return StateEnum.NOT_NULL; + } + + if ("org.jspecify.annotations.NullUnmarked".equals(fqnName)) { + return StateEnum.POSSIBLE_NULL_REPORT; + } + + if ("org.jspecify.annotations.NullnessUnspecified".equals(fqnName)) { + return StateEnum.POSSIBLE_NULL_EXPLICIT_UNSPECIFIED; + } } + + el = el.getEnclosingElement(); } - return def; + return fallbackState; } - + public interface AnnotationMirrorGetter { public Iterable getAnnotationMirrors(CompilationInfo info, Element el); } @@ -579,24 +747,19 @@ private static final class VisitorImpl extends CancellableTreeScanner variable2State = new HashMap<>(); - /** - * Finalized state of variables. Records for variables, which go out of scope is collected here. - */ - private final Map variable2StateFinal = new HashMap<>(); - + private Map variable2StateWhenTrue = new HashMap<>(); + private Map variable2StateWhenFalse = new HashMap<>(); + private final Map>> resumeBefore = new IdentityHashMap<>(); private final Map>> resumeAfter = new IdentityHashMap<>(); private Map> resumeOnExceptionHandler = new IdentityHashMap<>(); - private final Map expressionState = new IdentityHashMap<>(); + private final Map expressionState = new IdentityHashMap<>(); //the null state of the top-level type of the expression private final List pendingFinally = new LinkedList<>(); private List pendingYields = new ArrayList<>(); - private boolean not; - private boolean doNotRecord; private final TypeElement throwableEl; private final TypeMirror runtimeExceptionType; private final TypeMirror errorType; @@ -685,10 +848,10 @@ public State scan(Tree tree, Void p) { TypeMirror currentType = tree != null ? info.getTrees().getTypeMirror(new TreePath(getCurrentPath(), tree)) : null; if ((tree != null && tree.getKind() == Kind.LAMBDA_EXPRESSION) || (currentType != null && currentType.getKind().isPrimitive())) { - r = State.NOT_NULL; + r = new State(StateEnum.NOT_NULL); } - if (r != null && !doNotRecord) { + if (r != null) { // expressionState.put(tree, r); expressionState.put(tree, mergeIn(expressionState, tree, r)); } @@ -697,12 +860,6 @@ public State scan(Tree tree, Void p) { Collection varsOutScope = scopedVariables.get(tree); if (varsOutScope != null) { - for (VariableElement ve : varsOutScope) { - State s = variable2State.get(ve); - if (s != null) { - variable2StateFinal.put(ve, s); - } - } variable2State.keySet().removeAll(varsOutScope); } return r; @@ -721,13 +878,17 @@ private void resume(Tree tree, Map> @Override public State visitAssignment(AssignmentTree node, Void p) { Element e = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getVariable())); - Map orig = new HashMap<>(variable2State); State r = scan(node.getExpression(), p); + mergeSplitVariable2State(); + scan(node.getVariable(), p); - mergeHypotheticalVariable2State(orig); - + TypeMirror variableType = info.getTrees().getTypeMirror(new TreePath(getCurrentPath(), node.getVariable())); + TypeMirror expressionType = info.getTrees().getTypeMirror(new TreePath(getCurrentPath(), node.getExpression())); + + r = aliasToTargetType(expressionType, variableType, r); + if (isVariableElement(e)) { variable2State.put((VariableElement) e, r); } @@ -737,13 +898,12 @@ public State visitAssignment(AssignmentTree node, Void p) { @Override public State visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - Map orig = new HashMap<>(variable2State); - scan(node.getExpression(), p); + + mergeSplitVariable2State(); + scan(node.getVariable(), p); - - mergeHypotheticalVariable2State(orig); - + return null; } @@ -759,14 +919,22 @@ private void addScopedVariable(Tree t, VariableElement ve) { @Override public State visitVariable(VariableTree node, Void p) { Element e = info.getTrees().getElement(getCurrentPath()); - Map orig = new HashMap<>(variable2State); State r = scan(node.getInitializer(), p); - mergeHypotheticalVariable2State(orig); - + mergeSplitVariable2State(); + + if (node.getInitializer() != null) { + TypeMirror targetType = info.getTrees().getTypeMirror(new TreePath(getCurrentPath(), node.getType())); + TypeMirror initType = info.getTrees().getTypeMirror(new TreePath(getCurrentPath(), node.getInitializer())); + + r = aliasToTargetType(initType, targetType, r); + } else { + r = getStateFromAnnotations(info, e); + } + if (e != null) { if (e.getKind() == ElementKind.EXCEPTION_PARAMETER) { - r = NOT_NULL; + r = new State(StateEnum.NOT_NULL); } variable2State.put((VariableElement) e, r); TreePath pp = getCurrentPath().getParentPath(); @@ -774,60 +942,75 @@ public State visitVariable(VariableTree node, Void p) { addScopedVariable(pp.getLeaf(), (VariableElement)e); } } - + return r; } @Override public State visitMemberSelect(MemberSelectTree node, Void p) { - State expr = scan(node.getExpression(), p); - boolean wasNPE = false; - - if (expr == State.NULL || expr == State.NULL_HYPOTHETICAL || expr == State.POSSIBLE_NULL || expr == State.POSSIBLE_NULL_REPORT || expr == State.INSTANCE_OF_FALSE) { - wasNPE = true; - } - - Element site = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getExpression())); - - if (isVariableElement(site) && wasNPE && (variable2State.get((VariableElement) site) == null || !variable2State.get((VariableElement) site).isNotNull())) { - variable2State.put((VariableElement) site, NOT_NULL_BE_NPE); - } + State derefedState = scan(node.getExpression(), p); + TreePath derefed = new TreePath(getCurrentPath(), node.getExpression()); + + handleDereference(derefedState, derefed); + + Element site = info.getTrees().getElement(derefed); + // special case: if the memberSelect selects enum field = constant, it is never null. if (site != null && site.getKind() == ElementKind.ENUM) { Element enumConst = info.getTrees().getElement(getCurrentPath()); if (enumConst != null && enumConst.getKind() == ElementKind.ENUM_CONSTANT) { - return State.NOT_NULL; + return new State(StateEnum.NOT_NULL); } } return getStateFromAnnotations(info, info.getTrees().getElement(getCurrentPath())); } + private void handleDereference(State derefState, TreePath derefed) { + boolean wasNPE = false; + + if (derefState != null && !derefState.isNotNull()) { + wasNPE = true; + } + + Element site = info.getTrees().getElement(derefed); + + if (isVariableElement(site) && wasNPE) { + if (variable2State != null) { + if (!isDefinitellyNotNull(variable2State, (VariableElement) site)) { + setThisState(variable2State, (VariableElement) site, StateEnum.NOT_NULL_BE_NPE); + } + } else { + if (!isDefinitellyNotNull(variable2StateWhenTrue, (VariableElement) site)) { + setThisState(variable2StateWhenTrue, (VariableElement) site, StateEnum.NOT_NULL_BE_NPE); + } + if (!isDefinitellyNotNull(variable2StateWhenFalse, (VariableElement) site)) { + setThisState(variable2StateWhenFalse, (VariableElement) site, StateEnum.NOT_NULL_BE_NPE); + } + } + } + } + @Override public State visitLiteral(LiteralTree node, Void p) { if (node.getValue() == null) { - return State.NULL; + return new State(StateEnum.NULL); } else { - return State.NOT_NULL; + return new State(StateEnum.NOT_NULL); } } @Override public State visitIf(IfTree node, Void p) { - Map oldVariable2StateBeforeCondition = new HashMap<>(variable2State); - - State condition = scan(node.getCondition(), p); - + scan(node.getCondition(), p); + + Map elseVariable2State = selectVariableStates(true); + scan(node.getThenStatement(), null); Map variableStatesAfterThen = new HashMap<>(variable2State); - variable2State = new HashMap<>(oldVariable2StateBeforeCondition); - not = true; - doNotRecord = true; - scan(node.getCondition(), p); - not = false; - doNotRecord = false; + variable2State = elseVariable2State; scan(node.getElseStatement(), null); @@ -849,101 +1032,97 @@ public State visitIf(IfTree node, Void p) { public State visitBinary(BinaryTree node, Void p) { Kind kind = node.getKind(); - if (not) { - switch (kind) { - case CONDITIONAL_AND: kind = Kind.CONDITIONAL_OR; break; - case CONDITIONAL_OR: kind = Kind.CONDITIONAL_AND; break; - case EQUAL_TO: kind = Kind.NOT_EQUAL_TO; break; - case NOT_EQUAL_TO: kind = Kind.EQUAL_TO; break; - } - } - State left = null; State right = null; switch (kind) { case CONDITIONAL_AND: + scan(node.getLeftOperand(), p); + + Map afterLeftWhenFalse = selectVariableStates(true); + + scan(node.getRightOperand(), p); + + ensureStateSplit(); + + mergeInto(variable2StateWhenFalse, afterLeftWhenFalse); + + break; + case AND: case OR: case XOR: scan(node.getLeftOperand(), p); scan(node.getRightOperand(), p); break; case CONDITIONAL_OR: { - HashMap orig = new HashMap<>(variable2State); - scan(node.getLeftOperand(), p); - Map afterLeft = variable2State; - - variable2State = orig; + Map afterLeftWhenTrue = selectVariableStates(false); - boolean oldNot = not; - boolean oldDoNotRecord = doNotRecord; + scan(node.getRightOperand(), p); - not ^= true; - doNotRecord = true; - scan(node.getLeftOperand(), p); - not = oldNot; - doNotRecord = oldDoNotRecord; + ensureStateSplit(); - scan(node.getRightOperand(), p); + mergeInto(variable2StateWhenTrue, afterLeftWhenTrue); - mergeIntoVariable2State(afterLeft); break; } default: { - boolean oldNot = not; - not = false; left = scan(node.getLeftOperand(), p); right = scan(node.getRightOperand(), p); - not = oldNot; } } if (kind == Kind.EQUAL_TO) { - if (right == State.NULL) { - Element e = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getLeftOperand())); - - if (isVariableElement(e) && !hasDefiniteValue((VariableElement) e)) { - variable2State.put((VariableElement) e, State.NULL_HYPOTHETICAL); - - return null; - } + if (node.getRightOperand().getKind() == Kind.NULL_LITERAL) { + handleBinaryComparisonToNull(node.getLeftOperand(), StateEnum.NULL); + } else if (node.getLeftOperand().getKind() == Kind.NULL_LITERAL) { + handleBinaryComparisonToNull(node.getRightOperand(), StateEnum.NULL); } - if (left == State.NULL) { - Element e = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getRightOperand())); - - if (isVariableElement(e) && !hasDefiniteValue((VariableElement) e)) { - variable2State.put((VariableElement) e, State.NULL_HYPOTHETICAL); - - return null; - } + } else if (kind == Kind.NOT_EQUAL_TO) { + if (node.getRightOperand().getKind() == Kind.NULL_LITERAL) { + handleBinaryComparisonToNull(node.getLeftOperand(), StateEnum.NOT_NULL); + } else if (node.getLeftOperand().getKind() == Kind.NULL_LITERAL) { + handleBinaryComparisonToNull(node.getRightOperand(), StateEnum.NOT_NULL); } } - if (kind == Kind.NOT_EQUAL_TO) { - if (right == State.NULL) { - Element e = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getLeftOperand())); - - if (isVariableElement(e) && !hasDefiniteValue((VariableElement) e)) { - variable2State.put((VariableElement) e, State.NOT_NULL_HYPOTHETICAL); - - return null; - } + return null; + } + + private void handleBinaryComparisonToNull(ExpressionTree variableOperand, StateEnum trueState) { + Set variables = new HashSet<>(); + List expressionTodo = new ArrayList<>(); + + expressionTodo.add(new TreePath(getCurrentPath(), variableOperand)); + + while (!expressionTodo.isEmpty()) { + TreePath tp = expressionTodo.remove(expressionTodo.size() - 1); + Element e = info.getTrees().getElement(tp); + + if (isVariableElement(e)) { + variables.add((VariableElement) e); } - if (left == State.NULL) { - Element e = info.getTrees().getElement(new TreePath(getCurrentPath(), node.getRightOperand())); - - if (isVariableElement(e) && !hasDefiniteValue((VariableElement) e)) { - variable2State.put((VariableElement) e, State.NOT_NULL_HYPOTHETICAL); - - return null; + switch (tp.getLeaf().getKind()) { + case PARENTHESIZED -> expressionTodo.add(new TreePath(tp, ((ParenthesizedTree) tp.getLeaf()).getExpression())); + case ASSIGNMENT -> { + AssignmentTree at = (AssignmentTree) tp.getLeaf(); + + expressionTodo.add(new TreePath(tp, at.getVariable())); + expressionTodo.add(new TreePath(tp, at.getExpression())); } } } - - return null; + + for (VariableElement var : variables) { + if (!isDefinitellyNotNull(var)) { + ensureStateSplit(); + + setThisState(variable2StateWhenTrue, var, trueState); + setThisState(variable2StateWhenFalse, var, trueState.reverse()); + } + } } @Override @@ -961,7 +1140,9 @@ public State visitInstanceOf(InstanceOfTree node, Void p) { setState = !variable2State.get((VariableElement) e).isNotNull(); } if (setState) { - variable2State.put((VariableElement) e, not ? State.INSTANCE_OF_FALSE : State.INSTANCE_OF_TRUE); + ensureStateSplit(); + + setThisState(variable2StateWhenTrue, (VariableElement) e, StateEnum.NOT_NULL); } } @@ -970,27 +1151,23 @@ public State visitInstanceOf(InstanceOfTree node, Void p) { @Override public State visitConditionalExpression(ConditionalExpressionTree node, Void p) { - //TODO: handle the condition similarly to visitIf - Map oldVariable2State = new HashMap<>(variable2State); - scan(node.getCondition(), p); + Map elseVariable2State = selectVariableStates(true); + State thenSection = scan(node.getTrueExpression(), p); - + + mergeSplitVariable2State(); + Map variableStatesAfterThen = variable2State; - variable2State = oldVariable2State; - - not = true; - doNotRecord = true; - scan(node.getCondition(), p); - not = false; - doNotRecord = false; + variable2State = elseVariable2State; State elseSection = scan(node.getFalseExpression(), p); + + State result = State.strictMerge(thenSection, elseSection); - State result = State.collect(thenSection, elseSection); - + mergeSplitVariable2State(); mergeIntoVariable2State(variableStatesAfterThen); return result; @@ -999,14 +1176,12 @@ public State visitConditionalExpression(ConditionalExpressionTree node, Void p) @Override public State visitNewClass(NewClassTree node, Void p) { scan(node.getEnclosingExpression(), p); - scan(node.getIdentifier(), p); + State typeState = scan(node.getIdentifier(), p); scan(node.getTypeArguments(), p); for (Tree param : node.getArguments()) { - Map origVariable2State = variable2State; - variable2State = new HashMap<>(variable2State); scan(param, p); - mergeNonHypotheticalVariable2State(origVariable2State); + mergeSplitVariable2State(); } scan(node.getClassBody(), p); @@ -1016,26 +1191,58 @@ public State visitNewClass(NewClassTree node, Void p) { if (invoked != null && invoked.getKind() == ElementKind.CONSTRUCTOR) { recordResumeOnExceptionHandler((ExecutableElement) invoked); } - - return State.NOT_NULL; + + if (typeState != null) { + return typeState.setThisState(StateEnum.NOT_NULL); + } else { + return new State(StateEnum.NOT_NULL); + } } @Override public State visitMethodInvocation(MethodInvocationTree node, Void p) { scan(node.getTypeArguments(), p); - scan(node.getMethodSelect(), p); + + State receiverState; + TypeMirror receiverType; + ExpressionTree methodSelect = node.getMethodSelect(); + + switch (methodSelect.getKind()) { + case IDENTIFIER -> { + receiverState = new State(StateEnum.POSSIBLE_NULL); //TODO - should be "this" + receiverType = null; //TODO - should be "this" + } + case MEMBER_SELECT -> { + TreePath prevPath = this.currentPath; + try { + this.currentPath = new TreePath(getCurrentPath(), methodSelect); + ExpressionTree selected = ((MemberSelectTree) methodSelect).getExpression(); + TreePath selectedPath = new TreePath(currentPath, selected); + + receiverState = scan(selected, p); + handleDereference(receiverState, selectedPath); + receiverType = info.getTrees().getTypeMirror(selectedPath); + } finally { + this.currentPath = prevPath; + } + } + default -> { + //XXX: should not happen? + receiverState = new State(StateEnum.POSSIBLE_NULL); + receiverType = null; + scan(methodSelect, p); + } + } for (Tree param : node.getArguments()) { - Map origVariable2State = variable2State; - variable2State = new HashMap<>(variable2State); scan(param, p); - mergeNonHypotheticalVariable2State(origVariable2State); + mergeSplitVariable2State(); } Element e = info.getTrees().getElement(getCurrentPath()); if (e == null || e.getKind() != ElementKind.METHOD) { - return State.POSSIBLE_NULL; + return new State(StateEnum.POSSIBLE_NULL); } else { recordResumeOnExceptionHandler((ExecutableElement) e); visitAssertMethods(node, e); @@ -1043,6 +1250,25 @@ public State visitMethodInvocation(MethodInvocationTree node, Void p) { if (s != null) { return s; } + if (receiverType != null) { //ideally should be always true + if (receiverType.getKind() == TypeKind.DECLARED) { + DeclaredType receiver = (DeclaredType) receiverType; + Map marker2State = new HashMap<>(); + List ta = receiver.getTypeArguments(); + + if (receiverState.typeParameters != null && ta.size() == receiverState.typeParameters.size()) { + for (int i = 0; i < ta.size(); i++) { + marker2State.put(ta.get(i), receiverState.typeParameters.get(i)); + } + TypeMirror instantiatedReturnType = ((ExecutableType) info.getTypes().asMemberOf(receiver, e)).getReturnType(); + State instantiatedState = getStateFromAnnotations(info, instantiatedReturnType, marker2State::get, StateEnum.POSSIBLE_NULL); + TypeMirror declaredReturnType = ((ExecutableElement) e).getReturnType(); + State declaredState = getStateFromAnnotations(info, declaredReturnType, null); + + return State.weakMerge(instantiatedState, declaredState); + } + } + } } return getStateFromAnnotations(info, e); @@ -1059,7 +1285,7 @@ private State visitPrimitiveWrapperMethods(MethodInvocationTree node, Element e) case "toHexString":case "toOctalString": case "toBinaryString": // NOI18N case "valueOf":// NOI18N case "decode":// NOI18N - return NOT_NULL; + return new State(StateEnum.NOT_NULL); case "getLong": // NOI18N case "getShort": // NOI18N @@ -1078,9 +1304,9 @@ private State visitPrimitiveWrapperMethods(MethodInvocationTree node, Element e) return null; } if (m.getKind().isPrimitive()) { - return NOT_NULL; + return new State(StateEnum.NOT_NULL); } else if (NPECheck.isSafeToDereference(ctx.getInfo(), parPath)) { - return NOT_NULL; + return new State(StateEnum.NOT_NULL); } else { return null; } @@ -1094,14 +1320,14 @@ private void visitAssertMethods(MethodInvocationTree node, Element e) { if (!node.getArguments().isEmpty()) { String ownerFQN = ((TypeElement) e.getEnclosingElement()).getQualifiedName().toString(); Tree argument = null; - State targetState = null; + StateEnum targetState = null; switch (e.getSimpleName().toString()) { case "assertNotNull": case "requireNonNull": case "requireNonNullElse": - case "requireNonNullElseGet": targetState = State.NOT_NULL; break; - case "assertNull": targetState = State.NULL; break; + case "requireNonNullElseGet": targetState = StateEnum.NOT_NULL; break; + case "assertNull": targetState = StateEnum.NULL; break; } switch (ownerFQN) { @@ -1115,7 +1341,7 @@ private void visitAssertMethods(MethodInvocationTree node, Element e) { Element param = argument != null && targetState != null ? info.getTrees().getElement(new TreePath(getCurrentPath(), argument)) : null; if (param != null && isVariableElement(param)) { - variable2State.put((VariableElement) param, targetState); + setThisState(variable2State, (VariableElement) param, targetState); } } } @@ -1127,19 +1353,53 @@ public State visitIdentifier(IdentifierTree node, Void p) { Element e = info.getTrees().getElement(getCurrentPath()); if (e == null || !isVariableElement(e)) { - return State.POSSIBLE_NULL; + return new State(StateEnum.POSSIBLE_NULL); } if (e.getKind() == ElementKind.ENUM_CONSTANT) { // enum constants are never null - return State.NOT_NULL; + return new State(StateEnum.NOT_NULL); } - State s = variable2State.get((VariableElement) e); - if (s != null) { - return s; + if (variable2State != null) { + State s = variable2State.get((VariableElement) e); + if (s != null) { + return s; + } + + return getStateFromAnnotations(info, e); + } else { + State whenTrue = variable2StateWhenTrue.get((VariableElement) e); + State whenFalse = variable2StateWhenFalse.get((VariableElement) e); + + if (whenTrue == null) { + whenTrue = getStateFromAnnotations(info, e); + } + + if (whenFalse == null) { + whenFalse = getStateFromAnnotations(info, e); + } + + return State.strictMerge(whenTrue, whenFalse); } - return getStateFromAnnotations(info, e); + } + + @Override + public State visitParameterizedType(ParameterizedTypeTree node, Void p) { + State baseState = scan(node.getType(), p); + List taStates = node.getTypeArguments().stream().map(ta -> scan(ta, p)).toList(); + + return new State(baseState.thisTypeState, taStates); + } + + @Override + public State visitAnnotatedType(AnnotatedTypeTree node, Void p) { + super.visitAnnotatedType(node, p); + + //TODO: merge with underlying state? + TypeMirror type = info.getTrees().getTypeMirror(getCurrentPath()); + + return getStateFromAnnotations(info, type, StateEnum.POSSIBLE_NULL); } @Override @@ -1149,19 +1409,42 @@ public State visitWhileLoop(WhileLoopTree node, Void p) { @Override public State visitDoWhileLoop(DoWhileLoopTree node, Void p) { - return handleGeneralizedFor(Collections.singletonList(node.getStatement()), node.getCondition(), null, node.getStatement(), p); + if (!inCycle) { + inCycle = true; + + HashMap startState = new HashMap<>(variable2State); + + scan(node.getStatement(), p); + + scan(node.getCondition(), p); + + selectVariableStates(true); + + mergeIntoVariable2State(startState); + + inCycle = false; + } + + scan(node.getStatement(), p); + + scan(node.getCondition(), p); + + selectVariableStates(false); + + return null; } @Override public State visitUnary(UnaryTree node, Void p) { - boolean oldNot = not; - - not ^= node.getKind() == Kind.LOGICAL_COMPLEMENT; - State res = scan(node.getExpression(), p); - not = oldNot; - + if (variable2StateWhenFalse != null) { + Map temp = variable2StateWhenFalse; + + variable2StateWhenFalse = variable2StateWhenTrue; + variable2StateWhenTrue = temp; + } + return res; } @@ -1173,7 +1456,6 @@ public State visitMethod(MethodTree node, Void p) { try { variable2State = new HashMap<>(); - not = false; Element current = info.getTrees().getElement(getCurrentPath()); @@ -1212,60 +1494,32 @@ public State visitEnhancedForLoop(EnhancedForLoopTree node, Void p) { */ private boolean inCycle = false; - private Map findStateDifference(Map basepoint) { - Map m = new HashMap<>(); - for (Map.Entry vEntry : variable2State.entrySet()) { - VariableElement k = vEntry.getKey(); - State s = basepoint.get(k); - if (s != vEntry.getValue()) { - m.put(k, vEntry.getValue()); - } - } - return m; - } - private State handleGeneralizedFor(Iterable initializer, Tree condition, Iterable update, Tree statement, Void p) { scan(initializer, p); - - Map oldVariable2State = new HashMap<>(variable2State); - boolean oldNot = not; - boolean oldDoNotRecord = doNotRecord; - - not = true; - doNotRecord = true; - - scan(condition, p); - - not = oldNot; - - Map negConditionVariable2State = new HashMap<>(variable2State); - // get just the _changed_ stuff - Map negConditionChanges2State = findStateDifference(oldVariable2State); - - - doNotRecord = oldDoNotRecord; - if (!inCycle) { inCycle = true; - variable2State = new HashMap<>(oldVariable2State); - + scan(condition, p); + + Map negConditionVariable2State = selectVariableStates(true); + scan(statement, p); scan(update, p); - - mergeIntoVariable2State(oldVariable2State); + + mergeIntoVariable2State(negConditionVariable2State); + inCycle = false; - } else { - variable2State = oldVariable2State; } - + scan(condition, p); + + Map negConditionVariable2State = selectVariableStates(true); + scan(statement, p); scan(update, p); mergeIntoVariable2State(negConditionVariable2State); - forceIntoVariable2State(negConditionChanges2State); return null; } @@ -1273,15 +1527,18 @@ private State handleGeneralizedFor(Iterable initializer, Tree co @Override public State visitAssert(AssertTree node, Void p) { scan(node.getCondition(), p); - //XXX: todo clear hypothetical, evaluate negation? + selectVariableStates(true); scan(node.getDetail(), p); return null; } @Override public State visitArrayAccess(ArrayAccessTree node, Void p) { - super.visitArrayAccess(node, p); - return State.POSSIBLE_NULL; + State exprState = scan(node.getExpression(), p); + scan(node.getIndex(), p); + + return exprState != null && exprState.componentTypeState != null ? exprState.componentTypeState + : new State(StateEnum.POSSIBLE_NULL); } @Override @@ -1298,11 +1555,11 @@ public State visitSwitchExpression(SwitchExpressionTree node, Void p) { handleGeneralizedSwitch(node, node.getExpression(), node.getCases()); if (pendingYields.isEmpty()) { //should not happen (for valid source) - return State.POSSIBLE_NULL; + return new State(StateEnum.POSSIBLE_NULL); } State result = pendingYields.get(0); for (State s : pendingYields.subList(1, pendingYields.size())) { - result = State.collect(result, s); + result = State.strictMerge(result, s); } return result; } finally { @@ -1315,7 +1572,7 @@ private void handleGeneralizedSwitch(Tree switchTree, ExpressionTree expression, Element selectorElement = info.getTrees().getElement(new TreePath(getCurrentPath(), expression)); VariableElement selectorVariable = - isVariableElement(selectorElement) && !hasDefiniteValue((VariableElement) selectorElement) ? (VariableElement) selectorElement + isVariableElement(selectorElement) && !isDefinitellyNotNull((VariableElement) selectorElement) ? (VariableElement) selectorElement : null; Map origVariable2State = new HashMap<>(variable2State); @@ -1339,7 +1596,7 @@ private void handleGeneralizedSwitch(Tree switchTree, ExpressionTree expression, } if (selectorVariable != null) { - variable2State.put(selectorVariable, hasNull ? State.NULL_HYPOTHETICAL : State.NOT_NULL_HYPOTHETICAL); + setThisState(variable2State, selectorVariable, hasNull ? StateEnum.NULL : StateEnum.NOT_NULL); } State caseResult = scan(ct, null); @@ -1573,15 +1830,6 @@ private static void recordResume(Map(state)); } - private void forceIntoVariable2State(Map other) { - Map target = variable2State; - for (Entry e : other.entrySet()) { - State t = e.getValue(); - - target.put(e.getKey(), t); - } - } - private void mergeIntoVariable2State(Map other) { mergeInto(variable2State, other); } @@ -1594,93 +1842,266 @@ private void mergeInto(Map target, Map State mergeIn(Map m, K k, State nue) { if (m.containsKey(k)) { - State prev = (State)m.get(k); - return State.collect(nue, prev); + return State.strictMerge(nue, m.get(k)); } else { return nue; } } - private void mergeHypotheticalVariable2State(Map original) { - for (Entry e : variable2State.entrySet()) { - State t = e.getValue(); - - if (t == State.NULL_HYPOTHETICAL || t == State.NOT_NULL_HYPOTHETICAL) { - State originalValue = original.get(e.getKey()); - e.setValue(originalValue == State.POSSIBLE_NULL || originalValue == null ? State.POSSIBLE_NULL_REPORT : originalValue); - } + private void setThisState(Map target, VariableElement forElement, StateEnum newThisState) { + State existing = target.get(forElement); + if (existing != null) { + target.put(forElement, existing.setThisState(newThisState)); + } else { + target.put(forElement, new State(newThisState)); } } - - private void mergeNonHypotheticalVariable2State(Map original) { - Map backup = variable2State; - - variable2State = original; - - for (Entry e : backup.entrySet()) { - State t = e.getValue(); - - if (t != null && t != State.NOT_NULL_HYPOTHETICAL && t != NULL_HYPOTHETICAL && t != INSTANCE_OF_TRUE && t != INSTANCE_OF_FALSE) { - variable2State.put(e.getKey(), t); - } + + private void mergeSplitVariable2State() { + if (variable2State != null) { + return ; } + + variable2State = new HashMap<>(); + for (Entry e : variable2StateWhenTrue.entrySet()) { + State trueState = e.getValue(); + State falseState = variable2StateWhenFalse.get(e.getKey()); + + variable2State.put(e.getKey(), State.strictMerge(trueState, falseState)); + } + + variable2StateWhenTrue = null; + variable2StateWhenFalse = null; } - - private boolean hasDefiniteValue(VariableElement el) { - State s = variable2State.get(el); - + + private void ensureStateSplit() { + if (variable2State == null) { + return ; + } + variable2StateWhenTrue = new HashMap<>(variable2State); + variable2StateWhenFalse = new HashMap<>(variable2State); + variable2State = null; + } + + /** + * Fill in {@code variable2State} from {@code variable2StateWhenTrue} (iff {@code whenTrue == true}), + * or {@code variable2StateWhenFalse} (iff {@code whenTrue == false}), + * and return the other map. + */ + private Map selectVariableStates(boolean whenTrue) { + ensureStateSplit(); + + variable2State = whenTrue ? variable2StateWhenTrue : variable2StateWhenFalse; + + Map result = whenTrue ? variable2StateWhenFalse : variable2StateWhenTrue; + + variable2StateWhenTrue = null; + variable2StateWhenFalse = null; + + return result; + } + + private boolean isDefinitellyNotNull(VariableElement el) { + return variable2State != null ? isDefinitellyNotNull(variable2State, el) + : isDefinitellyNotNull(variable2StateWhenTrue, el) && isDefinitellyNotNull(variable2StateWhenFalse, el); + } + + private boolean isDefinitellyNotNull(Map in, VariableElement el) { + State s = in.get(el); + return s != null && s.isNotNull(); } private boolean isVariableElement(Element ve) { return NPECheck.isVariableElement(ctx, ve); } - + + private State aliasToTargetType(TypeMirror sourceType, TypeMirror targetType, State state) { + if (sourceType == null || sourceType.getKind() != TypeKind.DECLARED || + targetType == null || targetType.getKind() != TypeKind.DECLARED) { + return state; + } + DeclaredType source = (DeclaredType) sourceType; + DeclaredType target = (DeclaredType) targetType; + + if (state.typeParameters == null || source.getTypeArguments().size() != state.typeParameters.size()) { + //anything to reconcile the situation? + return state; + } + + Map marker2State = new HashMap<>(); + List ta = source.getTypeArguments(); + + for (int i = 0; i < ta.size(); i++) { + marker2State.put(ta.get(i), state.typeParameters.get(i)); + } + + //TODO: is this a reasonable reliable way to do the remapping? + TypeMirror remappedTargetType = info.getTypeUtilities().asSuper(source, (TypeElement) target.asElement()); + + if (remappedTargetType == null || remappedTargetType.getKind() != TypeKind.DECLARED) { + return state; + } + + DeclaredType remappedTarget = (DeclaredType) remappedTargetType; + + if (remappedTarget.getTypeArguments().size() != target.getTypeArguments().size()) { + return state; + } + + List typeParams = new ArrayList<>(); + + for (int i = 0; i < remappedTarget.getTypeArguments().size(); i++) { + State nested = marker2State.get(remappedTarget.getTypeArguments().get(i)); + + if (nested == null) { + nested = new State(StateEnum.POSSIBLE_NULL); + } else { + nested = aliasToTargetType(remappedTarget.getTypeArguments().get(i), target.getTypeArguments().get(i), nested); + } + + typeParams.add(nested); + } + + return new State(state.thisTypeState, typeParams); + } } - - static enum State { + + static class State { + + public static State strictMerge(State s1, State s2) { + return new State(StateEnum.strictCollect(s1 != null ? s1.thisTypeState : StateEnum.POSSIBLE_NULL, + s2 != null ? s2.thisTypeState : StateEnum.POSSIBLE_NULL), + mergeTypeParams(s1 != null ? s1.typeParameters : null, + s2 != null ? s2.typeParameters : null, + true), + (s1 != null && s1.componentTypeState != null) || + (s2 != null && s2.componentTypeState != null) ? + strictMerge(s1 != null ? s1.componentTypeState : null, + s2 != null ? s2.componentTypeState : null) : null); + } + + public static State weakMerge(State s1, State s2) { + return new State(StateEnum.weakCollect(s1 != null ? s1.thisTypeState : null, + s2 != null ? s2.thisTypeState : null), + mergeTypeParams(s1 != null ? s1.typeParameters : null, + s2 != null ? s2.typeParameters : null, + false), + (s1 != null && s1.componentTypeState != null) || + (s2 != null && s2.componentTypeState != null) ? + weakMerge(s1 != null ? s1.componentTypeState : null, + s2 != null ? s2.componentTypeState : null) : null); + } + + private static List mergeTypeParams(List typeParams1, List typeParams2, boolean strict) { + if (typeParams1 == null) { + return typeParams2; + } else if (typeParams2 == null) { + return typeParams1; + } else if (typeParams1.size() == typeParams2.size()) { + List typeParams = new ArrayList<>(); + for (int i = 0; i < typeParams1.size(); i++) { + typeParams.add(strict ? strictMerge(typeParams1.get(i), typeParams2.get(i)) + : weakMerge(typeParams1.get(i), typeParams2.get(i))); + } + return typeParams; + } else { + //TODO: anything can be done here? + return null; + } + } + + private final StateEnum thisTypeState; + private final List typeParameters; + private final State componentTypeState; + + public State(StateEnum thisTypeState) { + this(thisTypeState, null, null); + } + + public State(StateEnum thisTypeState, List typeParameters) { + this(thisTypeState, typeParameters, null); + } + + public State(StateEnum thisTypeState, State componentTypeState) { + this(thisTypeState, null, componentTypeState); + } + + private State(StateEnum thisTypeState, List typeParameters, State componentTypeState) { + if (typeParameters != null && componentTypeState != null) { + throw new IllegalStateException("Cannot have both type parameters and component type set."); + } + + this.thisTypeState = thisTypeState; + this.typeParameters = typeParameters; + this.componentTypeState = componentTypeState; + } + + public boolean isNotNull() { + return thisTypeState.isNotNull(); + } + + public State setThisState(StateEnum newThisState) { + return new State(newThisState, typeParameters); + } + + @Override + public String toString() { + if (typeParameters != null && !typeParameters.isEmpty()) { + return thisTypeState + "<" + typeParameters.stream().map(Object::toString).collect(Collectors.joining(", ")) + ">"; + } + + if (componentTypeState != null) { + return thisTypeState + "[" + componentTypeState + "]"; + } + + return thisTypeState.toString(); + } + } + + static enum StateEnum { NULL, - NULL_HYPOTHETICAL, POSSIBLE_NULL, + POSSIBLE_NULL_EXPLICIT_UNSPECIFIED, //mostly to support/help with JSpecify's "NullnessUnspecified" POSSIBLE_NULL_REPORT, - INSTANCE_OF_FALSE, NOT_NULL, - NOT_NULL_HYPOTHETICAL, - INSTANCE_OF_TRUE, NOT_NULL_BE_NPE; - public @CheckForNull State reverse() { + public @CheckForNull StateEnum reverse() { switch (this) { case NULL: return NOT_NULL; - case NULL_HYPOTHETICAL: - return NOT_NULL_HYPOTHETICAL; - case INSTANCE_OF_FALSE: - return INSTANCE_OF_TRUE; case POSSIBLE_NULL: + case POSSIBLE_NULL_EXPLICIT_UNSPECIFIED: case POSSIBLE_NULL_REPORT: return this; case NOT_NULL: case NOT_NULL_BE_NPE: return NULL; - case NOT_NULL_HYPOTHETICAL: - return NULL_HYPOTHETICAL; - case INSTANCE_OF_TRUE: - return INSTANCE_OF_FALSE; default: throw new IllegalStateException(); } } public boolean isNotNull() { - return this == NOT_NULL || this == NOT_NULL_BE_NPE || this == NOT_NULL_HYPOTHETICAL || this == INSTANCE_OF_TRUE; + return this == NOT_NULL || this == NOT_NULL_BE_NPE; } - - public static State collect(State s1, State s2) { + + public boolean isPossibleNulLReport() { + return this == POSSIBLE_NULL_REPORT; + } + + public static StateEnum weakCollect(StateEnum s1, StateEnum s2) { + if (s1 == null) return s2; + if (s2 == null) return s1; + return strictCollect(s1, s2); + } + + public static StateEnum strictCollect(StateEnum s1, StateEnum s2) { if (s1 == s2) return s1; - if (s1 == NULL || s2 == NULL || s1 == NULL_HYPOTHETICAL || s2 == NULL_HYPOTHETICAL) return POSSIBLE_NULL_REPORT; - if (s1 == POSSIBLE_NULL_REPORT || s2 == POSSIBLE_NULL_REPORT || s2 == INSTANCE_OF_FALSE) return POSSIBLE_NULL_REPORT; + if (s1 == NULL || s2 == NULL) return POSSIBLE_NULL_REPORT; + if (s1 == POSSIBLE_NULL_REPORT || s2 == POSSIBLE_NULL_REPORT) return POSSIBLE_NULL_REPORT; if (s1 != null && s2 != null && s1.isNotNull() && s2.isNotNull()) return NOT_NULL; return POSSIBLE_NULL; diff --git a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/NPECheckTest.java b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/NPECheckTest.java index f8edbeaff21a..8b7b110f664f 100644 --- a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/NPECheckTest.java +++ b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/bugs/NPECheckTest.java @@ -19,6 +19,7 @@ package org.netbeans.modules.java.hints.bugs; +import java.io.File; import org.netbeans.junit.NbTestCase; import org.netbeans.modules.java.hints.test.api.HintTest; import org.openide.filesystems.FileUtil; @@ -806,6 +807,21 @@ public void test222871() throws Exception { .assertWarnings(); } + public void test222871NewClass() throws Exception { + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " private void t(@CheckForNull String onSuccess, @CheckForNull Integer onError) {\n" + + " new Test(onSuccess != null || onError != null);\n" + + " new Test(onSuccess == null || onError == null);\n" + + " }\n" + + " public Test(boolean b) {}\n" + + " @interface CheckForNull {}\n" + + "}") + .run(NPECheck.class) + .assertWarnings(); + } + public void testWhileInitializeWithField() throws Exception { HintTest.create() .input("package test;\n" + @@ -1676,6 +1692,19 @@ public void testNETBEANS407a() throws Exception { .assertWarnings("4:41-4:50:verifier:ERR_NotNull"); } + public void testNETBEANS407a2() throws Exception { + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " public void test(Object o) {\n" + + " boolean b1 = o instanceof Integer;\n" + + " System.out.println(o.toString());\n" + + " }\n" + + "}") + .run(NPECheck.class) + .assertWarnings(); + } + public void testNETBEANS407b() throws Exception { HintTest.create() .input("package test;\n" + @@ -1685,7 +1714,10 @@ public void testNETBEANS407b() throws Exception { " }\n" + "}") .run(NPECheck.class) - .assertWarnings("3:45-3:53:verifier:Possibly Dereferencing null"); + .assertWarnings(); + //originally, there was this warning: + //3:45-3:53:verifier:Possibly Dereferencing null + //but the warning is very hard to defend, if the instanceof fails, we simply don't know anything } public void testNETBEANS407c() throws Exception { @@ -1970,6 +2002,405 @@ enum E {A} .assertWarnings("9:12-9:21:verifier:ERR_NotNull"); } + public void testIfAndInstanceOf() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + public class Test { + public @NotNull Object test(Object o) { + if (o instanceof String s) { + return s; + } else { + return o; //can't say anything + } + } + } + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings(); + } + + public void testTypeAnnotations1() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + public class Test { + private void test(Box<@NullAllowed String> boxOfString) { + boxOfString.get().toString(); + } + } + class Box { + private final T t; + public Box(T t) { this.t = t; } + public T get() { return t; } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + """) + .run(NPECheck.class) + .assertWarnings("4:26-4:34:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotations2() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(Box<@NotNull List<@NullAllowed String>> boxOfStrings) { + boxOfStrings.get().get(1).toString(); + } + } + class Box { + private final T t; + public Box(T t) { this.t = t; } + public @NullAllowed T get() { return t; } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("5:27-5:30:verifier:Possibly Dereferencing null", + "5:34-5:42:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotations3() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(Box<@NotNull String> boxOfStrings) { + boxOfStrings.get().get(1).toString(); + } + } + class Box { + private final T t; + public Box(T t) { this.t = t; } + public List<@NullAllowed T> get() { return null; } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("5:34-5:42:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotations4() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test() { + List l = new ArrayList<@NullAllowed String>(); + l.get(0).toString(); + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + """) + .run(NPECheck.class) + .assertWarnings("6:17-6:25:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotationsRemapping() throws Exception { + //needs to properly map types on assign(!!!!) + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test() { + I i = new C<@NotNull Integer, @NullAllowed String>(0, null); + i.a().toString(); //warning + i.b().toString(); //no warning + } + } + interface I { + public A a(); + public B b(); + } + record C(B b, A a) implements I {} + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("6:14-6:22:verifier:Possibly Dereferencing null"); + } + + //TODO: other types of assignment(!) + + public void testTypeAnnotationsStringMergeTypeParams() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(boolean b) { + List l; + if (b) { + l = new ArrayList<@NotNull String>(); + } else { + l = new ArrayList<@NullAllowed String>(); + } + l.get(0).toString(); + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("11:17-11:25:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotationsStringMergeTypeParamsRemapping() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(boolean b) { + I i; + if (b) { + i = new C<@NotNull Integer, @NullAllowed String>(0, null); + } else { + i = new C<@NotNull Integer, @NotNull String>(0, ""); + } + i.a().toString(); //warning + i.b().toString(); //no warning + } + } + interface I { + public A a(); + public B b(); + } + record C(B b, A a) implements I {} + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("11:14-11:22:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotationsArrays() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(@NotNull String[] arr1, @NullAllowed String[] arr2) { + if (arr1[0] != null) { + System.err.println("null!"); + } + arr2[0].toString(); + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("5:12-5:27:verifier:ERR_NotNull", + "8:16-8:24:verifier:Possibly Dereferencing null"); + } + + public void testTypeAnnotationsNestedTypesWarnings1() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test1(List<@NotNull String[]> arr) { + test2(arr); + } + private void test2(List<@NullAllowed String[]> arr) { + test1(arr); + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("5:14-5:17:verifier:Nullness states mismatch", + "8:14-8:17:verifier:Nullness states mismatch"); + } + + public void testJSpecifyNullMarked() throws Exception { + HintTest.create() + .sourceLevel("21") + .classpath(FileUtil.urlForArchiveOrDir(new File(System.getProperty("hints-jspecify.jar.location")))) + .input(""" + package test; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + public class Test { + public String test(@Nullable String s) { + return s; + } + } + """) + .run(NPECheck.class) + .assertWarnings("6:15-6:16:verifier:ERR_ReturningPossibleNullFromNonNull"); + } + + public void testJSpecifyUnspecified() throws Exception { + HintTest.create() + .sourceLevel("21") + .classpath(FileUtil.urlForArchiveOrDir(new File(System.getProperty("hints-jspecify.jar.location")))) + .input(""" + package test; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + import org.jspecify.annotations.NullnessUnspecified; + @NullMarked + public class Test { + public @NullnessUnspecified String test(@Nullable String s) { + return s; + } + public Object test(@NullnessUnspecified Object o) { + if (o instanceof String s) { + return s; + } else { + return o; //can't say anything + } + } + } + """) + .input("org/jspecify/annotations/NullnessUnspecified.java", + """ + package org.jspecify.annotations; + import java.lang.annotation.*; + @Target(ElementType.TYPE_USE) + public @interface NullnessUnspecified {} + """) + .run(NPECheck.class) + .assertWarnings(); + } + + public void testJSpecifyNullMarkedLocalVariables1() throws Exception { + HintTest.create() + .sourceLevel("21") + .classpath(FileUtil.urlForArchiveOrDir(new File(System.getProperty("hints-jspecify.jar.location")))) + .input(""" + package test; + import org.jspecify.annotations.NullMarked; + import org.jspecify.annotations.Nullable; + @NullMarked + public class Test { + public String test(@Nullable String s) { + String local = s; + return local != null ? local : ""; + } + } + """) + .run(NPECheck.class) + .assertWarnings(); + } + + public void testNotNullReturnType() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + public class Test { + public @NotNull String test(@NullAllowed String s) { + return s; + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + @Target(ElementType.TYPE_USE) + @interface NotNull {} + """) + .run(NPECheck.class) + .assertWarnings("4:15-4:16:verifier:ERR_ReturningPossibleNullFromNonNull"); + } + + public void testSynchronizedHint() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + public class Test { + public void test(@NullAllowed String s) { + Object o = null; + synchronized (o) {} + synchronized (s) {} + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + """) + .run(NPECheck.class) + .assertWarnings("5:22-5:23:verifier:Synchronizing on null", + "6:22-6:23:verifier:Synchronizing on possible null"); + } + + public void testAssignmentIsExpression() throws Exception { + HintTest.create() + .sourceLevel("21") + .input(""" + package test; + import java.lang.annotation.*; + import java.util.*; + public class Test { + private void test(@NullAllowed String str) { + String s; + if ((s = str) != null) { + s.toString(); + str.toString(); + } + } + } + @Target(ElementType.TYPE_USE) + @interface NullAllowed {} + """) + .run(NPECheck.class) + .assertWarnings(); + } + + //TODO: NullnessUnspecified + + //TODO: check full "assignment" type in hints;; needs to remap parameter types(!!!) + //TODO: when a declaration has annotations, the state on assign should be sensibly merged to it + private void performAnalysisTest(String fileName, String code, String... golden) throws Exception { HintTest.create() .input(fileName, code) diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TypeUtilities.java b/java/java.source.base/src/org/netbeans/api/java/source/TypeUtilities.java index 0041fec50388..a5fad921c316 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TypeUtilities.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TypeUtilities.java @@ -19,6 +19,7 @@ package org.netbeans.api.java.source; import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.CapturedType; import com.sun.tools.javac.code.Type.ClassType; @@ -186,7 +187,13 @@ public TypeMirror getDenotableType(TypeMirror type) { return t; } } - + + public TypeMirror asSuper(TypeMirror subtype, TypeElement superEl) { + Types types = Types.instance(info.impl.getJavacTask().getContext()); + + return types.asSuper((Type) subtype, (Symbol) superEl); + } + boolean checkDenotable(Type t) { return denotableChecker.visit(t, null); }