org.eclipse.jdt.internal.compiler.ast.FakedTrackingVariable Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ecj Show documentation
Show all versions of ecj Show documentation
Eclipse Compiler for Java(TM)
/*******************************************************************************
* Copyright (c) 2011, 2024 GK Software SE and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Stephan Herrmann - initial API and implementation
* Nikolay Metchev ([email protected]) - Contributions for
* bug 411098 - [compiler][resource] Invalid Resource Leak Warning using ternary operator inside try-with-resource
*******************************************************************************/
package org.eclipse.jdt.internal.compiler.ast;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.codegen.CodeStream;
import org.eclipse.jdt.internal.compiler.codegen.ConstantPool;
import org.eclipse.jdt.internal.compiler.flow.FinallyFlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowContext;
import org.eclipse.jdt.internal.compiler.flow.FlowInfo;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.LocalVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TagBits;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.compiler.util.Util;
/**
* A faked local variable declaration used for keeping track of data flows of a
* special variable. Certain events will be recorded by changing the null info
* for this variable.
*
* See bug 349326 - [1.7] new warning for missing try-with-resources
*/
public class FakedTrackingVariable extends LocalDeclaration {
private static final char[] UNASSIGNED_CLOSEABLE_NAME = "".toCharArray(); //$NON-NLS-1$
private static final char[] UNASSIGNED_CLOSEABLE_NAME_TEMPLATE = "".toCharArray(); //$NON-NLS-1$
private static final char[] TEMPLATE_ARGUMENT = "{0}".toCharArray(); //$NON-NLS-1$
// a call to close() was seen at least on one path:
private static final int CLOSE_SEEN = 1;
// the resource is shared with outside code either by
// - passing as an arg in a method call or
// - obtaining this from a method call or array reference
// Interpret that we may or may not be responsible for closing
private static final int SHARED_WITH_OUTSIDE = 2;
// the resource is likely owned by outside code (owner has responsibility to close):
// - obtained as argument of the current method, or via a field read
// - stored into a field
// - returned as the result of this method
private static final int OWNED_BY_OUTSIDE = 4;
// If close() is invoked from a nested method (inside a local type) report remaining problems only as potential:
private static final int CLOSED_IN_NESTED_METHOD = 8;
// explicit closing has been reported already against this resource:
private static final int REPORTED_EXPLICIT_CLOSE = 16;
// a location independent potential problem has been reported against this resource:
private static final int REPORTED_POTENTIAL_LEAK = 32;
// a location independent definitive problem has been reported against this resource:
private static final int REPORTED_DEFINITIVE_LEAK = 64;
// a local declarations that acts as the element variable of a foreach loop (should never suggest to use t-w-r):
private static final int FOREACH_ELEMENT_VAR = 128;
// - passed as an effectively final resource in t-w-r JDK 9 and above
private static final int TWR_EFFECTIVELY_FINAL = 256;
public MessageSend acquisition;
public static boolean TEST_372319 = false; // see https://bugs.eclipse.org/372319
/**
* Bitset of {@link #CLOSE_SEEN}, {@link #SHARED_WITH_OUTSIDE}, {@link #OWNED_BY_OUTSIDE}, {@link #CLOSED_IN_NESTED_METHOD}, {@link #REPORTED_EXPLICIT_CLOSE}, {@link #REPORTED_POTENTIAL_LEAK} and {@link #REPORTED_DEFINITIVE_LEAK}.
*/
private int globalClosingState = 0;
private boolean useAnnotations = false;
public static final int NOT_OWNED = -2;
public static final int NOT_OWNED_PER_DEFAULT = -1;
public static final int OWNED_PER_DEFAULT = 1;
public static final int OWNED = 2;
static int owningStateFromTagBits(long owningTagBits, int defaultState) {
if (owningTagBits == TagBits.AnnotationOwning)
return OWNED;
if (owningTagBits == TagBits.AnnotationNotOwning)
return NOT_OWNED;
return defaultState;
}
/**
* One of {@link #NOT_OWNED}, {@link #NOT_OWNED_PER_DEFAULT}, {@link #OWNED_PER_DEFAULT}, {@link #OWNED}.
* A value of {@code 0} signals unknown state.
*/
public int owningState = 0;
public LocalVariableBinding originalBinding; // the real local being tracked, can be null for preliminary track vars for allocation expressions
public FieldBinding originalFieldBinding; // when tracking an @Owning field of resource type
public FakedTrackingVariable innerTracker; // chained tracking variable of a chained (wrapped) resource
public FakedTrackingVariable outerTracker; // inverse of 'innerTracker'
MethodScope methodScope; // designates the method declaring this variable
private HashMap recordedLocations; // initially null, ASTNode -> Integer
// temporary storage while analyzing "res = new Res();":
private ASTNode currentAssignment; // temporarily store the assignment as the location for error reporting
// if tracking var was allocated from a finally context, record here the flow context of the corresponding try block
private FlowContext tryContext;
// designate the exact scope where this FTV was created
private BlockScope blockScope;
// within try blocks remember in which contexts the local was re-assigned
private Set modificationContexts;
public FakedTrackingVariable(LocalVariableBinding original, ASTNode location, FlowInfo flowInfo, FlowContext flowContext, int nullStatus, boolean useAnnotations) {
this(original.name, location, original.declaringScope, flowInfo, flowContext, nullStatus, useAnnotations);
this.methodScope = original.declaringScope.methodScope();
this.originalBinding = original;
}
public FakedTrackingVariable(LocalVariableBinding original, BlockScope scope, ASTNode location, FlowInfo flowInfo, FlowContext flowContext, int nullStatus, boolean useAnnotations) {
this(original.name, location, original.declaringScope, flowInfo, flowContext, nullStatus, useAnnotations);
this.methodScope = original.declaringScope.methodScope();
this.originalBinding = original;
this.blockScope = scope;
}
public FakedTrackingVariable(FieldBinding original, BlockScope scope, ASTNode location, FlowInfo flowInfo, FlowContext flowContext, int nullStatus, boolean useAnnotations) {
this(original.name, location, scope, flowInfo, flowContext, nullStatus, useAnnotations);
this.originalFieldBinding = original;
this.blockScope = scope;
}
private FakedTrackingVariable(char[] name, ASTNode location, BlockScope scope, FlowInfo flowInfo, FlowContext flowContext,
int nullStatus, boolean useAnnotations) {
super(name, location.sourceStart, location.sourceEnd);
this.type = new SingleTypeReference(
TypeConstants.OBJECT,
((long)this.sourceStart <<32)+this.sourceEnd);
this.useAnnotations = useAnnotations;
// inside a finally block?
while (flowContext != null) {
if (flowContext instanceof FinallyFlowContext) {
// yes -> connect to the corresponding try block:
this.tryContext = ((FinallyFlowContext) flowContext).tryContext;
break;
}
flowContext = flowContext.parent;
}
resolve(scope);
if (nullStatus != 0)
flowInfo.markNullStatus(this.binding, nullStatus); // mark that this flow has seen the resource
}
/* Create an unassigned tracking variable while analyzing an allocation expression: */
private FakedTrackingVariable(BlockScope scope, ASTNode location, FlowInfo flowInfo, int nullStatus) {
super(UNASSIGNED_CLOSEABLE_NAME, location.sourceStart, location.sourceEnd);
this.type = new SingleTypeReference(
TypeConstants.OBJECT,
((long)this.sourceStart <<32)+this.sourceEnd);
this.methodScope = scope.methodScope();
this.originalBinding = null;
this.useAnnotations = scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled;
resolve(scope);
if (nullStatus != 0)
flowInfo.markNullStatus(this.binding, nullStatus); // mark that this flow has seen the resource
}
private void attachTo(LocalVariableBinding local) {
local.closeTracker = this;
this.originalBinding = local;
}
@Override
public void generateCode(BlockScope currentScope, CodeStream codeStream)
{ /* NOP - this variable is completely dummy, ie. for analysis only. */ }
@Override
public void resolve (BlockScope scope) {
// only need the binding, which is used as reference in FlowInfo methods.
this.binding = new LocalVariableBinding(
this.name,
scope.getJavaLangObject(), // dummy, just needs to be a reference type
0,
false);
this.binding.closeTracker = this;
this.binding.declaringScope = scope;
this.binding.setConstant(Constant.NotAConstant);
this.binding.useFlag = LocalVariableBinding.USED;
// use a free slot without assigning it:
this.binding.id = scope.registerTrackingVariable(this);
}
/** retrieve a closetracker for local that signifies a certain risk of resource leak in flowInfo. */
private static FakedTrackingVariable getRiskyCloseTrackerAt(LocalVariableBinding local, Scope scope, FlowInfo flowInfo) {
if (local.closeTracker == null) {
return null;
}
if (flowInfo.nullStatus(local.closeTracker.binding) != FlowInfo.UNKNOWN)
return local.closeTracker;
while (scope instanceof BlockScope) {
FakedTrackingVariable tracker = ((BlockScope) scope).getCloseTrackerFor(local);
if (tracker != null) {
if (tracker.riskyNullStatusAt(flowInfo) != 0)
return tracker;
if (tracker.hasDefinitelyNoResource(flowInfo))
return null;
}
scope = scope.parent;
}
return null;
}
/**
* If expression resolves to a value of type AutoCloseable answer the variable that tracks closing of that local.
* Covers two cases:
*
* - value is a local variable reference, create tracking variable it if needed.
*
- value is an allocation expression, return a preliminary tracking variable if set.
*
* @return a new {@link FakedTrackingVariable} or null.
*/
public static FakedTrackingVariable getCloseTrackingVariable(Expression expression, FlowInfo flowInfo, FlowContext flowContext, boolean useAnnotations) {
while (true) {
if (expression instanceof CastExpression)
expression = ((CastExpression) expression).expression;
else if (expression instanceof Assignment)
expression = ((Assignment) expression).expression;
else if (expression instanceof ConditionalExpression) {
return getMoreUnsafeFromBranches((ConditionalExpression)expression, flowInfo,
branch -> getCloseTrackingVariable(branch, flowInfo, flowContext, useAnnotations));
} else if (expression instanceof SwitchExpression) {
for (Expression re : ((SwitchExpression) expression).resultExpressions) {
FakedTrackingVariable fakedTrackingVariable = getCloseTrackingVariable(re, flowInfo, flowContext, useAnnotations);
if (fakedTrackingVariable != null) {
return fakedTrackingVariable;
}
}
return null;
}
else
break;
}
FieldBinding fieldBinding = null;
if (expression instanceof SingleNameReference) {
SingleNameReference name = (SingleNameReference) expression;
if (name.binding instanceof LocalVariableBinding) {
LocalVariableBinding local = (LocalVariableBinding)name.binding;
if (local.closeTracker != null)
return local.closeTracker;
if (!isCloseableNotWhiteListed(expression.resolvedType))
return null;
if ((local.tagBits & TagBits.IsResource) != 0)
return null;
// tracking var doesn't yet exist. This happens in finally block
// which is analyzed before the corresponding try block
Statement location = local.declaration;
local.closeTracker = new FakedTrackingVariable(local, location, flowInfo, flowContext, FlowInfo.UNKNOWN, useAnnotations);
if (local.isParameter()) {
local.closeTracker.globalClosingState |= OWNED_BY_OUTSIDE;
// status of this tracker is now UNKNOWN
}
return local.closeTracker;
} else if (name.binding instanceof FieldBinding) {
fieldBinding = (FieldBinding) name.binding;
}
} else if (expression instanceof FieldReference) {
FieldReference fieldReference = (FieldReference) expression;
if (fieldReference.receiver.isThis())
fieldBinding = fieldReference.binding;
} else if (expression instanceof AllocationExpression) {
// return any preliminary tracking variable from analyseCloseableAllocation
return ((AllocationExpression) expression).closeTracker;
} else if (expression instanceof MessageSend) {
// return any preliminary tracking variable from analyseCloseableAcquisition
return ((MessageSend) expression).closeTracker;
}
if (fieldBinding != null)
return fieldBinding.closeTracker;
return null;
}
/**
* Before analyzing an assignment of this shape: singleName = new Allocation()
* connect any tracking variable of the LHS with the allocation on the RHS.
* Also the assignment is temporarily stored in the tracking variable in case we need to
* report errors because the assignment leaves the old LHS value unclosed.
* In this case the assignment should be used as the error location.
*
* @param location the assignment/local declaration being analyzed
* @param local the local variable being assigned to
* @param rhs the rhs of the assignment resp. the initialization of the local variable declaration.
* Precondition: client has already checked that the resolved type of this expression is either a closeable type or NULL.
*/
public static FakedTrackingVariable preConnectTrackerAcrossAssignment(ASTNode location, LocalVariableBinding local, Expression rhs, FlowInfo flowInfo, boolean useAnnotations) {
FakedTrackingVariable closeTracker = null;
if (containsAllocation(rhs)) {
closeTracker = local.closeTracker;
if (closeTracker == null) {
if (rhs.resolvedType != TypeBinding.NULL) { // not NULL means valid closeable as per method precondition
closeTracker = new FakedTrackingVariable(local, location, flowInfo, null, FlowInfo.UNKNOWN, useAnnotations);
if (local.isParameter()) {
closeTracker.globalClosingState |= OWNED_BY_OUTSIDE;
}
}
}
if (closeTracker != null) {
closeTracker.currentAssignment = location;
preConnectTrackerAcrossAssignment(location, local, flowInfo, closeTracker, rhs, useAnnotations);
}
} else if (rhs instanceof MessageSend) {
MessageSend messageSend = (MessageSend) rhs;
closeTracker = local.closeTracker;
if (closeTracker != null) {
closeTracker = handleReassignment(flowInfo, closeTracker, location);
}
if (rhs.resolvedType != TypeBinding.NULL // not NULL means valid closeable as per method precondition
&& (!rhs.resolvedType.hasTypeBit(TypeIds.BitResourceFreeCloseable)
|| isBlacklistedMethod(rhs))) {
if (closeTracker == null)
closeTracker = new FakedTrackingVariable(local, location, flowInfo, null, FlowInfo.UNKNOWN, useAnnotations);
messageSend.closeTracker = closeTracker;
closeTracker.currentAssignment = location;
}
if (closeTracker != null) {
if (messageSend.binding != null && ((messageSend.binding.tagBits & TagBits.AnnotationNotOwning) == 0))
closeTracker.owningState = OWNED;
}
}
return closeTracker;
}
private static boolean containsAllocation(SwitchExpression location) {
for (Expression re : location.resultExpressions) {
if (containsAllocation(re))
return true;
}
return false;
}
private static boolean containsAllocation(ASTNode location) {
if (location instanceof AllocationExpression) {
return true;
} else if (location instanceof ConditionalExpression) {
ConditionalExpression conditional = (ConditionalExpression) location;
return containsAllocation(conditional.valueIfTrue) || containsAllocation(conditional.valueIfFalse);
} else if (location instanceof SwitchExpression) {
return containsAllocation((SwitchExpression) location);
} else if (location instanceof CastExpression) {
return containsAllocation(((CastExpression) location).expression);
} else if (location instanceof MessageSend) {
if (isFluentMethod(((MessageSend) location).binding))
return containsAllocation(((MessageSend) location).receiver);
}
return false;
}
private static void preConnectTrackerAcrossAssignment(ASTNode location, LocalVariableBinding local, FlowInfo flowInfo,
FakedTrackingVariable closeTracker, Expression expression, boolean useAnnotations) {
if (expression instanceof AllocationExpression) {
preConnectTrackerAcrossAssignment(location, local, flowInfo, (AllocationExpression) expression, closeTracker, useAnnotations);
} else if (expression instanceof ConditionalExpression) {
preConnectTrackerAcrossAssignment(location, local, flowInfo, (ConditionalExpression) expression, closeTracker, useAnnotations);
} else if (expression instanceof SwitchExpression) {
preConnectTrackerAcrossAssignment(location, local, flowInfo, (SwitchExpression) expression, closeTracker, useAnnotations);
} else if (expression instanceof CastExpression) {
preConnectTrackerAcrossAssignment(location, local, ((CastExpression) expression).expression, flowInfo, useAnnotations);
} else if (expression instanceof MessageSend) {
if (isFluentMethod(((MessageSend) expression).binding))
preConnectTrackerAcrossAssignment(location, local, ((MessageSend) expression).receiver, flowInfo, useAnnotations);
}
}
private static void preConnectTrackerAcrossAssignment(ASTNode location, LocalVariableBinding local, FlowInfo flowInfo,
ConditionalExpression conditional, FakedTrackingVariable closeTracker, boolean useAnnotations) {
preConnectTrackerAcrossAssignment(location, local, flowInfo, closeTracker, conditional.valueIfFalse, useAnnotations);
preConnectTrackerAcrossAssignment(location, local, flowInfo, closeTracker, conditional.valueIfTrue, useAnnotations);
}
private static void preConnectTrackerAcrossAssignment(ASTNode location, LocalVariableBinding local, FlowInfo flowInfo,
SwitchExpression se, FakedTrackingVariable closeTracker, boolean useAnnotations) {
for (Expression re : se.resultExpressions) {
preConnectTrackerAcrossAssignment(location, local, flowInfo, closeTracker, re, useAnnotations);
}
}
private static void preConnectTrackerAcrossAssignment(ASTNode location, LocalVariableBinding local, FlowInfo flowInfo,
AllocationExpression allocationExpression, FakedTrackingVariable closeTracker, boolean useAnnotations) {
allocationExpression.closeTracker = closeTracker;
if (allocationExpression.arguments != null && allocationExpression.arguments.length > 0) {
// also push into nested allocations, see https://bugs.eclipse.org/368709
Expression firstArg = allocationExpression.arguments[0];
if (isCloseableNotWhiteListed(firstArg.resolvedType)) {
FakedTrackingVariable inner = preConnectTrackerAcrossAssignment(location, local, firstArg, flowInfo, useAnnotations);
if (inner != closeTracker && closeTracker.innerTracker == null)
closeTracker.innerTracker = inner;
}
}
}
/**
* Compute/assign a tracking variable for a freshly allocated closeable value, using information from our white lists.
* See Bug 358903 - Filter practically unimportant resource leak warnings
*/
public static void analyseCloseableAllocation(BlockScope scope, FlowInfo flowInfo, FlowContext flowContext, AllocationExpression allocation) {
// client has checked that the resolvedType is an AutoCloseable, hence the following cast is safe:
if (((ReferenceBinding)allocation.resolvedType).hasTypeBit(TypeIds.BitResourceFreeCloseable)) {
// remove unnecessary attempts (closeable is not relevant)
if (allocation.closeTracker != null) {
allocation.closeTracker.withdraw();
allocation.closeTracker = null;
}
} else if (((ReferenceBinding)allocation.resolvedType).hasTypeBit(TypeIds.BitWrapperCloseable)) {
boolean isWrapper = true;
if (allocation.arguments != null && allocation.arguments.length > 0) {
// find the wrapped resource represented by its tracking var:
FakedTrackingVariable innerTracker = findCloseTracker(scope, flowInfo, allocation.arguments[0]);
if (innerTracker != null) {
FakedTrackingVariable currentInner = innerTracker;
do {
if (currentInner == allocation.closeTracker)
return; // self wrap (res = new Res(res)) -> neither change (here) nor remove (below)
// also check for indirect cycles, see https://bugs.eclipse.org/368709
currentInner = currentInner.innerTracker;
} while (currentInner != null);
int newStatus = FlowInfo.NULL;
if (allocation.closeTracker == null) {
allocation.closeTracker = new FakedTrackingVariable(scope, allocation, flowInfo, FlowInfo.UNKNOWN); // no local available, closeable is unassigned
} else {
if (scope.finallyInfo != null) {
// inject results from analysing a finally block onto the newly connected wrapper
int finallyStatus = scope.finallyInfo.nullStatus(allocation.closeTracker.binding);
if (finallyStatus != FlowInfo.UNKNOWN)
newStatus = finallyStatus;
}
}
if (allocation.closeTracker.innerTracker != null && allocation.closeTracker.innerTracker != innerTracker) {
innerTracker = pickMoreUnsafe(allocation.closeTracker.innerTracker, innerTracker, flowInfo);
}
allocation.closeTracker.innerTracker = innerTracker;
innerTracker.outerTracker = allocation.closeTracker;
flowInfo.markNullStatus(allocation.closeTracker.binding, newStatus);
if (newStatus != FlowInfo.NULL) {
// propagate results from a finally block also into nested resources:
FakedTrackingVariable currentTracker = innerTracker;
while (currentTracker != null) {
flowInfo.markNullStatus(currentTracker.binding, newStatus);
currentTracker.globalClosingState |= allocation.closeTracker.globalClosingState;
currentTracker = currentTracker.innerTracker;
}
}
return; // keep chaining wrapper (by avoiding to fall through to removeTrackingVar below)
} else {
if (!isAnyCloseable(allocation.arguments[0].resolvedType)) {
isWrapper = false; // argument is not closeable
}
}
} else {
isWrapper = false; // no argument
}
// successful wrapper detection has exited above, let's see why that failed
if (isWrapper) {
// remove unnecessary attempts (wrapper has no relevant inner)
if (allocation.closeTracker != null) {
allocation.closeTracker.withdraw();
allocation.closeTracker = null;
}
} else {
// allocation does not provide a resource as the first argument -> don't treat as a wrapper
handleRegularResource(scope, flowInfo, flowContext, allocation);
}
} else { // regular resource
handleRegularResource(scope, flowInfo, flowContext, allocation);
}
}
/**
* Check if a message send acquires a closeable from its receiver, see:
* Bug 463320 - [compiler][resource] potential "resource leak" problem disappears when local variable inlined
*/
public static FlowInfo analyseCloseableAcquisition(BlockScope scope, FlowInfo flowInfo, FlowContext flowContext, MessageSend acquisition) {
if (isFluentMethod(acquisition.binding)) {
// share the existing close tracker of the receiver (if any):
acquisition.closeTracker = findCloseTracker(scope, flowInfo, acquisition.receiver);
return flowInfo;
}
// client has checked that the resolvedType is an AutoCloseable, hence the following cast is safe:
if (((ReferenceBinding)acquisition.resolvedType).hasTypeBit(TypeIds.BitResourceFreeCloseable)
&& !isBlacklistedMethod(acquisition)) {
// remove unnecessary attempts (closeable is not relevant)
if (acquisition.closeTracker != null) {
acquisition.closeTracker.withdraw();
acquisition.closeTracker = null;
}
return flowInfo;
} else { // regular resource
FakedTrackingVariable tracker = acquisition.closeTracker;
if (scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled) {
long owningTagBits = acquisition.binding.tagBits & TagBits.AnnotationOwningMASK;
int initialNullStatus = (owningTagBits == TagBits.AnnotationNotOwning) ? FlowInfo.NON_NULL : FlowInfo.NULL;
if (tracker == null) {
acquisition.closeTracker =
tracker = new FakedTrackingVariable(scope, acquisition, flowInfo, initialNullStatus); // no local available, closeable is unassigned
tracker.owningState = owningStateFromTagBits(owningTagBits, OWNED_PER_DEFAULT);
} else {
flowInfo.markNullStatus(tracker.binding, initialNullStatus);
}
tracker.acquisition = acquisition;
return flowInfo;
}
if (tracker != null) {
// pre-connected tracker means: directly assigning the acquisition to a local, forget special treatment:
// (in the unannotated case the pre-connected tracker has no valuable information)
tracker.withdraw();
acquisition.closeTracker = null;
return flowInfo;
} else {
tracker = new FakedTrackingVariable(scope, acquisition, flowInfo, FlowInfo.UNKNOWN); // no local available, closeable is unassigned
acquisition.closeTracker = tracker;
}
tracker.acquisition = acquisition;
FlowInfo outsideInfo = flowInfo.copy();
outsideInfo.markAsDefinitelyNonNull(tracker.binding);
tracker.markNullStatus(flowInfo, flowContext, FlowInfo.NULL);
return FlowInfo.conditional(outsideInfo, flowInfo);
}
}
private static boolean isFluentMethod(MethodBinding binding) {
if (binding.isStatic())
return false;
ReferenceBinding declaringClass = binding.declaringClass;
if (declaringClass.equals(binding.returnType)) {
for (char[][] compoundName : TypeConstants.FLUENT_RESOURCE_CLASSES) {
if (CharOperation.equals(compoundName, declaringClass.compoundName))
return true;
}
}
return false;
}
private static FakedTrackingVariable getMoreUnsafeFromBranches(ConditionalExpression conditionalExpression,
FlowInfo flowInfo, Function retriever)
{
FakedTrackingVariable trackerIfTrue = retriever.apply(conditionalExpression.valueIfTrue);
FakedTrackingVariable trackerIfFalse = retriever.apply(conditionalExpression.valueIfFalse);
if (trackerIfTrue == null)
return trackerIfFalse;
if (trackerIfFalse == null)
return trackerIfTrue;
return pickMoreUnsafe(trackerIfTrue, trackerIfFalse, flowInfo);
}
private static FakedTrackingVariable pickMoreUnsafe(FakedTrackingVariable tracker1, FakedTrackingVariable tracker2, FlowInfo info) {
// whichever of the two trackers has stronger indication to be leaking will be returned,
// the other one will be removed from the scope (considered to be merged into the former).
int status1 = info.nullStatus(tracker1.binding);
int status2 = info.nullStatus(tracker2.binding);
if (status1 == FlowInfo.NULL || status2 == FlowInfo.NON_NULL) return pick(tracker1, tracker2);
if (status1 == FlowInfo.NON_NULL || status2 == FlowInfo.NULL) return pick(tracker2, tracker1);
if ((status1 & FlowInfo.POTENTIALLY_NULL) != 0) return pick(tracker1, tracker2);
if ((status2 & FlowInfo.POTENTIALLY_NULL) != 0) return pick(tracker2, tracker1);
return pick(tracker1, tracker2);
}
private static FakedTrackingVariable pick(FakedTrackingVariable tracker1, FakedTrackingVariable tracker2) {
tracker2.withdraw();
return tracker1;
}
private static void handleRegularResource(BlockScope scope, FlowInfo flowInfo, FlowContext flowContext, AllocationExpression allocation) {
FakedTrackingVariable presetTracker = allocation.closeTracker;
LocalVariableBinding local = null;
if (presetTracker != null && presetTracker.originalBinding != null) {
if (presetTracker.isInFinallyBlockOf(flowContext) && presetTracker.recordFirstModification(flowContext)) {
// not a re-assignment after the one seen within finally, but this excuse is valid only once.
} else {
local = presetTracker.originalBinding;
// the current assignment forgets a previous resource in the LHS, may cause a leak
// report now because handleResourceAssignment can't distinguish this from a self-wrap situation
handleReassignment(flowInfo, presetTracker, presetTracker.currentAssignment);
if (local != null && !presetTracker.hasDefinitelyNoResource(flowInfo) && presetTracker.currentAssignment instanceof Assignment) {
boolean useAnnotations = scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled;
allocation.closeTracker =
local.closeTracker =
new FakedTrackingVariable(local, scope, presetTracker.currentAssignment, flowInfo, flowContext, FlowInfo.NULL, useAnnotations);
if (useAnnotations)
allocation.closeTracker.owningState = OWNED;
// if finally closes the given local, this may affect any resource bound to the same local
// (but re-assignment over an existing unclosed resource will be recorded (at-location) in handleReassignment() above).
FlowInfo enclosingFinallyInfo = null;
Scope current = scope;
while (current instanceof BlockScope && enclosingFinallyInfo == null) {
enclosingFinallyInfo = ((BlockScope) current).finallyInfo;
current = current.parent;
}
if (enclosingFinallyInfo != null) {
int finallyNullStatus = enclosingFinallyInfo.nullStatus(presetTracker.binding);
enclosingFinallyInfo.markNullStatus(local.closeTracker.binding, finallyNullStatus);
}
presetTracker.markNullStatus(flowInfo, flowContext, FlowInfo.UNKNOWN); // no longer relevant in this flow
}
}
} else {
allocation.closeTracker = new FakedTrackingVariable(scope, allocation, flowInfo, FlowInfo.UNKNOWN); // no local available, closeable is unassigned
}
allocation.closeTracker.markNullStatus(flowInfo, flowContext, FlowInfo.NULL);
}
/** Was this FTV created from the finally block of the current context? */
private boolean isInFinallyBlockOf(FlowContext flowContext) {
if (this.tryContext != null) {
do {
if (flowContext == this.tryContext)
return true;
flowContext = flowContext.parent;
} while (flowContext != null);
}
return false;
}
/** Try to remember flowContext as the first one where this FTV is being modified. */
private boolean recordFirstModification(FlowContext flowContext) {
if (this.modificationContexts == null) {
this.modificationContexts = new HashSet<>();
this.modificationContexts.add(flowContext);
return true;
}
FlowContext current = flowContext;
while (current != null) {
if (this.modificationContexts.contains(current))
return false;
current = current.parent;
}
return this.modificationContexts.add(flowContext);
}
private static FakedTrackingVariable handleReassignment(FlowInfo flowInfo, FakedTrackingVariable existingTracker, ASTNode location) {
int riskyStatus = existingTracker.riskyNullStatusAt(flowInfo);
if (riskyStatus != 0
&& !(location instanceof LocalDeclaration)) // forgetting old val in local decl is syntactically impossible
{
existingTracker.recordErrorLocation(location, riskyStatus);
return null; // stop using this tracker after re-assignment
}
return existingTracker;
}
/** Find an existing tracking variable for the argument of an allocation for a resource wrapper. */
private static FakedTrackingVariable findCloseTracker(BlockScope scope, FlowInfo flowInfo, Expression arg)
{
while (arg instanceof Assignment) {
Assignment assign = (Assignment)arg;
LocalVariableBinding innerLocal = assign.localVariableBinding();
if (innerLocal != null) {
// nested assignment has already been processed
return innerLocal.closeTracker;
} else {
arg = assign.expression; // unwrap assignment and fall through
}
}
if (arg instanceof SingleNameReference) {
// is allocation arg a reference to an existing closeable?
LocalVariableBinding local = arg.localVariableBinding();
if (local != null) {
return local.closeTracker;
}
} else if (arg instanceof AllocationExpression) {
// nested allocation
return ((AllocationExpression)arg).closeTracker;
} else if (arg instanceof MessageSend) {
return ((MessageSend) arg).closeTracker;
}
return null; // not a tracked expression
}
/**
* Given the rhs of an assignment or local declaration has a (Auto)Closeable type (or null), setup for leak analysis now:
* Create or re-use a tracking variable, and wire and initialize everything.
* @param scope scope containing the assignment
* @param upstreamInfo info without analysis of the rhs, use this to determine the status of a resource being disconnected
* @param flowInfo info with analysis of the rhs, use this for recording resource status because this will be passed downstream
* @param location where to report warnigs/errors against
* @param rhs the right hand side of the assignment, this expression is to be analyzed.
* The caller has already checked that the rhs is either of a closeable type or null.
* @param local the local variable into which the rhs is being assigned
*/
public static void handleResourceAssignment(BlockScope scope, FlowInfo upstreamInfo, FlowInfo flowInfo, FlowContext flowContext, ASTNode location, Expression rhs, LocalVariableBinding local)
{
// does the LHS (local) already have a tracker, indicating we may leak a resource by the assignment?
FakedTrackingVariable previousTracker = getRiskyCloseTrackerAt(local, scope, upstreamInfo);
FakedTrackingVariable disconnectedTracker = null;
if (previousTracker != null) {
// assigning to a variable already holding an AutoCloseable, has it been closed before?
if (previousTracker.riskyNullStatusAt(upstreamInfo) != 0) // only if previous value may be relevant
disconnectedTracker = previousTracker; // report error below, unless we have a self-wrap assignment
} else {
previousTracker = local.closeTracker; // not yet risky, but may still be releavant below
}
boolean useAnnotations = scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled;
rhsAnalyis:
if (rhs.resolvedType != TypeBinding.NULL) {
// new value is AutoCloseable, start tracking, possibly re-using existing tracker var:
FakedTrackingVariable rhsTrackVar = getCloseTrackingVariable(rhs, flowInfo, flowContext, useAnnotations);
if (rhsTrackVar != null) { // 1. if RHS has a tracking variable...
if (local.closeTracker == null) {
// null shouldn't occur but let's play safe:
if (rhsTrackVar.originalBinding != null) {
local.closeTracker = rhsTrackVar; // a.: let fresh LHS share it
} else if (rhsTrackVar.originalFieldBinding != null) {
local.closeTracker = rhsTrackVar;
}
if (rhsTrackVar.currentAssignment == location) {
// pre-set tracker from lhs - passed from outside (or foreach)?
// now it's a fresh resource
rhsTrackVar.globalClosingState &= ~(SHARED_WITH_OUTSIDE|OWNED_BY_OUTSIDE|FOREACH_ELEMENT_VAR);
}
} else {
if (rhs instanceof AllocationExpression || rhs instanceof ConditionalExpression || rhs instanceof SwitchExpression || rhs instanceof MessageSend) {
if (rhsTrackVar == disconnectedTracker)
return; // b.: self wrapper: res = new Wrap(res); -> done!
if (local.closeTracker == rhsTrackVar
&& rhsTrackVar.isNotOwned()) {
// c.: assigning a fresh resource (pre-connected alloc)
// to a local previously holding an alien resource -> start over
local.closeTracker = new FakedTrackingVariable(local, scope, location, flowInfo, flowContext, FlowInfo.NULL, useAnnotations);
// still check disconnectedTracker below
break rhsAnalyis;
}
}
rhsTrackVar.attachTo(local); // d.: conflicting LHS and RHS, proceed with recordErrorLocation below
}
// keep close-status of RHS unchanged across this assignment
} else if (previousTracker != null) { // 2. re-use tracking variable from the LHS?
FlowContext currentFlowContext = flowContext;
checkReuseTracker : {
if (previousTracker.tryContext != null) {
while (currentFlowContext != null) {
if (previousTracker.tryContext == currentFlowContext) {
// "previous" location was the finally block of the current try statement.
// -> This is not a re-assignment.
// see https://bugs.eclipse.org/388996
break checkReuseTracker;
}
currentFlowContext = currentFlowContext.parent;
}
}
// re-assigning from a fresh value, mark as not-closed again:
if ((previousTracker.globalClosingState & (SHARED_WITH_OUTSIDE|OWNED_BY_OUTSIDE|FOREACH_ELEMENT_VAR)) == 0
&& flowInfo.hasNullInfoFor(previousTracker.binding)) // avoid spilling info into a branch that doesn't see the corresponding resource
previousTracker.markNullStatus(flowInfo, flowContext, FlowInfo.NULL);
local.closeTracker = analyseCloseableExpression(scope, flowInfo, flowContext, useAnnotations, local, location, rhs, previousTracker);
}
} else { // 3. no re-use, create a fresh tracking variable:
rhsTrackVar = analyseCloseableExpression(scope, flowInfo, flowContext, useAnnotations, local, location, rhs, null);
if (rhsTrackVar != null) {
rhsTrackVar.attachTo(local);
if (!useAnnotations) {
// a fresh resource, mark as not-closed:
if ((rhsTrackVar.globalClosingState & (SHARED_WITH_OUTSIDE|OWNED_BY_OUTSIDE|FOREACH_ELEMENT_VAR)) == 0)
rhsTrackVar.markNullStatus(flowInfo, flowContext, FlowInfo.NULL);
}
// TODO(stephan): this might be useful, but I could not find a test case for it:
// if (flowContext.initsOnFinally != null)
// flowContext.initsOnFinally.markAsDefinitelyNonNull(trackerBinding);
}
}
}
if (disconnectedTracker != null) {
if (disconnectedTracker.innerTracker != null && disconnectedTracker.innerTracker.binding.declaringScope == scope) {
// discard tracker for the wrapper but keep the inner:
disconnectedTracker.innerTracker.outerTracker = null;
scope.pruneWrapperTrackingVar(disconnectedTracker);
} else {
int upstreamStatus = disconnectedTracker.riskyNullStatusAt(upstreamInfo);
if (upstreamStatus != 0)
disconnectedTracker.recordErrorLocation(location, upstreamStatus);
}
}
}
public int riskyNullStatusAt(FlowInfo info) {
if (hasDefinitelyNoResource(info))
return 0;
int nullStatus = getNullStatusAggressively(this.binding, info);
if ((nullStatus & (FlowInfo.UNKNOWN | FlowInfo.NON_NULL)) == 0)
return nullStatus;
return 0;
}
/**
* When assigning an rhs of an (Auto)Closeable type (or null) to a field, inspect annotations
* to find out if the assignment assigns ownership to the instance (rather than current method).
* @param scope scope containing the assignment
* @param flowInfo info with analysis of the rhs, use this for recording resource status because this will be passed downstream
* @param location where to report warnigs/errors against
* @param rhs the right hand side of the assignment, this expression is to be analyzed.
* The caller has already checked that the rhs is either of a closeable type or null.
*/
public static void handleResourceFieldAssignment(BlockScope scope, FlowInfo flowInfo, FlowContext flowContext, ASTNode location, Expression rhs)
{
boolean useAnnotations = scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled;
if (rhs.resolvedType != TypeBinding.NULL) {
// new value is AutoCloseable, start tracking, possibly re-using existing tracker var:
FakedTrackingVariable rhsTrackVar = getCloseTrackingVariable(rhs, flowInfo, flowContext, useAnnotations);
if (rhsTrackVar != null) {
if (useAnnotations) {
if (location instanceof Assignment) {
Expression lhs = ((Assignment) location).lhs;
FieldBinding field = null;
// only consider access to field of the current instance:
if (lhs instanceof SingleNameReference) {
field = ((SingleNameReference) lhs).fieldBinding();
} else if (lhs instanceof FieldReference) {
FieldReference fieldReference = (FieldReference) lhs;
if (fieldReference.receiver.isThis())
field = fieldReference.binding;
}
if (field != null) {
if (!field.isStatic() && (field.tagBits & TagBits.AnnotationOwning) != 0) {
rhsTrackVar.markNullStatus(flowInfo, flowContext, FlowInfo.NON_NULL);
} else {
rhsTrackVar.markAsShared();
}
}
}
}
// TODO(stephan): this might be useful, but I could not find a test case for it:
// if (flowContext.initsOnFinally != null)
// flowContext.initsOnFinally.markAsDefinitelyNonNull(trackerBinding);
}
}
}
/**
* Analyze structure of a closeable expression, matching (chained) resources against our white lists.
* @param scope scope of the expression
* @param flowInfo where to record close status
* @param useAnnotations is annotation based resource analysis enabled
* @param local local variable to which the closeable is being assigned
* @param location where to flag errors/warnings against
* @param expression expression to be analyzed
* @param previousTracker when analyzing a re-assignment we may already have a tracking variable for local,
* which we should then re-use
* @return a tracking variable associated with local or null if no need to track
*/
private static FakedTrackingVariable analyseCloseableExpression(BlockScope scope, FlowInfo flowInfo, FlowContext flowContext, boolean useAnnotations,
LocalVariableBinding local, ASTNode location, Expression expression, FakedTrackingVariable previousTracker)
{
// unwrap uninteresting nodes:
while (true) {
if (expression instanceof Assignment)
expression = ((Assignment)expression).expression;
else if (expression instanceof CastExpression)
expression = ((CastExpression) expression).expression;
else
break;
}
if (expression instanceof Literal) {
return null;
}
// delegate to components:
if (expression instanceof ConditionalExpression) {
return getMoreUnsafeFromBranches((ConditionalExpression) expression, flowInfo,
branch -> analyseCloseableExpression(scope, flowInfo, flowContext, useAnnotations,
local, location, branch, previousTracker));
} else if (expression instanceof SwitchExpression) {
FakedTrackingVariable mostRisky = null;
for (Expression result : ((SwitchExpression) expression).resultExpressions) {
FakedTrackingVariable current = analyseCloseableExpression(scope, flowInfo, flowContext, useAnnotations,
local, location, result, previousTracker);
if (mostRisky == null)
mostRisky = current;
else
mostRisky = pickMoreUnsafe(mostRisky, current, flowInfo);
}
return mostRisky;
}
boolean isResourceProducer = false;
if (expression.resolvedType instanceof ReferenceBinding) {
ReferenceBinding resourceType = (ReferenceBinding) expression.resolvedType;
if (resourceType.hasTypeBit(TypeIds.BitResourceFreeCloseable)) {
if (isBlacklistedMethod(expression))
isResourceProducer = true;
else
return null; // (a) resource-free closeable: -> null
}
}
if (local == null) {
FakedTrackingVariable tracker = null;
if (useAnnotations && (expression.bits & RestrictiveFlagMASK) == Binding.FIELD) {
// field read
FieldBinding fieldBinding = ((Reference) expression).lastFieldBinding();
long owningBits = 0;
if (fieldBinding != null) {
owningBits = fieldBinding.getAnnotationTagBits() & TagBits.AnnotationOwningMASK;
}
int status = FlowInfo.UNKNOWN;
if (owningBits == TagBits.AnnotationOwning) {
status = FlowInfo.NON_NULL;
} else if (owningBits == TagBits.AnnotationNotOwning) {
status = FlowInfo.POTENTIALLY_NULL;
}
tracker = new FakedTrackingVariable(local, scope, location, flowInfo, flowContext, status, useAnnotations);
tracker.owningState = NOT_OWNED;
}
return tracker;
}
// analyze by node type:
if (expression instanceof AllocationExpression) {
// allocation expressions already have their tracking variables analyzed by analyseCloseableAllocation(..)
FakedTrackingVariable tracker = ((AllocationExpression) expression).closeTracker;
if (tracker != null && tracker.originalBinding == null) {
// tracker without original binding (unassigned closeable) shouldn't reach here but let's play safe
return null;
}
return tracker;
} else if (expression instanceof MessageSend
|| expression instanceof ArrayReference)
{
int initialNullStatus = 0;
if (isBlacklistedMethod(expression)) {
initialNullStatus = FlowInfo.NULL;
} else if (useAnnotations) {
initialNullStatus = getNullStatusFromMessageSend(expression);
}
if (initialNullStatus != 0)
return new FakedTrackingVariable(local, location, flowInfo, flowContext, initialNullStatus, useAnnotations);
// we *might* be responsible for the resource obtained
FakedTrackingVariable tracker = new FakedTrackingVariable(local, location, flowInfo, flowContext, FlowInfo.POTENTIALLY_NULL, useAnnotations); // shed some doubt
if (!isResourceProducer && !useAnnotations)
tracker.globalClosingState |= SHARED_WITH_OUTSIDE;
return tracker;
} else if (
(expression.bits & RestrictiveFlagMASK) == Binding.FIELD
||((expression instanceof QualifiedNameReference)
&& ((QualifiedNameReference) expression).isFieldAccess()))
{
if (!useAnnotations) {
// responsibility for this resource probably lies at a higher level
FakedTrackingVariable tracker = new FakedTrackingVariable(local, location, flowInfo, flowContext, FlowInfo.UNKNOWN, useAnnotations);
tracker.globalClosingState |= OWNED_BY_OUTSIDE;
// leave state as UNKNOWN, the bit OWNED_BY_OUTSIDE will prevent spurious warnings
return tracker;
}
}
if (local.closeTracker != null)
// (c): inner has already been analyzed: -> re-use track var
return local.closeTracker;
FakedTrackingVariable newTracker = new FakedTrackingVariable(local, location, flowInfo, flowContext, FlowInfo.UNKNOWN, useAnnotations);
LocalVariableBinding rhsLocal = expression.localVariableBinding();
if (rhsLocal != null && rhsLocal.isParameter()) {
newTracker.globalClosingState |= OWNED_BY_OUTSIDE;
}
return newTracker;
}
private static boolean isBlacklistedMethod(Expression expression) {
if (expression instanceof MessageSend) {
MethodBinding method = ((MessageSend) expression).binding;
if (method != null && method.isValidBinding())
// for all methods in java.nio.file.Files that return a resource (Stream) it really needs closing
return CharOperation.equals(method.declaringClass.compoundName, TypeConstants.JAVA_NIO_FILE_FILES);
}
return false;
}
/* pre: usesOwningAnnotations. */
protected static int getNullStatusFromMessageSend(Expression expression) {
if (expression instanceof MessageSend) {
if ((((MessageSend) expression).binding.tagBits & TagBits.AnnotationNotOwning) != 0)
return FlowInfo.NON_NULL;
return FlowInfo.NULL; // per default assume responsibility to close
}
return 0;
}
public static void cleanUpAfterAssignment(BlockScope currentScope, int lhsBits, Expression expression) {
// remove all remaining track vars with no original binding
// unwrap uninteresting nodes:
while (true) {
if (expression instanceof Assignment)
expression = ((Assignment)expression).expression;
else if (expression instanceof CastExpression)
expression = ((CastExpression) expression).expression;
else
break;
}
if (expression instanceof AllocationExpression) {
FakedTrackingVariable tracker = ((AllocationExpression) expression).closeTracker;
if (tracker != null && tracker.originalBinding == null) {
tracker.withdraw();
((AllocationExpression) expression).closeTracker = null;
}
} else if (expression instanceof MessageSend) {
FakedTrackingVariable tracker = ((MessageSend) expression).closeTracker;
if (tracker != null && tracker.originalBinding == null) {
tracker.withdraw();
((MessageSend) expression).closeTracker = null;
}
} else {
// assignment passing a local into a field?
LocalVariableBinding local = expression.localVariableBinding();
if (local != null
&& ((lhsBits & Binding.FIELD) != 0)
&& !currentScope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled
&& local.closeTracker != null) {
local.closeTracker.withdraw(); // TODO: may want to use local.closeTracker.markPassedToOutside(..,true)
}
}
}
/**
* Unassigned closeables are not visible beyond their enclosing statement, immediately report and remove after each statement.
* @param returnMissingOwning at a return statement this signals {@code true} when the enclosing method lacks an {@code @Owning} annotation.
*/
public static void cleanUpUnassigned(BlockScope scope, ASTNode location, FlowInfo flowInfo, boolean returnMissingOwning) {
if (!scope.hasResourceTrackers()) return;
boolean useAnnotations = scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled;
location.traverse(new ASTVisitor() {
@Override
public boolean visit(MessageSend messageSend, BlockScope skope) {
FakedTrackingVariable closeTracker = messageSend.closeTracker;
handle(closeTracker, flowInfo, messageSend, skope);
return true;
}
@Override
public boolean visit(AllocationExpression allocation, BlockScope skope) {
if (handle(allocation.closeTracker, flowInfo, allocation, skope))
allocation.closeTracker = null;
return true;
}
/** @return has the tracker been withdrawn? */
protected boolean handle(FakedTrackingVariable closeTracker, FlowInfo flow, ASTNode loc, BlockScope skope) {
if (closeTracker != null && closeTracker.originalBinding == null && closeTracker.originalFieldBinding == null) {
int nullStatus = closeTracker.riskyNullStatusAt(flow);
if (nullStatus != 0) {
int reportFlag = closeTracker.reportError(skope.problemReporter(), loc, nullStatus);
closeTracker.markAllConnected(ftv -> ftv.globalClosingState |= reportFlag);
} else if (returnMissingOwning && useAnnotations) {
skope.problemReporter().shouldMarkMethodAsOwning(location);
}
closeTracker.withdraw();
return true;
}
return false;
}
},
scope);
}
/** Answer wither the given type binding is a subtype of java.lang.AutoCloseable. */
public static boolean isAnyCloseable(TypeBinding typeBinding) {
return typeBinding instanceof ReferenceBinding
&& ((ReferenceBinding)typeBinding).hasTypeBit(TypeIds.BitAutoCloseable|TypeIds.BitCloseable);
}
/** Answer wither the given type binding is a subtype of java.lang.AutoCloseable. */
public static boolean isCloseableNotWhiteListed(TypeBinding typeBinding) {
if (typeBinding instanceof ReferenceBinding) {
ReferenceBinding referenceBinding = (ReferenceBinding)typeBinding;
return referenceBinding.hasTypeBit(TypeIds.BitAutoCloseable|TypeIds.BitCloseable)
&& !referenceBinding.hasTypeBit(TypeIds.BitResourceFreeCloseable);
}
return false;
}
public int findMostSpecificStatus(FlowInfo flowInfo, BlockScope currentScope, BlockScope locationScope) {
int status = FlowInfo.UNKNOWN;
FakedTrackingVariable currentTracker = this;
// loop as to consider wrappers (per white list) encapsulating an inner resource.
while (currentTracker != null) {
LocalVariableBinding currentVar = currentTracker.binding;
int currentStatus = getNullStatusAggressively(currentVar, flowInfo);
if (locationScope != null) // only check at method exit points
currentStatus = mergeCloseStatus(locationScope, currentStatus, currentVar, currentScope);
if (currentStatus == FlowInfo.NON_NULL) {
status = currentStatus;
break; // closed -> stop searching
} else if (status == FlowInfo.NULL || status == FlowInfo.UNKNOWN) {
status = currentStatus; // improved although not yet safe -> keep searching for better
}
currentTracker = currentTracker.innerTracker;
}
return status;
}
/**
* Get the null status looking even into unreachable flows
* @return one of the constants FlowInfo.{NULL,POTENTIALLY_NULL,POTENTIALLY_NON_NULL,NON_NULL}.
*/
private int getNullStatusAggressively(LocalVariableBinding local, FlowInfo flowInfo) {
if (flowInfo == FlowInfo.DEAD_END) {
return FlowInfo.UNKNOWN;
}
int reachMode = flowInfo.reachMode();
int status = 0;
try {
// unreachable flowInfo is too shy in reporting null-issues, temporarily forget reachability:
if (reachMode != FlowInfo.REACHABLE)
flowInfo.tagBits &= ~FlowInfo.UNREACHABLE;
status = flowInfo.nullStatus(local);
if (TEST_372319) { // see https://bugs.eclipse.org/372319
try {
Thread.sleep(5); // increase probability of concurrency bug
} catch (InterruptedException e) { /* nop */ }
}
} finally {
// reset
flowInfo.tagBits |= reachMode;
}
// at this point some combinations are not useful so flatten to a single bit:
if ((status & FlowInfo.NULL) != 0) {
if ((status & (FlowInfo.NON_NULL | FlowInfo.POTENTIALLY_NON_NULL)) != 0)
return FlowInfo.POTENTIALLY_NULL; // null + doubt = pot null
return FlowInfo.NULL;
} else if ((status & FlowInfo.NON_NULL) != 0) {
if ((status & FlowInfo.POTENTIALLY_NULL) != 0)
return FlowInfo.POTENTIALLY_NULL; // non-null + doubt = pot null
return FlowInfo.NON_NULL;
} else if ((status & FlowInfo.POTENTIALLY_NULL) != 0) {
return FlowInfo.POTENTIALLY_NULL;
} else if (status == FlowInfo.UNKNOWN) {
// if unassigned resource (not having an originalBinding) is not withdrawn it is unclosed:
if (this.originalBinding == null && this.originalFieldBinding == null)
return FlowInfo.NULL;
}
return status;
}
public int mergeCloseStatus(BlockScope currentScope, int status, LocalVariableBinding local, BlockScope outerScope) {
// get the most suitable null status representing whether resource 'binding' has been closed
// start at 'currentScope' and potentially travel out until 'outerScope'
// at each scope consult any recorded 'finallyInfo'.
if (status != FlowInfo.NON_NULL) {
if (currentScope.finallyInfo != null) {
int finallyStatus = currentScope.finallyInfo.nullStatus(local);
if (finallyStatus == FlowInfo.NON_NULL)
return finallyStatus;
if (finallyStatus != FlowInfo.NULL && currentScope.finallyInfo.hasNullInfoFor(local)) // neither is NON_NULL, but not both are NULL => call it POTENTIALLY_NULL
status = FlowInfo.POTENTIALLY_NULL;
}
if (currentScope != outerScope && currentScope.parent instanceof BlockScope)
return mergeCloseStatus(((BlockScope) currentScope.parent), status, local, outerScope);
}
return status;
}
/** Mark that this resource is closed locally. */
public void markClose(FlowInfo flowInfo, FlowContext flowContext) {
markAllConnected(current -> {
flowInfo.markAsDefinitelyNonNull(current.binding);
current.globalClosingState |= CLOSE_SEEN;
flowContext.markFinallyNullStatus(current.binding, FlowInfo.NON_NULL);
});
}
public void markNullStatus(FlowInfo flowInfo, FlowContext flowContext, int status) {
markAllConnected(current -> {
flowInfo.markNullStatus(current.binding, status);
flowContext.markFinallyNullStatus(current.binding, status);
});
}
public void markOwnedByOutside(FlowInfo flowInfo, FlowContext flowContext) {
markAllConnected(current -> {
flowInfo.markAsDefinitelyNonNull(current.binding);
flowContext.markFinallyNullStatus(current.binding, FlowInfo.NON_NULL);
current.globalClosingState = FakedTrackingVariable.OWNED_BY_OUTSIDE;
});
}
public void markAllConnected(Consumer operation) {
FakedTrackingVariable current = this;
do {
operation.accept(current);
current = current.innerTracker;
} while (current != null);
current = this.outerTracker;
while (current != null) {
operation.accept(current);
current = current.outerTracker;
}
}
/** Mark that this resource is closed from a nested method (inside a local class). */
public void markClosedInNestedMethod() {
this.globalClosingState |= CLOSED_IN_NESTED_METHOD;
}
public boolean isClosedInNestedMethod() {
return (this.globalClosingState & CLOSED_IN_NESTED_METHOD) != 0;
}
/** Mark that this resource is closed from a try-with-resource with the tracking variable being effectively final). */
public void markClosedEffectivelyFinal() {
this.globalClosingState |= TWR_EFFECTIVELY_FINAL;
}
/**
* Mark that this resource is passed to some outside code
* (as argument to a method/ctor call or as a return value from the current method),
* and thus should be considered as potentially closed.
* @param owned should the resource be considered owned by some outside?
*/
public static FlowInfo markPassedToOutside(BlockScope scope, Expression expression, FlowInfo flowInfo, FlowContext flowContext, boolean owned) {
FakedTrackingVariable trackVar = getCloseTrackingVariable(expression, flowInfo, flowContext,
scope.compilerOptions().isAnnotationBasedResourceAnalysisEnabled);
if (trackVar != null) {
// insert info that the tracked resource *may* be closed (by the target method, i.e.)
FlowInfo infoResourceIsClosed = owned ? flowInfo : flowInfo.copy();
int flag = owned ? OWNED_BY_OUTSIDE : SHARED_WITH_OUTSIDE;
trackVar.markAllConnected(ftv -> {
ftv.globalClosingState |= flag;
if (scope.methodScope() != ftv.methodScope)
ftv.globalClosingState |= CLOSED_IN_NESTED_METHOD;
ftv.markNullStatus(flowInfo, flowContext, FlowInfo.NON_NULL);
});
if (owned) {
return infoResourceIsClosed; // don't let downstream signal any problems on this flow
} else {
return FlowInfo.conditional(flowInfo, infoResourceIsClosed).unconditionalCopy(); // only report potential problems on this flow
}
}
return flowInfo;
}
public static void markForeachElementVar(LocalDeclaration local) {
if (local.binding != null && local.binding.closeTracker != null) {
local.binding.closeTracker.globalClosingState |= FOREACH_ELEMENT_VAR;
}
}
/**
* Iterator for a set of FakedTrackingVariable, which dispenses the elements
* according to the priorities defined by enum {@link Stage}.
* Resources whose outer is owned by an enclosing scope are never answered,
* unless we are analysing on behalf of an exit (return/throw).
*/
public static class IteratorForReporting implements Iterator {
private final Set varSet;
private final Scope scope;
private final boolean atExit;
private Stage stage;
private Iterator iterator;
private FakedTrackingVariable next;
enum Stage {
/** 1. prio: all top-level resources, ie., resources with no outer. */
OuterLess,
/** 2. prio: resources whose outer has already been processed (element of the same varSet). */
InnerOfProcessed,
/** 3. prio: resources whose outer is not owned by any enclosing scope. */
InnerOfNotEnclosing,
/** 4. prio: when analysing on behalf of an exit point: anything not picked before. */
AtExit
}
public IteratorForReporting(List variables, Scope scope, boolean atExit) {
this.varSet = new HashSet<>(variables);
this.scope = scope;
this.atExit = atExit;
setUpForStage(Stage.OuterLess);
}
@Override
public boolean hasNext() {
FakedTrackingVariable trackingVar;
switch (this.stage) {
case OuterLess:
while (this.iterator.hasNext()) {
trackingVar = this.iterator.next();
if (trackingVar.outerTracker == null)
return found(trackingVar);
}
setUpForStage(Stage.InnerOfProcessed);
//$FALL-THROUGH$
case InnerOfProcessed:
while (this.iterator.hasNext()) {
trackingVar = this.iterator.next();
FakedTrackingVariable outer = trackingVar.outerTracker;
if (outer.binding.declaringScope == this.scope && !this.varSet.contains(outer))
return found(trackingVar);
}
setUpForStage(Stage.InnerOfNotEnclosing);
//$FALL-THROUGH$
case InnerOfNotEnclosing:
searchAlien: while (this.iterator.hasNext()) {
trackingVar = this.iterator.next();
FakedTrackingVariable outer = trackingVar.outerTracker;
if (!this.varSet.contains(outer)) {
Scope outerTrackerScope = outer.binding.declaringScope;
Scope currentScope = this.scope;
while ((currentScope = currentScope.parent) instanceof BlockScope) {
if (outerTrackerScope == currentScope)
break searchAlien;
}
return found(trackingVar);
}
}
setUpForStage(Stage.AtExit);
//$FALL-THROUGH$
case AtExit:
if (this.atExit && this.iterator.hasNext())
return found(this.iterator.next());
return false;
default: throw new IllegalStateException("Unexpected Stage "+this.stage); //$NON-NLS-1$
}
}
private boolean found(FakedTrackingVariable trackingVar) {
this.iterator.remove();
this.next = trackingVar;
return true;
}
private void setUpForStage(Stage nextStage) {
this.iterator = this.varSet.iterator();
this.stage = nextStage;
}
@Override
public FakedTrackingVariable next() {
return this.next;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Answer true if we know for sure that no resource is bound to this variable
* at the point of 'flowInfo'.
*/
public boolean hasDefinitelyNoResource(FlowInfo flowInfo) {
if (this.originalBinding == null) return false; // shouldn't happen but keep quiet.
if (flowInfo.isDefinitelyNull(this.originalBinding)) {
return true;
}
if (!(flowInfo.isDefinitelyAssigned(this.originalBinding)
|| flowInfo.isPotentiallyAssigned(this.originalBinding))) {
return true;
}
return false;
}
public boolean isClosedInFinallyOfEnclosing(BlockScope scope) {
BlockScope currentScope = scope;
while (true) {
if (currentScope.finallyInfo != null
&& currentScope.finallyInfo.isDefinitelyNonNull(this.binding)) {
return true; // closed in enclosing finally
}
if (!(currentScope.parent instanceof BlockScope)) {
return false;
}
currentScope = (BlockScope) currentScope.parent;
}
}
public boolean isClosedInFinally() {
return this.blockScope != null && isClosedInFinallyOfEnclosing(this.blockScope);
}
/**
* If current is the same as 'returnedResource' or a wrapper thereof,
* mark as reported and return true, otherwise false.
*
* When using {@code @Owning} annotation, do not mark as reported, to proceed to precise analysis
*/
public boolean isResourceBeingReturned(FakedTrackingVariable returnedResource, boolean useOwningAnnotation) {
FakedTrackingVariable current = this;
do {
if (current == returnedResource) {
if (!useOwningAnnotation)
this.globalClosingState |= REPORTED_DEFINITIVE_LEAK;
return true;
}
current = current.innerTracker;
} while (current != null);
return false;
}
public void withdraw() {
// must unregister at the declaringScope, note that twr resources are owned by the scope enclosing the twr
this.binding.declaringScope.removeTrackingVar(this);
if (this.acquisition != null && this.acquisition.closeTracker == this)
this.acquisition.closeTracker = null;
}
public void recordErrorLocation(ASTNode location, int nullStatus) {
if (isNotOwned()) {
return;
}
if (this.recordedLocations == null)
this.recordedLocations = new HashMap<>();
this.recordedLocations.put(location, Integer.valueOf(nullStatus));
}
public boolean reportRecordedErrors(Scope scope, int mergedStatus, boolean atDeadEnd) {
FakedTrackingVariable current = this;
while (current.globalClosingState == 0) {
current = current.innerTracker;
if (current == null) {
// no relevant state found -> report:
if (atDeadEnd && neverClosedAtLocations())
mergedStatus = FlowInfo.NULL;
if ((mergedStatus & (FlowInfo.NULL|FlowInfo.POTENTIALLY_NULL|FlowInfo.POTENTIALLY_NON_NULL)) != 0) {
reportError(scope.problemReporter(), null, mergedStatus);
return true;
} else {
break;
}
}
}
boolean hasReported = false;
if (this.recordedLocations != null) {
Iterator> locations = this.recordedLocations.entrySet().iterator();
int reportFlags = 0;
while (locations.hasNext()) {
Entry entry = locations.next();
reportFlags |= reportError(scope.problemReporter(), entry.getKey(), entry.getValue().intValue());
hasReported = true;
}
if (reportFlags != 0) {
// after all locations have been reported, mark as reported to prevent duplicate report via an outer wrapper
current = this;
do {
current.globalClosingState |= reportFlags;
} while ((current = current.innerTracker) != null);
}
}
return hasReported;
}
public boolean hasRecordedLocations() {
return this.recordedLocations != null;
}
private boolean neverClosedAtLocations() {
if (this.recordedLocations != null) {
for (Object value : this.recordedLocations.values())
if (!value.equals(FlowInfo.NULL))
return false;
}
return true;
}
public int reportError(ProblemReporter problemReporter, ASTNode location, int nullStatus) {
if (isNotOwned()) {
return 0; // TODO: should we still propagate some flags??
}
// which degree of problem?
boolean isPotentialProblem = false;
if (nullStatus == FlowInfo.NULL) {
if ((this.globalClosingState & CLOSED_IN_NESTED_METHOD) != 0)
isPotentialProblem = true;
} else if ((nullStatus & (FlowInfo.POTENTIALLY_NULL|FlowInfo.POTENTIALLY_NON_NULL)) != 0) {
isPotentialProblem = true;
}
// report:
if (isPotentialProblem) {
if ((this.globalClosingState & (REPORTED_POTENTIAL_LEAK|REPORTED_DEFINITIVE_LEAK)) != 0)
return 0;
// if ((this.globalClosingState & (ACQUIRED_FROM_OUTSIDE)) != 0
// && location instanceof ReturnStatement
// && ((ReturnStatement) location).expression == this.acquisition)
// return 0; // directly returning a resource acquired from a message send: don't assume responsibility
problemReporter.potentiallyUnclosedCloseable(this, location);
} else {
if ((this.globalClosingState & (REPORTED_DEFINITIVE_LEAK)) != 0)
return 0;
problemReporter.unclosedCloseable(this, location);
}
// propagate flag to inners:
int reportFlag = isPotentialProblem ? REPORTED_POTENTIAL_LEAK : REPORTED_DEFINITIVE_LEAK;
if (location == null) { // if location != null flags will be set after the loop over locations
markAllConnected(current -> current.globalClosingState |= reportFlag);
}
return reportFlag;
}
public void reportExplicitClosing(ProblemReporter problemReporter) {
if (this.originalBinding != null && this.originalBinding.isParameter())
return;
if ((this.globalClosingState & CLOSE_SEEN) == 0)
return;
if ((this.globalClosingState & (TWR_EFFECTIVELY_FINAL|OWNED_BY_OUTSIDE|REPORTED_EXPLICIT_CLOSE|FOREACH_ELEMENT_VAR)) == 0) { // can't use t-w-r for OWNED_BY_OUTSIDE
if (this.originalFieldBinding != null && this.blockScope instanceof MethodScope) {
AbstractMethodDeclaration method = ((MethodScope) this.blockScope).referenceMethod();
if (method.binding != null && method.binding.isClosingMethod()) {
return; // this is the canonical close method, nothing to warn about.
}
}
this.globalClosingState |= REPORTED_EXPLICIT_CLOSE;
problemReporter.explicitlyClosedAutoCloseable(this);
}
}
public String nameForReporting(ASTNode location, ReferenceContext referenceContext) {
if (this.name == UNASSIGNED_CLOSEABLE_NAME) {
if (location != null && referenceContext != null) {
CompilationResult compResult = referenceContext.compilationResult();
if (compResult != null) {
int[] lineEnds = compResult.getLineSeparatorPositions();
int resourceLine = Util.getLineNumber(this.sourceStart, lineEnds , 0, lineEnds.length-1);
int reportLine = Util.getLineNumber(location.sourceStart, lineEnds , 0, lineEnds.length-1);
if (resourceLine != reportLine) {
char[] replacement = Integer.toString(resourceLine).toCharArray();
return String.valueOf(CharOperation.replace(UNASSIGNED_CLOSEABLE_NAME_TEMPLATE, TEMPLATE_ARGUMENT, replacement));
}
}
}
}
if (this.originalFieldBinding != null)
return String.valueOf(CharOperation.concat(ConstantPool.This, this.name, '.'));
return String.valueOf(this.name);
}
public void markAsShared() {
this.globalClosingState |= SHARED_WITH_OUTSIDE;
}
public boolean isShared() {
return (this.globalClosingState & SHARED_WITH_OUTSIDE) != 0;
}
protected boolean isNotOwned() {
if (this.useAnnotations)
return this.owningState < 0;
return (this.globalClosingState & OWNED_BY_OUTSIDE) != 0;
}
public boolean closeSeen() {
return (this.globalClosingState & CLOSE_SEEN) != 0;
}
}