Java tutorial
/* * SonarQube Java * Copyright (C) 2012-2018 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java.bytecode.cfg; import com.google.common.collect.HashMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multiset; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Handle; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.util.Printer; import org.sonar.java.ast.parser.JavaParser; import org.sonar.java.bytecode.cfg.testdata.CFGTestData; import org.sonar.java.bytecode.loader.SquidClassLoader; import org.sonar.java.resolve.BytecodeCompleter; import org.sonar.java.resolve.Convert; import org.sonar.java.resolve.SemanticModel; import org.sonar.java.se.SETestUtils; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; import static org.assertj.core.api.Assertions.assertThat; import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.JSR; import static org.objectweb.asm.Opcodes.NOP; import static org.sonar.java.bytecode.cfg.Instructions.FIELD_INSN; import static org.sonar.java.bytecode.cfg.Instructions.INT_INSN; import static org.sonar.java.bytecode.cfg.Instructions.JUMP_INSN; import static org.sonar.java.bytecode.cfg.Instructions.METHOD_INSN; import static org.sonar.java.bytecode.cfg.Instructions.NO_OPERAND_INSN; import static org.sonar.java.bytecode.cfg.Instructions.TYPE_INSN; import static org.sonar.java.bytecode.cfg.Instructions.VAR_INSN; public class BytecodeCFGBuilderTest { @Test public void test() throws Exception { BytecodeCFG cfg = getCFGForMethod("fun"); StringBuilder sb = new StringBuilder(); cfg.blocks.forEach(b -> sb.append(b.printBlock())); assertThat(sb.toString()) .isEqualTo("B0(Exit)\n" + "B1\n" + "0: ILOAD\n" + "IFEQ Jumps to: B2(true) B3(false) \n" + "B2\n" + "0: LDC\n" + "1: ARETURN\n" + "Jumps to: B0 \n" + "B3\n" + "0: ALOAD\n" + "IFNONNULL Jumps to: B4(true) B5(false) \n" + "B4\n" + "0: LDC\n" + "1: ARETURN\n" + "Jumps to: B0 \n" + "B5\n" + "0: ACONST_NULL\n" + "1: ARETURN\n" + "Jumps to: B0 \n"); } static class InnerClass { private Object fun(boolean a, Object b) { if (a) { if (b == null) { return null; } return ""; } else { return "not a"; } } private boolean label_goto(Object b) { return b == null; } } @Test public void label_goto_successors() throws Exception { BytecodeCFG cfg = getCFGForMethod("label_goto"); StringBuilder sb = new StringBuilder(); cfg.blocks.forEach(b -> sb.append(b.printBlock())); assertThat(sb.toString()).isEqualTo("B0(Exit)\n" + "B1\n" + "0: ALOAD\n" + "IFNONNULL Jumps to: B2(true) B3(false) \n" + "B2\n" + "0: ICONST_0\n" + "Jumps to: B4 \n" + "B3\n" + "0: ICONST_1\n" + "GOTO Jumps to: B4 \n" + "B4\n" + "0: IRETURN\n" + "Jumps to: B0 \n"); } private BytecodeCFG getCFGForMethod(String methodName) { SquidClassLoader squidClassLoader = new SquidClassLoader( Lists.newArrayList(new File("target/test-classes"), new File("target/classes"))); File file = new File("src/test/java/org/sonar/java/bytecode/cfg/BytecodeCFGBuilderTest.java"); CompilationUnitTree tree = (CompilationUnitTree) JavaParser.createParser().parse(file); SemanticModel.createFor(tree, squidClassLoader); Symbol.TypeSymbol innerClass = ((Symbol.TypeSymbol) ((ClassTree) tree.types().get(0)).symbol() .lookupSymbols("InnerClass").iterator().next()); Symbol.MethodSymbol symbol = (Symbol.MethodSymbol) innerClass.lookupSymbols(methodName).iterator().next(); return SETestUtils.bytecodeCFG(symbol.signature(), squidClassLoader); } @Test public void test_all_instructions_are_part_of_CFG() throws Exception { SquidClassLoader squidClassLoader = new SquidClassLoader( Lists.newArrayList(new File("target/test-classes"), new File("target/classes"))); File file = new File("src/test/java/org/sonar/java/bytecode/cfg/testdata/CFGTestData.java"); CompilationUnitTree tree = (CompilationUnitTree) JavaParser.createParser().parse(file); SemanticModel.createFor(tree, squidClassLoader); Symbol.TypeSymbol testClazz = ((ClassTree) tree.types().get(0)).symbol(); ClassReader cr = new ClassReader(squidClassLoader .getResourceAsStream(Convert.bytecodeName(CFGTestData.class.getCanonicalName()) + ".class")); ClassNode classNode = new ClassNode(BytecodeCompleter.ASM_API_VERSION); cr.accept(classNode, 0); for (MethodNode method : classNode.methods) { Multiset<String> opcodes = Arrays.stream(method.instructions.toArray()).map(AbstractInsnNode::getOpcode) .filter(opcode -> opcode != -1).map(opcode -> Printer.OPCODES[opcode]) .collect(Collectors.toCollection(HashMultiset::create)); Symbol methodSymbol = Iterables.getOnlyElement(testClazz.lookupSymbols(method.name)); BytecodeCFG bytecodeCFG = SETestUtils.bytecodeCFG(((Symbol.MethodSymbol) methodSymbol).signature(), squidClassLoader); Multiset<String> cfgOpcodes = cfgOpcodes(bytecodeCFG); assertThat(cfgOpcodes).isEqualTo(opcodes); } } private Multiset<String> cfgOpcodes(BytecodeCFG bytecodeCFG) { return bytecodeCFG.blocks.stream() .flatMap(block -> Stream.concat(block.instructions.stream(), Stream.of(block.terminator))) .filter(Objects::nonNull).map(Instruction::opcode).map(opcode -> Printer.OPCODES[opcode]) .collect(Collectors.toCollection(HashMultiset::create)); } @Test public void all_opcodes_should_be_visited() throws Exception { Instructions ins = new Instructions(); Predicate<Integer> filterReturnAndThrow = opcode -> !((Opcodes.IRETURN <= opcode && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW); NO_OPERAND_INSN.stream().filter(filterReturnAndThrow).forEach(ins::visitInsn); INT_INSN.forEach(i -> ins.visitIntInsn(i, 0)); VAR_INSN.forEach(i -> ins.visitVarInsn(i, 0)); TYPE_INSN.forEach(i -> ins.visitTypeInsn(i, "java/lang/Object")); FIELD_INSN.forEach(i -> ins.visitFieldInsn(i, "java/lang/Object", "foo", "D(D)")); METHOD_INSN.forEach(i -> ins.visitMethodInsn(i, "java/lang/Object", "foo", "()V", i == INVOKEINTERFACE)); JUMP_INSN.forEach(i -> { Label jumpLabel = new Label(); ins.visitJumpInsn(i, jumpLabel); ins.visitLabel(jumpLabel); }); ins.visitLdcInsn("a"); ins.visitIincInsn(0, 1); Handle handle = new Handle(H_INVOKESTATIC, "", "", "()V", false); ins.visitInvokeDynamicInsn("sleep", "()V", handle); ins.visitLookupSwitchInsn(new Label(), new int[] {}, new Label[] {}); ins.visitMultiANewArrayInsn("B", 1); Label l0 = new Label(); Label dflt = new Label(); Label case0 = new Label(); ins.visitTableSwitchInsn(0, 1, dflt, case0); ins.visitLabel(dflt); ins.visitInsn(NOP); ins.visitLabel(l0); ins.visitInsn(NOP); BytecodeCFG cfg = ins.cfg(); Multiset<String> cfgOpcodes = cfgOpcodes(cfg); List<String> collect = Instructions.OPCODES.stream().filter(filterReturnAndThrow) .map(op -> Printer.OPCODES[op]).collect(Collectors.toList()); assertThat(cfgOpcodes).containsAll(collect); } @Test public void visited_label_should_be_assigned_to_true_successor() throws Exception { Label label0 = new Label(); Label label1 = new Label(); BytecodeCFG cfg = new Instructions().visitVarInsn(Opcodes.ALOAD, 0).visitJumpInsn(Opcodes.IFNULL, label0) .visitJumpInsn(Opcodes.IFEQ, label0).visitInsn(Opcodes.ICONST_0).visitJumpInsn(Opcodes.GOTO, label1) .visitLabel(label0).visitInsn(Opcodes.ICONST_1).visitLabel(label1).visitInsn(Opcodes.IRETURN).cfg(); BytecodeCFG.Block block3 = cfg.blocks.get(3); assertThat(block3.terminator.opcode).isEqualTo(Opcodes.IFEQ); assertThat(block3.falseSuccessor()).isNotNull().isSameAs(cfg.blocks.get(4)); assertThat(block3.trueSuccessor()).isNotNull().isSameAs(cfg.blocks.get(2)); assertThat(block3.successors).hasSize(2); assertThat(block3.successors()).hasSize(2); } @Test public void goto_successors() throws Exception { Label label0 = new Label(); Label label1 = new Label(); BytecodeCFG cfg = new Instructions().visitVarInsn(Opcodes.ALOAD, 0).visitJumpInsn(Opcodes.IFNULL, label0) .visitVarInsn(Opcodes.ALOAD, 0).visitJumpInsn(Opcodes.IFNULL, label1).visitVarInsn(Opcodes.ALOAD, 0) .visitVarInsn(Opcodes.ALOAD, 0).visitJumpInsn(Opcodes.IFEQ, label0).visitInsn(Opcodes.ICONST_0) .visitJumpInsn(Opcodes.GOTO, label1).visitLabel(label0).visitInsn(Opcodes.ICONST_1) .visitLabel(label1).visitInsn(Opcodes.IRETURN).cfg(); assertThat(cfg.blocks.get(6).successors).containsExactly(cfg.blocks.get(4)); } @Test public void isNotBlank_goto_followed_by_label() throws Exception { SquidClassLoader classLoader = new SquidClassLoader( Lists.newArrayList(new File("src/test/commons-lang-2.1"))); // apache commons 2.1 isNotBlank has a goto followed by an unreferenced label : see SONARJAVA-2461 BytecodeCFG bytecodeCFG = SETestUtils .bytecodeCFG("org.apache.commons.lang.StringUtils#isNotBlank(Ljava/lang/String;)Z", classLoader); assertThat(bytecodeCFG).isNotNull(); assertThat(bytecodeCFG.blocks).hasSize(11); assertThat(bytecodeCFG.blocks.get(4).successors).containsExactly(bytecodeCFG.blocks.get(6)); } @Test public void supportJSRandRET() throws Exception { SquidClassLoader classLoader = new SquidClassLoader(Lists.newArrayList(new File("src/test/JsrRet"))); BytecodeCFG bytecodeCFG = SETestUtils.bytecodeCFG("jdk3.AllInstructions#jsrAndRetInstructions(I)I", classLoader); assertThat(bytecodeCFG).isNotNull(); bytecodeCFG.blocks.stream().map(b -> b.terminator).filter(Objects::nonNull) .forEach(t -> assertThat(t.opcode).isNotEqualTo(JSR)); } @Test public void try_catch_finally() throws Exception { String methodName = "tryCatch"; String expectedCFG = "B0(Exit)\n" + "B1\n" + "Jumps to: B2 \n" + "B2\n" + "0: ALOAD\n" + "1: INVOKESPECIAL\n" + "2: ALOAD\n" + "3: INVOKESPECIAL\n" + "Jumps to: B3 B5(Exception:java.io.IOException) B7(Exception:!UncaughtException!) \n" + "B3\n" + "0: GETSTATIC\n" + "1: LDC\n" + "2: INVOKEVIRTUAL\n" + "GOTO Jumps to: B4 \n" + "B4\n" + "0: RETURN\n" + "Jumps to: B0 \n" + "B5\n" + "0: ASTORE\n" + "1: ALOAD\n" + "2: INVOKEVIRTUAL\n" + "Jumps to: B6 B7(Exception:!UncaughtException!) \n" + "B6\n" + "0: GETSTATIC\n" + "1: LDC\n" + "2: INVOKEVIRTUAL\n" + "GOTO Jumps to: B4 \n" + "B7\n" + "0: ASTORE\n" + "1: GETSTATIC\n" + "2: LDC\n" + "3: INVOKEVIRTUAL\n" + "4: ALOAD\n" + "5: ATHROW\n" + "Jumps to: B0 \n"; assertCFGforMethod(methodName, expectedCFG); } @Test public void try_catch_finally_with_multiplePaths_in_try() throws Exception { String methodName = "tryCatchWithMultiplePaths"; assertCFGforMethod(methodName, "B0(Exit)\n" + "B1\n" + "Jumps to: B2 \n" + "B2\n" + "0: ALOAD\n" + "1: INVOKESPECIAL\n" + "2: ILOAD\n" + "IFNE Jumps to: B3(true) B4(false) B8(Exception:java.io.IOException) B10(Exception:!UncaughtException!) \n" + "B3\n" + "0: ALOAD\n" + "1: INVOKESPECIAL\n" + "Jumps to: B5 B8(Exception:java.io.IOException) B10(Exception:!UncaughtException!) \n" + "B4\n" + "0: ALOAD\n" + "1: INVOKESPECIAL\n" + "GOTO Jumps to: B5 B8(Exception:java.io.IOException) B10(Exception:!UncaughtException!) \n" + "B5\n" + "0: GETSTATIC\n" + "1: LDC\n" + "2: INVOKEVIRTUAL\n" + "Jumps to: B6 B8(Exception:java.io.IOException) B10(Exception:!UncaughtException!) \n" + "B6\n" + "0: GETSTATIC\n" + "1: LDC\n" + "2: INVOKEVIRTUAL\n" + "GOTO Jumps to: B7 \n" + "B7\n" + "0: RETURN\n" + "Jumps to: B0 \n" + "B8\n" + "0: ASTORE\n" + "1: ALOAD\n" + "2: INVOKEVIRTUAL\n" + "Jumps to: B9 B10(Exception:!UncaughtException!) \n" + "B9\n" + "0: GETSTATIC\n" + "1: LDC\n" + "2: INVOKEVIRTUAL\n" + "GOTO Jumps to: B7 \n" + "B10\n" + "0: ASTORE\n" + "1: GETSTATIC\n" + "2: LDC\n" + "3: INVOKEVIRTUAL\n" + "4: ALOAD\n" + "5: ATHROW\n" + "Jumps to: B0 \n"); } @Test public void last_block_is_ends_with_GOTO() throws Exception { assertCFGforMethod("loopWithoutStopCondition", "B0(Exit)\n" + "B1\n" + "0: ICONST_0\n" + "1: ISTORE\n" + "Jumps to: B2 \n" + "B2\n" + "0: ILOAD\n" + "1: GETSTATIC\n" + "2: ILOAD\n" + "3: IALOAD\n" + "IF_ICMPGT Jumps to: B3(true) B4(false) \n" + "B3\n" + "0: IINC\n" + "GOTO Jumps to: B2 \n" + "B4\n" + "0: ILOAD\n" + "1: ICONST_1\n" + "2: IADD\n" + "3: IRETURN\n" + "Jumps to: B0 \n"); } @Test public void test_class_not_found_logs() throws Exception { SquidClassLoader squidClassLoader = new SquidClassLoader( Lists.newArrayList(new File("target/test-classes"), new File("target/classes"))); BytecodeCFG cfg = SETestUtils.bytecodeCFG("nonsense#foo", squidClassLoader); assertThat(cfg).isNull(); } private void assertCFGforMethod(String methodName, String expectedCFG) { BytecodeCFG cfg = getBytecodeCFG(methodName, "src/test/java/org/sonar/java/bytecode/cfg/BytecodeCFGBuilderTest.java"); StringBuilder sb = new StringBuilder(); cfg.blocks.forEach(b -> sb.append(b.printBlock())); assertThat(sb.toString()).isEqualTo(expectedCFG); } public static BytecodeCFG getBytecodeCFG(String methodName, String filename) { SquidClassLoader squidClassLoader = new SquidClassLoader( Lists.newArrayList(new File("target/test-classes"), new File("target/classes"))); File file = new File(filename); CompilationUnitTree tree = (CompilationUnitTree) JavaParser.createParser().parse(file); SemanticModel.createFor(tree, squidClassLoader); List<Tree> classMembers = ((ClassTree) tree.types().get(0)).members(); Symbol.MethodSymbol symbol = classMembers.stream().filter(m -> m instanceof MethodTree) .map(m -> ((MethodTree) m).symbol()).filter(s -> methodName.equals(s.name())).findFirst() .orElseThrow(IllegalStateException::new); return SETestUtils.bytecodeCFG(symbol.signature(), squidClassLoader); } private void tryCatch() { try { bar(); fun(); } catch (IOException e) { e.printStackTrace(); } finally { System.out.println("finally"); } } private void tryCatchWithMultiplePaths(int i) { try { bar(); if (i == 0) { fun(); } else { fun2(); } System.out.println("endOfTry"); } catch (IOException e) { e.printStackTrace(); } finally { System.out.println("finally"); } } private void bar() { } private void fun() throws IOException { } private void fun2() throws IOException { } final static int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE }; static int loopWithoutStopCondition(int x) { for (int i = 0;; i++) if (x <= sizeTable[i]) return i + 1; } }