more reflection support
git-svn-id: https://wala.svn.sourceforge.net/svnroot/wala/trunk@2673 f5eafffb-2e1d-0410-98e4-8ec43c5233c4
This commit is contained in:
parent
3085b16ae6
commit
660f3a6707
|
@ -30,31 +30,13 @@ class ClassNewInstanceContextSelector implements ContextSelector {
|
|||
public ClassNewInstanceContextSelector() {
|
||||
}
|
||||
|
||||
public boolean allSitesDispatchIdentically(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean contextIsIrrelevant(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Context getCalleeTarget(CGNode caller, CallSiteReference site, IMethod callee, InstanceKey receiver) {
|
||||
if (mayUnderstand(caller, site, callee, receiver)) {
|
||||
if (callee.getReference().equals(ClassNewInstanceContextInterpreter.CLASS_NEW_INSTANCE_REF) && isTypeConstant(receiver)) {
|
||||
IClass c = (IClass) ((ConstantKey) receiver).getValue();
|
||||
return new JavaTypeContext(new PointType(c));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This object may understand a dispatch to Class.newInstance() when the class is a class constant.
|
||||
*/
|
||||
public boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
if (targetMethod.getReference().equals(ClassNewInstanceContextInterpreter.CLASS_NEW_INSTANCE_REF) && isTypeConstant(instance)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isTypeConstant(InstanceKey instance) {
|
||||
if (instance instanceof ConstantKey) {
|
||||
|
|
|
@ -46,38 +46,5 @@ class FactoryContextSelector implements ContextSelector {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* @see com.ibm.wala.ipa.callgraph.ContextSelector#mayUnderstand(com.ibm.wala.ipa.callgraph.CGNode, com.ibm.wala.classLoader.CallSiteReference, com.ibm.wala.classLoader.IMethod, com.ibm.wala.ipa.callgraph.propagation.InstanceKey)
|
||||
*/
|
||||
public boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
if (targetMethod == null) {
|
||||
throw new IllegalArgumentException("targetMethod is null");
|
||||
}
|
||||
if (targetMethod.isSynthetic()) {
|
||||
SyntheticMethod s = (SyntheticMethod) targetMethod;
|
||||
if (s.isFactoryMethod()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* @see com.ibm.wala.ipa.callgraph.ContextSelector#getBoundOnNumberOfTargets(com.ibm.wala.ipa.callgraph.CGNode, com.ibm.wala.classLoader.CallSiteReference, com.ibm.wala.classLoader.IMethod)
|
||||
*/
|
||||
public int getBoundOnNumberOfTargets(CGNode caller, CallSiteReference site, IMethod callee) {
|
||||
if (callee == null) {
|
||||
throw new IllegalArgumentException("callee is null");
|
||||
}
|
||||
if (callee.isSynthetic()) {
|
||||
SyntheticMethod s = (SyntheticMethod) callee;
|
||||
if (s.isFactoryMethod()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,14 +35,6 @@ class ForNameContextSelector implements ContextSelector {
|
|||
public ForNameContextSelector() {
|
||||
}
|
||||
|
||||
public boolean allSitesDispatchIdentically(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean contextIsIrrelevant(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the {@link CallSiteReference} invokes Class.forName(s) and s is a string constant, return a
|
||||
* {@link JavaTypeContext} representing the type named by s, if we can resolve it in the {@link IClassHierarchy}.
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2008 IBM Corporation.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IBM Corporation - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.ibm.wala.analysis.reflection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
import com.ibm.wala.cfg.ControlFlowGraph;
|
||||
import com.ibm.wala.cfg.InducedCFG;
|
||||
import com.ibm.wala.classLoader.CallSiteReference;
|
||||
import com.ibm.wala.classLoader.IClass;
|
||||
import com.ibm.wala.classLoader.IMethod;
|
||||
import com.ibm.wala.classLoader.NewSiteReference;
|
||||
import com.ibm.wala.ipa.callgraph.CGNode;
|
||||
import com.ibm.wala.ipa.callgraph.propagation.SSAContextInterpreter;
|
||||
import com.ibm.wala.ipa.summaries.SyntheticIR;
|
||||
import com.ibm.wala.ssa.DefUse;
|
||||
import com.ibm.wala.ssa.IR;
|
||||
import com.ibm.wala.ssa.ISSABasicBlock;
|
||||
import com.ibm.wala.ssa.SSAInstruction;
|
||||
import com.ibm.wala.ssa.SSALoadClassInstruction;
|
||||
import com.ibm.wala.ssa.SSAOptions;
|
||||
import com.ibm.wala.ssa.SSAReturnInstruction;
|
||||
import com.ibm.wala.types.FieldReference;
|
||||
import com.ibm.wala.types.TypeReference;
|
||||
import com.ibm.wala.util.collections.EmptyIterator;
|
||||
import com.ibm.wala.util.debug.Assertions;
|
||||
|
||||
/**
|
||||
* {@link SSAContextInterpreter} specialized to interpret Object.getClass() in a {@link JavaTypeContext}
|
||||
*
|
||||
* @author sjfink
|
||||
*/
|
||||
public class GetClassContextInterpeter implements SSAContextInterpreter {
|
||||
|
||||
public IR getIR(CGNode node) {
|
||||
if (node == null) {
|
||||
throw new IllegalArgumentException("node is null");
|
||||
}
|
||||
if (Assertions.verifyAssertions) {
|
||||
Assertions._assert(understands(node));
|
||||
}
|
||||
IR result = makeIR(node.getMethod(), (JavaTypeContext) node.getContext());
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getNumberOfStatements(CGNode node) {
|
||||
if (Assertions.verifyAssertions) {
|
||||
Assertions._assert(understands(node));
|
||||
}
|
||||
return getIR(node).getInstructions().length;
|
||||
}
|
||||
|
||||
public boolean understands(CGNode node) {
|
||||
if (node == null) {
|
||||
throw new IllegalArgumentException("node is null");
|
||||
}
|
||||
if (!(node.getContext() instanceof JavaTypeContext)) {
|
||||
return false;
|
||||
}
|
||||
return node.getMethod().getReference().equals(GetClassContextSelector.GET_CLASS);
|
||||
}
|
||||
|
||||
public Iterator<NewSiteReference> iterateNewSites(CGNode node) {
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
|
||||
public Iterator<CallSiteReference> iterateCallSites(CGNode node) {
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
|
||||
private SSAInstruction[] makeStatements(JavaTypeContext context) {
|
||||
ArrayList<SSAInstruction> statements = new ArrayList<SSAInstruction>();
|
||||
int nextLocal = 2;
|
||||
int retValue = nextLocal++;
|
||||
TypeReference tr = context.getType().getTypeReference();
|
||||
if (tr != null) {
|
||||
SSALoadClassInstruction l = new SSALoadClassInstruction(retValue, tr);
|
||||
statements.add(l);
|
||||
SSAReturnInstruction R = new SSAReturnInstruction(retValue, false);
|
||||
statements.add(R);
|
||||
}
|
||||
SSAInstruction[] result = new SSAInstruction[statements.size()];
|
||||
Iterator<SSAInstruction> it = statements.iterator();
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = it.next();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private IR makeIR(IMethod method, JavaTypeContext context) {
|
||||
SSAInstruction instrs[] = makeStatements(context);
|
||||
return new SyntheticIR(method, context, new InducedCFG(instrs, method, context), instrs, SSAOptions.defaultOptions(), null);
|
||||
}
|
||||
|
||||
public boolean recordFactoryType(CGNode node, IClass klass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Iterator<FieldReference> iterateFieldsRead(CGNode node) {
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
|
||||
public Iterator<FieldReference> iterateFieldsWritten(CGNode node) {
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
|
||||
public ControlFlowGraph<ISSABasicBlock> getCFG(CGNode N) {
|
||||
return getIR(N).getControlFlowGraph();
|
||||
}
|
||||
|
||||
public DefUse getDU(CGNode node) {
|
||||
return new DefUse(getIR(node));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*******************************************************************************
|
||||
* Copyright (c) 2008 IBM Corporation.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IBM Corporation - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.ibm.wala.analysis.reflection;
|
||||
|
||||
import com.ibm.wala.analysis.typeInference.PointType;
|
||||
import com.ibm.wala.classLoader.CallSiteReference;
|
||||
import com.ibm.wala.classLoader.IMethod;
|
||||
import com.ibm.wala.ipa.callgraph.CGNode;
|
||||
import com.ibm.wala.ipa.callgraph.Context;
|
||||
import com.ibm.wala.ipa.callgraph.ContextSelector;
|
||||
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
|
||||
import com.ibm.wala.types.MethodReference;
|
||||
import com.ibm.wala.types.TypeReference;
|
||||
|
||||
/**
|
||||
* A {@link ContextSelector} to intercept calls to Object.getClass()
|
||||
*
|
||||
* @author sjfink
|
||||
*/
|
||||
class GetClassContextSelector implements ContextSelector {
|
||||
|
||||
public final static MethodReference GET_CLASS = MethodReference.findOrCreate(TypeReference.JavaLangObject, "getClass", "()Ljava/lang/Class;");
|
||||
|
||||
public GetClassContextSelector() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @see com.ibm.wala.ipa.callgraph.ContextSelector#getCalleeTarget(com.ibm.wala.ipa.callgraph.CGNode,
|
||||
* com.ibm.wala.classLoader.CallSiteReference, com.ibm.wala.classLoader.IMethod,
|
||||
* com.ibm.wala.ipa.callgraph.propagation.InstanceKey)
|
||||
*/
|
||||
public Context getCalleeTarget(CGNode caller, CallSiteReference site, IMethod callee, InstanceKey receiver) {
|
||||
if (callee.getReference().equals(GET_CLASS)) {
|
||||
return new JavaTypeContext(new PointType(receiver.getConcreteType()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -41,14 +41,6 @@ class JavaLangClassContextSelector implements ContextSelector {
|
|||
public JavaLangClassContextSelector() {
|
||||
}
|
||||
|
||||
public boolean allSitesDispatchIdentically(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean contextIsIrrelevant(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the {@link CallSiteReference} invokes a method we understand and c is a type constant, return a
|
||||
* {@link JavaTypeContext} representing the type named by s, if we can resolve it in the {@link IClassHierarchy}.
|
||||
|
@ -73,7 +65,7 @@ class JavaLangClassContextSelector implements ContextSelector {
|
|||
/**
|
||||
* This object may understand a dispatch to Class.getContructor when the receiver is a type constant.
|
||||
*/
|
||||
public boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
private boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
if (targetMethod.getReference().equals(JavaLangClassContextInterpreter.GET_CONSTRUCTOR) && getTypeConstant(instance) != null) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ public class ReflectionContextInterpreter extends DelegatingSSAContextInterprete
|
|||
|
||||
private ReflectionContextInterpreter(IClassHierarchy cha, AnalysisOptions options, AnalysisCache cache,
|
||||
ReflectionSpecification userSpec) {
|
||||
super(new ReflectiveInvocationInterpreter(), new DelegatingSSAContextInterpreter(
|
||||
new JavaLangClassContextInterpreter(), new DelegatingSSAContextInterpreter(new DelegatingSSAContextInterpreter(
|
||||
new ForNameContextInterpreter(), new ClassNewInstanceContextInterpreter(cha)), new FactoryBypassInterpreter(options,
|
||||
cache, userSpec))));
|
||||
super(new ReflectiveInvocationInterpreter(), new DelegatingSSAContextInterpreter(new DelegatingSSAContextInterpreter(
|
||||
new GetClassContextInterpeter(), new JavaLangClassContextInterpreter()), new DelegatingSSAContextInterpreter(
|
||||
new DelegatingSSAContextInterpreter(new ForNameContextInterpreter(), new ClassNewInstanceContextInterpreter(cha)),
|
||||
new FactoryBypassInterpreter(options, cache, userSpec))));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ public class ReflectionContextSelector extends DelegatingContextSelector {
|
|||
*/
|
||||
private ReflectionContextSelector() {
|
||||
super(new ReflectiveInvocationSelector(), new DelegatingContextSelector(new JavaLangClassContextSelector(),
|
||||
new DelegatingContextSelector(new DelegatingContextSelector(new ForNameContextSelector(),
|
||||
new ClassNewInstanceContextSelector()), new FactoryContextSelector())));
|
||||
new DelegatingContextSelector(new DelegatingContextSelector(new DelegatingContextSelector(new ForNameContextSelector(),
|
||||
new GetClassContextSelector()), new ClassNewInstanceContextSelector()), new FactoryContextSelector())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,14 +37,6 @@ class ReflectiveInvocationSelector implements ContextSelector {
|
|||
public ReflectiveInvocationSelector() {
|
||||
}
|
||||
|
||||
public boolean allSitesDispatchIdentically(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean contextIsIrrelevant(CGNode node, CallSiteReference site) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callee target based on the following criteria:
|
||||
* <ol>
|
||||
|
@ -113,7 +105,7 @@ class ReflectiveInvocationSelector implements ContextSelector {
|
|||
/**
|
||||
* This object may understand a dispatch to Constructor.newInstance().
|
||||
*/
|
||||
public boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
private boolean mayUnderstand(CGNode caller, CallSiteReference site, IMethod targetMethod, InstanceKey instance) {
|
||||
if (instance instanceof ConstantKey) {
|
||||
if (targetMethod.getReference().equals(ReflectiveInvocationInterpreter.METHOD_INVOKE) || isConstructorConstant(instance)
|
||||
&& targetMethod.getReference().equals(ReflectiveInvocationInterpreter.CTOR_NEW_INSTANCE)) {
|
||||
|
|
|
@ -409,6 +409,7 @@ public class PDG implements NumberedGraph<Statement> {
|
|||
if (Assertions.verifyAssertions && dOptions.isIgnoreExceptions()) {
|
||||
Assertions._assert(!s.getKind().equals(Kind.EXC_RET_CALLER));
|
||||
}
|
||||
|
||||
ValueNumberCarrier a = (ValueNumberCarrier) s;
|
||||
for (Iterator<SSAInstruction> it2 = DU.getUses(a.getValueNumber()); it2.hasNext();) {
|
||||
SSAInstruction use = it2.next();
|
||||
|
|
|
@ -27,6 +27,8 @@ import com.ibm.wala.ipa.slicer.Slicer.DataDependenceOptions;
|
|||
import com.ibm.wala.ipa.slicer.Statement.Kind;
|
||||
import com.ibm.wala.ssa.IR;
|
||||
import com.ibm.wala.ssa.SSAAbstractInvokeInstruction;
|
||||
import com.ibm.wala.types.MethodReference;
|
||||
import com.ibm.wala.types.TypeReference;
|
||||
import com.ibm.wala.util.collections.CompoundIterator;
|
||||
import com.ibm.wala.util.collections.EmptyIterator;
|
||||
import com.ibm.wala.util.collections.HashMapFactory;
|
||||
|
@ -337,6 +339,10 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
// a virtual dispatch is just like a cast. No flow.
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
if (dOptions.isTerminateAtCast() && isUninformativeForReflection(pac.getNode())) {
|
||||
// don't track flow for reflection
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
|
||||
// data dependence predecessors
|
||||
for (Iterator<? extends CGNode> it = cg.getPredNodes(N.getNode()); it.hasNext();) {
|
||||
|
@ -356,11 +362,11 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
}
|
||||
}
|
||||
}
|
||||
// if (!cOptions.equals(ControlDependenceOptions.NONE)) {
|
||||
// Statement s = new MethodEntryStatement(N.getNode());
|
||||
// addNode(s);
|
||||
// result.add(s);
|
||||
// }
|
||||
// if (!cOptions.equals(ControlDependenceOptions.NONE)) {
|
||||
// Statement s = new MethodEntryStatement(N.getNode());
|
||||
// addNode(s);
|
||||
// result.add(s);
|
||||
// }
|
||||
return result.iterator();
|
||||
}
|
||||
case HEAP_PARAM_CALLEE: {
|
||||
|
@ -383,11 +389,11 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
}
|
||||
}
|
||||
}
|
||||
// if (!cOptions.equals(ControlDependenceOptions.NONE)) {
|
||||
// Statement s = new MethodEntryStatement(N.getNode());
|
||||
// addNode(s);
|
||||
// result.add(s);
|
||||
// }
|
||||
// if (!cOptions.equals(ControlDependenceOptions.NONE)) {
|
||||
// Statement s = new MethodEntryStatement(N.getNode());
|
||||
// addNode(s);
|
||||
// result.add(s);
|
||||
// }
|
||||
return result.iterator();
|
||||
}
|
||||
case METHOD_ENTRY:
|
||||
|
@ -420,6 +426,9 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
}
|
||||
|
||||
public Iterator<? extends Statement> getSuccNodes(Statement N) {
|
||||
if (dOptions.isTerminateAtCast() && isUninformativeForReflection(N.getNode())) {
|
||||
return EmptyIterator.instance();
|
||||
}
|
||||
addPDGStatementNodes(N.getNode());
|
||||
switch (N.getKind()) {
|
||||
case NORMAL:
|
||||
|
@ -463,7 +472,6 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
IntSet indices = ir.getCallInstructionIndices(site);
|
||||
for (IntIterator ii = indices.intIterator(); ii.hasNext();) {
|
||||
int i = ii.next();
|
||||
// SSAAbstractInvokeInstruction call = (SSAAbstractInvokeInstruction) ir.getInstructions()[i];
|
||||
Statement s = new ExceptionalReturnCaller(caller, i);
|
||||
addNode(s);
|
||||
result.add(s);
|
||||
|
@ -485,7 +493,6 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
IntSet indices = ir.getCallInstructionIndices(site);
|
||||
for (IntIterator ii = indices.intIterator(); ii.hasNext();) {
|
||||
int i = ii.next();
|
||||
// SSAAbstractInvokeInstruction call = (SSAAbstractInvokeInstruction) ir.getInstructions()[i];
|
||||
Statement s = new NormalReturnCaller(caller, i);
|
||||
addNode(s);
|
||||
result.add(s);
|
||||
|
@ -529,6 +536,10 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
// a virtual dispatch is just like a cast.
|
||||
continue;
|
||||
}
|
||||
if (dOptions.isTerminateAtCast() && isUninformativeForReflection(t)) {
|
||||
// don't track reflection into reflective invokes
|
||||
continue;
|
||||
}
|
||||
if (call.getUse(i) == pac.getValueNumber()) {
|
||||
Statement s = new ParamCallee(t, i + 1);
|
||||
addNode(s);
|
||||
|
@ -560,6 +571,23 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we cut off flow into node t when processing reflection?
|
||||
*/
|
||||
private boolean isUninformativeForReflection(CGNode t) {
|
||||
if (t.getMethod().getDeclaringClass().getReference().equals(TypeReference.JavaLangReflectMethod)) {
|
||||
return true;
|
||||
}
|
||||
if (t.getMethod().getDeclaringClass().getReference().equals(TypeReference.JavaLangReflectConstructor)) {
|
||||
return true;
|
||||
}
|
||||
if (t.getMethod().getSelector().equals(MethodReference.equalsSelector)) {
|
||||
Assertions.UNREACHABLE();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasEdge(Statement src, Statement dst) {
|
||||
addPDGStatementNodes(src.getNode());
|
||||
addPDGStatementNodes(dst.getNode());
|
||||
|
@ -728,7 +756,7 @@ public class SDG extends AbstractNumberedGraph<Statement> implements ISDG {
|
|||
public DataDependenceOptions getDOptions() {
|
||||
return dOptions;
|
||||
}
|
||||
|
||||
|
||||
public CallGraph getCallGraph() {
|
||||
return cg;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue