package hu.advancedweb.scott.instrumentation.transformation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.MethodNode;
/**
* Tracks local variable scopes and field accesses in test methods
* and passes this information to the next visitor.
*
* @author David Csakvari
*/
public class ScopeExtractorTestMethodVisitor extends MethodNode {
private TreeMap<Integer, Label> lines = new TreeMap<>();
private List<LocalVariableScopeLabels> scopes = new ArrayList<>();
private Set<AccessedField> accessedFields = new LinkedHashSet<>();
private Set<TryCatchBlockLabels> tryCatchBlocks = new HashSet<>();
private StateEmitterTestMethodVisitor next;
private String className;
private int lineNumber;
private Map<Integer, Integer> lineNumerToFirstOccurrenceOfVariables;
public ScopeExtractorTestMethodVisitor(StateEmitterTestMethodVisitor next, final int access, final String name, final String desc, final String signature, final String[] exceptions, String className) {
super(Opcodes.ASM5, access, name, desc, signature, exceptions);
this.next = next;
this.className = className;
lineNumerToFirstOccurrenceOfVariables = new HashMap<>();
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
reset();
return super.visitAnnotation(desc, visible);
}
private void reset() {
scopes.clear();
accessedFields.clear();
lines.clear();
tryCatchBlocks.clear();
}
@Override
public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
super.visitTryCatchBlock(start, end, handler, type);
tryCatchBlocks.add(new TryCatchBlockLabels(start, handler));
}
@Override
public void visitLineNumber(int lineNumber, Label start) {
this.lineNumber = lineNumber;
super.visitLineNumber(lineNumber, start);
lines.put(lineNumber, start);
}
@Override
public void visitVarInsn(int opcode, int var) {
if (VariableType.isStoreOperation(opcode)) {
lineNumerToFirstOccurrenceOfVariables.putIfAbsent(var, this.lineNumber);
}
super.visitVarInsn(opcode, var);
}
@Override
public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
super.visitLocalVariable(name, desc, signature, start, end, index);
scopes.add(new LocalVariableScopeLabels(index, name, desc, start, end));
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
super.visitFieldInsn(opcode, owner, name, desc);
final boolean isStatic;
if (Opcodes.GETSTATIC == opcode || Opcodes.PUTSTATIC == opcode) {
isStatic = true;
} else {
isStatic = false;
}
if (className.equals(owner)) {
accessedFields.add(new AccessedField(owner, name, desc, isStatic));
}
}
@Override
public void visitEnd() {
List<LocalVariableScope> localVariableScopes = new ArrayList<>();
for (LocalVariableScopeLabels range : scopes) {
if (range.name.equals("this")) {
continue;
}
localVariableScopes.add(calculateScope(range));
}
next.setLocalVariableScopes(localVariableScopes);
next.setAccessedFields(accessedFields);
accept(next);
}
/**
* Calculate the start and end line numbers for variable scopes.
* If the LocalVariableScope start line is 0, then it is an input parameter,
* as it's scope start label appeared before the method body.
*/
private LocalVariableScope calculateScope(LocalVariableScopeLabels range) {
int prevLine = 0;
int startLine = lines.firstKey();
int endLine = lines.lastKey();
TryCatchBlockLabels inTry = null;
Stack<TryCatchBlockLabels> tryCatchBlockScopes = new Stack<>();
for (Map.Entry<Integer, Label> entry : lines.entrySet()) {
Label label = entry.getValue();
if (label == range.start) {
startLine = prevLine;
if (!tryCatchBlockScopes.isEmpty()) {
inTry = tryCatchBlockScopes.peek();
}
} else if (inTry == null && label == range.end) {
endLine = prevLine;
} else if (inTry != null && label == inTry.handler) {
endLine = prevLine;
}
/*
* Fix for issue #14: Variable scopes in Try blocks previously had wrong end line number,
* as they pointied to the end of the catch block, even if they were declared in the try block.
*/
for (TryCatchBlockLabels tryCatchBlock : tryCatchBlocks) {
if (label == tryCatchBlock.start) {
tryCatchBlockScopes.push(tryCatchBlock);
} else if (label == tryCatchBlock.handler) {
tryCatchBlockScopes.pop();
}
}
prevLine = entry.getKey();
}
if (startLine > endLine) {
// Sometimes the end label is for an earlier line number than the start label, see Issue #17.
int tmp = startLine;
startLine = endLine;
endLine = tmp;
}
if (lineNumerToFirstOccurrenceOfVariables.containsKey(range.var)) {
if (startLine < lineNumerToFirstOccurrenceOfVariables.get(range.var)) {
/*
* For variables in nested scopes the start Label sometimes points to an earlier line,
* e.g. to the start of the method.
* In these cases the start line of the scope has to be corrected.
*/
startLine = lineNumerToFirstOccurrenceOfVariables.get(range.var);
}
}
return new LocalVariableScope(range.var, range.name, VariableType.getByDesc(range.desc), startLine, endLine);
}
private static class LocalVariableScopeLabels {
final int var;
final String name;
final String desc;
final Label start;
final Label end;
public LocalVariableScopeLabels(int var, String name, String desc, Label start, Label end) {
this.var = var;
this.name = name;
this.desc = desc;
this.start = start;
this.end = end;
}
}
private static class TryCatchBlockLabels {
final Label start;
final Label handler;
public TryCatchBlockLabels(Label start, Label handler) {
this.start = start;
this.handler = handler;
}
}
}