From 4557d052561fed35439a5497cd3a2a12d0106cff Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 21 Dec 2021 15:16:48 +0000 Subject: [PATCH] fix: use correct type for anonymous class instance (#597) --- .../main/java/jadx/core/codegen/InsnGen.java | 16 +----- .../main/java/jadx/core/codegen/NameGen.java | 9 ++-- .../java/jadx/core/dex/attributes/AType.java | 4 ++ .../nodes/AnonymousClassBaseAttr.java | 28 ++++++++++ .../attributes/nodes/MethodBridgeAttr.java | 28 ++++++++++ .../attributes/nodes/MethodOverrideAttr.java | 8 +++ .../core/dex/nodes/utils/MethodUtils.java | 26 +++++++++- .../dex/visitors/OverrideMethodVisitor.java | 23 +++++---- .../core/dex/visitors/ProcessAnonymous.java | 13 +++++ .../typeinference/TypeInferenceVisitor.java | 51 +++++++++++++++---- .../inner/TestAnonymousClass16.java | 17 +++++-- .../inner/TestAnonymousClass3a.java | 3 ++ 12 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassBaseAttr.java create mode 100644 jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodBridgeAttr.java diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 9dc8da3ae..75fc28a91 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -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(' '); diff --git a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java index e9834f93b..9f73a8343 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java @@ -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()); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java index 9da221d1a..ab2a6fdf1 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java @@ -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 implements IJadxAttrType { public static final AType ENUM_CLASS = new AType<>(); public static final AType ENUM_MAP = new AType<>(); public static final AType CLASS_TYPE_VARS = new AType<>(); + public static final AType ANONYMOUS_CLASS_BASE = new AType<>(); // field public static final AType FIELD_INIT_INSN = new AType<>(); @@ -63,6 +66,7 @@ public final class AType implements IJadxAttrType { public static final AType METHOD_OVERRIDE = new AType<>(); public static final AType METHOD_TYPE_VARS = new AType<>(); public static final AType> TRY_BLOCKS_LIST = new AType<>(); + public static final AType BRIDGED_BY = new AType<>(); // region public static final AType DECLARE_VARIABLES = new AType<>(); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassBaseAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassBaseAttr.java new file mode 100644 index 000000000..849ef0ca5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/AnonymousClassBaseAttr.java @@ -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 getAttrType() { + return AType.ANONYMOUS_CLASS_BASE; + } + + @Override + public String toString() { + return "AnonymousClassBaseAttr{" + baseType + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodBridgeAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodBridgeAttr.java new file mode 100644 index 000000000..695491e82 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodBridgeAttr.java @@ -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 getAttrType() { + return AType.BRIDGED_BY; + } + + @Override + public String toString() { + return "BRIDGED_BY: " + bridgeMth; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodOverrideAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodOverrideAttr.java index a6dee964a..8491fc96a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodOverrideAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/MethodOverrideAttr.java @@ -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 getOverrideList() { return overrideList; } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java index 4c7c82e7a..3d1132ec3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/utils/MethodUtils.java @@ -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 collectedMths) { + private boolean processMethodArgsOverloaded(ArgType startCls, MethodInfo mthInfo, @Nullable List 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(); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java index becb7e225..3f261afd5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/OverrideMethodVisitor.java @@ -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; } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java index 83bae8995..844a3f6d5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -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()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java index a77d60392..bdbba4d54 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java @@ -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) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass16.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass16.java index e380fed25..7df3cf796 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass16.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass16.java @@ -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() + "}", + "};"); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3a.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3a.java index d30edaa13..90636d173 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3a.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass3a.java @@ -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() {")