fix: use correct type for anonymous class instance (#597)
This commit is contained in:
@@ -717,14 +717,7 @@ public class InsnGen {
|
||||
throw new CodegenException("Anonymous inner class unlimited recursion detected."
|
||||
+ " Convert class to inner: " + cls.getClassInfo().getFullName());
|
||||
}
|
||||
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
ArgType parent;
|
||||
if (cls.getInterfaces().size() == 1) {
|
||||
parent = cls.getInterfaces().get(0);
|
||||
} else {
|
||||
parent = cls.getSuperClass();
|
||||
}
|
||||
ArgType parent = cls.get(AType.ANONYMOUS_CLASS_BASE).getBaseType();
|
||||
// hide empty anonymous constructors
|
||||
for (MethodNode ctor : cls.getMethods()) {
|
||||
if (ctor.contains(AFlag.ANONYMOUS_CONSTRUCTOR)
|
||||
@@ -732,13 +725,8 @@ public class InsnGen {
|
||||
ctor.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
}
|
||||
|
||||
code.add("new ");
|
||||
if (parent == null) {
|
||||
code.add("Object");
|
||||
} else {
|
||||
useClass(code, parent);
|
||||
}
|
||||
useClass(code, parent);
|
||||
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
|
||||
generateMethodArguments(code, insn, 0, callMth);
|
||||
code.add(' ');
|
||||
|
||||
@@ -162,8 +162,7 @@ public class NameGen {
|
||||
InsnNode assignInsn = assignArg.getParentInsn();
|
||||
if (assignInsn != null) {
|
||||
String name = makeNameFromInsn(assignInsn);
|
||||
if (name != null && !NameMapper.isReserved(name)) {
|
||||
assignArg.setName(name);
|
||||
if (name != null && NameMapper.isValidAndPrintable(name)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -202,7 +201,11 @@ public class NameGen {
|
||||
return vName;
|
||||
}
|
||||
if (shortName != null) {
|
||||
return StringUtils.escape(shortName.toLowerCase());
|
||||
String lower = StringUtils.escape(shortName.toLowerCase());
|
||||
if (shortName.equals(lower)) {
|
||||
return lower + "Var";
|
||||
}
|
||||
return lower;
|
||||
}
|
||||
}
|
||||
return StringUtils.escape(type.toString());
|
||||
|
||||
@@ -2,6 +2,7 @@ package jadx.core.dex.attributes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttrType;
|
||||
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
|
||||
import jadx.core.dex.attributes.nodes.ClassTypeVarsAttr;
|
||||
import jadx.core.dex.attributes.nodes.DeclareVariablesAttr;
|
||||
import jadx.core.dex.attributes.nodes.EdgeInsnAttr;
|
||||
@@ -16,6 +17,7 @@ import jadx.core.dex.attributes.nodes.JumpInfo;
|
||||
import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr;
|
||||
import jadx.core.dex.attributes.nodes.LoopInfo;
|
||||
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodTypeVarsAttr;
|
||||
@@ -51,6 +53,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<EnumClassAttr> ENUM_CLASS = new AType<>();
|
||||
public static final AType<EnumMapAttr> ENUM_MAP = new AType<>();
|
||||
public static final AType<ClassTypeVarsAttr> CLASS_TYPE_VARS = new AType<>();
|
||||
public static final AType<AnonymousClassBaseAttr> ANONYMOUS_CLASS_BASE = new AType<>();
|
||||
|
||||
// field
|
||||
public static final AType<FieldInitInsnAttr> FIELD_INIT_INSN = new AType<>();
|
||||
@@ -63,6 +66,7 @@ public final class AType<T extends IJadxAttribute> implements IJadxAttrType<T> {
|
||||
public static final AType<MethodOverrideAttr> METHOD_OVERRIDE = new AType<>();
|
||||
public static final AType<MethodTypeVarsAttr> METHOD_TYPE_VARS = new AType<>();
|
||||
public static final AType<AttrList<TryCatchBlockAttr>> TRY_BLOCKS_LIST = new AType<>();
|
||||
public static final AType<MethodBridgeAttr> BRIDGED_BY = new AType<>();
|
||||
|
||||
// region
|
||||
public static final AType<DeclareVariablesAttr> DECLARE_VARIABLES = new AType<>();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
|
||||
public class AnonymousClassBaseAttr extends PinnedAttribute {
|
||||
|
||||
private final ArgType baseType;
|
||||
|
||||
public AnonymousClassBaseAttr(ArgType baseType) {
|
||||
this.baseType = baseType;
|
||||
}
|
||||
|
||||
public ArgType getBaseType() {
|
||||
return baseType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AType<AnonymousClassBaseAttr> getAttrType() {
|
||||
return AType.ANONYMOUS_CLASS_BASE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AnonymousClassBaseAttr{" + baseType + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package jadx.core.dex.attributes.nodes;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
|
||||
public class MethodBridgeAttr extends PinnedAttribute {
|
||||
|
||||
private final MethodNode bridgeMth;
|
||||
|
||||
public MethodBridgeAttr(MethodNode bridgeMth) {
|
||||
this.bridgeMth = bridgeMth;
|
||||
}
|
||||
|
||||
public MethodNode getBridgeMth() {
|
||||
return bridgeMth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AType<MethodBridgeAttr> getAttrType() {
|
||||
return AType.BRIDGED_BY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BRIDGED_BY: " + bridgeMth;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@ package jadx.core.dex.attributes.nodes;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.nodes.IMethodDetails;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.utils.Utils;
|
||||
|
||||
public class MethodOverrideAttr extends PinnedAttribute {
|
||||
|
||||
@@ -29,6 +32,11 @@ public class MethodOverrideAttr extends PinnedAttribute {
|
||||
return overrideList.isEmpty();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IMethodDetails getBaseMth() {
|
||||
return Utils.last(overrideList);
|
||||
}
|
||||
|
||||
public List<IMethodDetails> getOverrideList() {
|
||||
return overrideList;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
import jadx.core.clsp.ClspClass;
|
||||
import jadx.core.clsp.ClspMethod;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.BaseInvokeNode;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
@@ -67,7 +70,7 @@ public class MethodUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
|
||||
private boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List<IMethodDetails> collectedMths) {
|
||||
if (startCls == null || !startCls.isObject()) {
|
||||
return false;
|
||||
}
|
||||
@@ -122,4 +125,25 @@ public class MethodUtils {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IMethodDetails getOverrideBaseMth(MethodNode mth) {
|
||||
MethodOverrideAttr overrideAttr = mth.get(AType.METHOD_OVERRIDE);
|
||||
if (overrideAttr == null) {
|
||||
return null;
|
||||
}
|
||||
return overrideAttr.getBaseMth();
|
||||
}
|
||||
|
||||
public ClassInfo getMethodOriginDeclClass(MethodNode mth) {
|
||||
IMethodDetails baseMth = getOverrideBaseMth(mth);
|
||||
if (baseMth != null) {
|
||||
return baseMth.getMethodInfo().getDeclClass();
|
||||
}
|
||||
MethodBridgeAttr bridgeAttr = mth.get(AType.BRIDGED_BY);
|
||||
if (bridgeAttr != null) {
|
||||
return getMethodOriginDeclClass(bridgeAttr.getBridgeMth());
|
||||
}
|
||||
return mth.getMethodInfo().getDeclClass();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import jadx.core.clsp.ClspClass;
|
||||
import jadx.core.clsp.ClspMethod;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.MethodBridgeAttr;
|
||||
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
|
||||
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
@@ -65,13 +66,13 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
MethodOverrideAttr attr = processOverrideMethods(cls, mth, superTypes);
|
||||
if (attr != null) {
|
||||
mth.addAttr(attr);
|
||||
IMethodDetails baseMth = Utils.last(attr.getOverrideList());
|
||||
IMethodDetails baseMth = attr.getBaseMth();
|
||||
if (baseMth != null) {
|
||||
boolean updated = fixMethodReturnType(mth, baseMth, superTypes);
|
||||
updated |= fixMethodArgTypes(mth, baseMth, superTypes);
|
||||
if (updated && cls.root().getArgs().isRenameValid()) {
|
||||
if (updated) {
|
||||
// check if new signature cause method collisions
|
||||
fixMethodSignatureCollisions(mth);
|
||||
checkMethodSignatureCollisions(mth, cls.root().getArgs().isRenameValid());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,7 +344,7 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fixMethodSignatureCollisions(MethodNode mth) {
|
||||
private void checkMethodSignatureCollisions(MethodNode mth, boolean rename) {
|
||||
String mthName = mth.getMethodInfo().getAlias();
|
||||
String newSignature = MethodInfo.makeShortId(mthName, mth.getArgTypes(), null);
|
||||
for (MethodNode otherMth : mth.getParentClass().getMethods()) {
|
||||
@@ -351,12 +352,16 @@ public class OverrideMethodVisitor extends AbstractVisitor {
|
||||
if (otherMthName.equals(mthName) && otherMth != mth) {
|
||||
String otherSignature = otherMth.getMethodInfo().makeSignature(true, false);
|
||||
if (otherSignature.equals(newSignature)) {
|
||||
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
|
||||
otherMth.addWarnComment("Can't rename method to resolve collision");
|
||||
} else {
|
||||
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
|
||||
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
|
||||
if (rename) {
|
||||
if (otherMth.contains(AFlag.DONT_RENAME) || otherMth.contains(AType.METHOD_OVERRIDE)) {
|
||||
otherMth.addWarnComment("Can't rename method to resolve collision");
|
||||
} else {
|
||||
otherMth.getMethodInfo().setAlias(makeNewAlias(otherMth));
|
||||
otherMth.addAttr(new RenameReasonAttr("avoid collision after fix types in other method"));
|
||||
}
|
||||
}
|
||||
otherMth.addAttr(new MethodBridgeAttr(mth));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
@@ -39,6 +41,7 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
return;
|
||||
}
|
||||
cls.add(AFlag.ANONYMOUS_CLASS);
|
||||
cls.addAttr(new AnonymousClassBaseAttr(getBaseType(cls)));
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
@@ -49,6 +52,16 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
private static ArgType getBaseType(ClassNode cls) {
|
||||
ArgType parent;
|
||||
if (cls.getInterfaces().size() == 1) {
|
||||
parent = cls.getInterfaces().get(0);
|
||||
} else {
|
||||
parent = cls.getSuperClass();
|
||||
}
|
||||
return parent != null ? parent : ArgType.OBJECT;
|
||||
}
|
||||
|
||||
private static boolean isStaticFieldUsedOutside(ClassNode cls) {
|
||||
ClassNode topCls = cls.getTopParentClass();
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
|
||||
+42
-9
@@ -19,6 +19,7 @@ import jadx.core.Consts;
|
||||
import jadx.core.clsp.ClspGraph;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
|
||||
import jadx.core.dex.attributes.nodes.PhiListAttr;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.instructions.ArithNode;
|
||||
@@ -35,12 +36,15 @@ import jadx.core.dex.instructions.args.LiteralArg;
|
||||
import jadx.core.dex.instructions.args.PrimitiveType;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.SSAVar;
|
||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||
import jadx.core.dex.instructions.mods.TernaryInsn;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.IMethodDetails;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.nodes.utils.MethodUtils;
|
||||
import jadx.core.dex.trycatch.ExcHandlerAttr;
|
||||
import jadx.core.dex.visitors.AbstractVisitor;
|
||||
import jadx.core.dex.visitors.AttachMethodDetails;
|
||||
@@ -273,6 +277,11 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType));
|
||||
break;
|
||||
|
||||
case CONSTRUCTOR:
|
||||
ArgType ctrClsType = replaceAnonymousType((ConstructorInsn) insn);
|
||||
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, ctrClsType));
|
||||
break;
|
||||
|
||||
case CONST:
|
||||
LiteralArg constLit = (LiteralArg) insn.getArg(0);
|
||||
addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType()));
|
||||
@@ -308,6 +317,19 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
private ArgType replaceAnonymousType(ConstructorInsn ctr) {
|
||||
if (ctr.isNewInstance()) {
|
||||
ClassNode ctrCls = root.resolveClass(ctr.getClassType());
|
||||
if (ctrCls != null && ctrCls.contains(AFlag.DONT_GENERATE)) {
|
||||
AnonymousClassBaseAttr baseTypeAttr = ctrCls.get(AType.ANONYMOUS_CLASS_BASE);
|
||||
if (baseTypeAttr != null) {
|
||||
return baseTypeAttr.getBaseType();
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctr.getClassType().getType();
|
||||
}
|
||||
|
||||
private ITypeBound makeAssignFieldGetBound(IndexInsnNode insn) {
|
||||
ArgType initType = insn.getResult().getInitType();
|
||||
if (initType.containsTypeVariable()) {
|
||||
@@ -340,7 +362,7 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
return null;
|
||||
}
|
||||
if (insn instanceof BaseInvokeNode) {
|
||||
TypeBoundInvokeUse invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
|
||||
ITypeBound invokeUseBound = makeInvokeUseBound(regArg, (BaseInvokeNode) insn);
|
||||
if (invokeUseBound != null) {
|
||||
return invokeUseBound;
|
||||
}
|
||||
@@ -352,21 +374,32 @@ public final class TypeInferenceVisitor extends AbstractVisitor {
|
||||
return new TypeBoundConst(BoundEnum.USE, regArg.getInitType(), regArg);
|
||||
}
|
||||
|
||||
private TypeBoundInvokeUse makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
|
||||
private ITypeBound makeInvokeUseBound(RegisterArg regArg, BaseInvokeNode invoke) {
|
||||
InsnArg instanceArg = invoke.getInstanceArg();
|
||||
if (instanceArg == null || instanceArg == regArg) {
|
||||
if (instanceArg == null) {
|
||||
return null;
|
||||
}
|
||||
IMethodDetails methodDetails = root.getMethodUtils().getMethodDetails(invoke);
|
||||
MethodUtils methodUtils = root.getMethodUtils();
|
||||
IMethodDetails methodDetails = methodUtils.getMethodDetails(invoke);
|
||||
if (methodDetails == null) {
|
||||
return null;
|
||||
}
|
||||
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
|
||||
ArgType argType = methodDetails.getArgTypes().get(argIndex);
|
||||
if (!argType.containsTypeVariable()) {
|
||||
return null;
|
||||
if (instanceArg != regArg) {
|
||||
int argIndex = invoke.getArgIndex(regArg) - invoke.getFirstArgOffset();
|
||||
ArgType argType = methodDetails.getArgTypes().get(argIndex);
|
||||
if (!argType.containsTypeVariable()) {
|
||||
return null;
|
||||
}
|
||||
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
|
||||
}
|
||||
return new TypeBoundInvokeUse(root, invoke, regArg, argType);
|
||||
|
||||
// for override methods use origin declared class as type
|
||||
if (methodDetails instanceof MethodNode) {
|
||||
MethodNode callMth = (MethodNode) methodDetails;
|
||||
ClassInfo declCls = methodUtils.getMethodOriginDeclClass(callMth);
|
||||
return new TypeBoundConst(BoundEnum.USE, declCls.getType(), regArg);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean tryPossibleTypes(MethodNode mth, SSAVar var, ArgType type) {
|
||||
|
||||
@@ -2,9 +2,11 @@ package jadx.tests.integration.inner;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.NotYetImplemented;
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestAnonymousClass16 extends IntegrationTest {
|
||||
|
||||
public static class TestCls {
|
||||
@@ -26,9 +28,18 @@ public class TestAnonymousClass16 extends IntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@NotYetImplemented
|
||||
public void test() {
|
||||
getArgs().setCommentsLevel(CommentsLevel.NONE);
|
||||
noDebugInfo();
|
||||
getClassNode(TestCls.class);
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.doesNotContain("r0")
|
||||
.doesNotContain("AnonymousClass1 r0 = ")
|
||||
.containsLines(2,
|
||||
"Something something = new Something() {",
|
||||
indent() + "{",
|
||||
indent(2) + "put(\"a\", \"b\");",
|
||||
indent() + "}",
|
||||
"};");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package jadx.tests.integration.inner;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.NotYetImplemented;
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.tests.api.IntegrationTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
@@ -41,9 +42,11 @@ public class TestAnonymousClass3a extends IntegrationTest {
|
||||
@Test
|
||||
@NotYetImplemented
|
||||
public void test() {
|
||||
getArgs().setCommentsLevel(CommentsLevel.NONE);
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.doesNotContain("synthetic")
|
||||
.doesNotContain("access$00")
|
||||
.doesNotContain("AnonymousClass_")
|
||||
.doesNotContain("unused = ")
|
||||
.containsLine(4, "public void run() {")
|
||||
|
||||
Reference in New Issue
Block a user