Skip to content

Commit

Permalink
Merge pull request #1363 from hcoles/feature/lambda_exclusion_cleanup
Browse files Browse the repository at this point in the history
tidy up filter and test
  • Loading branch information
hcoles authored Nov 11, 2024
2 parents 35a8ce8 + 1752f03 commit 7ec969b
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.pitest.bytecode.analysis.ClassTree;
import org.pitest.bytecode.analysis.MethodTree;
import org.pitest.functional.FCollection;
import org.pitest.functional.prelude.Prelude;
import org.pitest.classinfo.ClassName;
import org.pitest.mutationtest.build.InterceptorType;
import org.pitest.mutationtest.build.MutationInterceptor;
import org.pitest.mutationtest.engine.Location;
import org.pitest.mutationtest.engine.Mutater;
import org.pitest.mutationtest.engine.MutationDetails;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Optional;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ExcludedAnnotationInterceptor implements MutationInterceptor {

Expand All @@ -42,20 +44,21 @@ public InterceptorType type() {
@Override
public void begin(ClassTree clazz) {
this.skipClass = clazz.annotations().stream()
.anyMatch(avoidedAnnotation());
.anyMatch(avoidedAnnotation());
if (!this.skipClass) {
// 1. Collect methods with avoided annotations or that override such methods
// 1. Collect methods with avoided annotations
final List<MethodTree> avoidedMethods = clazz.methods().stream()
.filter(hasAvoidedAnnotation())
.collect(Collectors.toList());
.filter(hasAvoidedAnnotation())
.collect(Collectors.toList());

// Collect method names along with descriptors to handle overloaded methods
final Set<MethodSignature> avoidedMethodSignatures = avoidedMethods.stream()
.map(method -> new MethodSignature(method.rawNode().name, method.rawNode().desc))
.collect(Collectors.toSet());
final Set<Location> avoidedMethodSignatures = avoidedMethods.stream()
.map(method -> new Location(clazz.name(), method.rawNode().name, method.rawNode().desc))
.collect(Collectors.toSet());

// Keep track of processed methods to avoid infinite loops
Set<MethodSignature> processedMethods = new HashSet<>(avoidedMethodSignatures);
// Keep track of processed methods to avoid infinite loops - TODO not clear
// that this is necessary
Set<Location> processedMethods = new HashSet<>(avoidedMethodSignatures);

// 2. For each avoided method, collect lambda methods recursively
for (MethodTree avoidedMethod : avoidedMethods) {
Expand All @@ -64,8 +67,8 @@ public void begin(ClassTree clazz) {

// 3. Create a predicate to match mutations in methods to avoid
this.annotatedMethodMatcher = mutation -> {
MethodSignature mutationSignature = new MethodSignature(
mutation.getMethod(), mutation.getId().getLocation().getMethodDesc());
Location mutationSignature = Location.location(clazz.name(),
mutation.getMethod(), mutation.getId().getLocation().getMethodDesc());
return avoidedMethodSignatures.contains(mutationSignature);
};
}
Expand All @@ -80,57 +83,58 @@ public void begin(ClassTree clazz) {
* @param processedMethods The set of already processed methods to prevent infinite loops.
*/
private void collectLambdaMethods(MethodTree method, ClassTree clazz,
Set<MethodSignature> avoidedMethodSignatures,
Set<MethodSignature> processedMethods) {
Set<Location> avoidedMethodSignatures,
Set<Location> processedMethods) {
Queue<MethodTree> methodsToProcess = new LinkedList<>();
methodsToProcess.add(method);

while (!methodsToProcess.isEmpty()) {
MethodTree currentMethod = methodsToProcess.poll();
Set<Location> lambdas = currentMethod.instructions().stream()
.flatMap(n -> lambdaCallsToClass(clazz.name(), n))
.filter(l -> !avoidedMethodSignatures.contains(l) && !processedMethods.contains(l))
.collect(Collectors.toSet());

for (AbstractInsnNode insn : currentMethod.rawNode().instructions) {
if (insn instanceof InvokeDynamicInsnNode) {
InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;

for (Object bsmArg : indy.bsmArgs) {
if (bsmArg instanceof Handle) {
Handle handle = (Handle) bsmArg;
// Check if the method is in the same class and is a lambda method
if (handle.getOwner().equals(clazz.rawNode().name) && handle.getName().startsWith("lambda$")) {
MethodSignature lambdaMethodSignature = new MethodSignature(handle.getName(), handle.getDesc());
if (!avoidedMethodSignatures.contains(lambdaMethodSignature)
&& !processedMethods.contains(lambdaMethodSignature)) {
avoidedMethodSignatures.add(lambdaMethodSignature);
processedMethods.add(lambdaMethodSignature);
// Find the MethodTree for this lambda method
MethodTree lambdaMethod = findMethodTree(clazz, handle.getName(), handle.getDesc());
if (lambdaMethod != null) {
methodsToProcess.add(lambdaMethod);
}
}
}
}
}
}
}
List<MethodTree> recurse = lambdas.stream()
.map(clazz::method)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());

methodsToProcess.addAll(recurse);

avoidedMethodSignatures.addAll(lambdas);
processedMethods.addAll(lambdas);
}
}

private MethodTree findMethodTree(ClassTree clazz, String methodName, String methodDesc) {
return clazz.methods().stream()
.filter(m -> m.rawNode().name.equals(methodName) && m.rawNode().desc.equals(methodDesc))
.findFirst()
.orElse(null);
}
private Stream<Location> lambdaCallsToClass(ClassName clazz, AbstractInsnNode insn) {
if (!(insn instanceof InvokeDynamicInsnNode)) {
return Stream.empty();
}

InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn;

for (Object bsmArg : indy.bsmArgs) {
if (bsmArg instanceof Handle) {
Handle handle = (Handle) bsmArg;
// Check if the method is in the same class and is a lambda method
if (handle.getOwner().equals(clazz.asInternalName()) && handle.getName().startsWith("lambda$")) {
return Stream.of(Location.location(clazz,handle.getName(), handle.getDesc()));
}
}
}
return Stream.empty();
}

/**
* Creates a predicate that checks if a method has an avoided annotation.
*
* @return A predicate that returns true if the method should be avoided.
*/
private Predicate<MethodTree> hasAvoidedAnnotation() {
return methodTree ->
methodTree.annotations().stream().anyMatch(avoidedAnnotation());
methodTree.annotations().stream().anyMatch(avoidedAnnotation());
}

private Predicate<AnnotationNode> avoidedAnnotation() {
Expand All @@ -139,12 +143,14 @@ private Predicate<AnnotationNode> avoidedAnnotation() {

@Override
public Collection<MutationDetails> intercept(
Collection<MutationDetails> mutations, Mutater m) {
Collection<MutationDetails> mutations, Mutater m) {
if (this.skipClass) {
return Collections.emptyList();
}

return FCollection.filter(mutations, Prelude.not(this.annotatedMethodMatcher));
return mutations.stream()
.filter(this.annotatedMethodMatcher.negate())
.collect(Collectors.toList());
}

@Override
Expand All @@ -162,34 +168,4 @@ boolean shouldAvoid(String desc) {
return false;
}

/**
* Represents a method signature with its name and descriptor.
* Used to uniquely identify methods, especially overloaded ones.
*/
private static class MethodSignature {
private final String name;
private final String desc;

MethodSignature(String name, String desc) {
this.name = name;
this.desc = desc;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MethodSignature that = (MethodSignature) obj;
return name.equals(that.name) && desc.equals(that.desc);
}

@Override
public int hashCode() {
return name.hashCode() * 31 + desc.hashCode();
}
}
}
Loading

0 comments on commit 7ec969b

Please sign in to comment.