1 /******************************************************************************* 2 * Copyright (c) 2009, 2017 Mountainminds GmbH & Co. KG and Contributors 3 * All rights reserved. This program and the accompanying materials 4 * are made available under the terms of the Eclipse Public License v1.0 5 * which accompanies this distribution, and is available at 6 * http://www.eclipse.org/legal/epl-v10.html 7 * 8 * Contributors: 9 * Evgeny Mandrikov - initial API and implementation 10 * 11 *******************************************************************************/ 12 package org.jacoco.core.internal.analysis.filter; 13 14 import org.objectweb.asm.Opcodes; 15 import org.objectweb.asm.tree.AbstractInsnNode; 16 import org.objectweb.asm.tree.MethodInsnNode; 17 import org.objectweb.asm.tree.MethodNode; 18 import org.objectweb.asm.tree.TryCatchBlockNode; 19 20 /** 21 * Filters code that javac generates for try-with-resources statement. 22 */ 23 public final class TryWithResourcesJavacFilter implements IFilter { 24 25 public void filter(final String className, final String superClassName, 26 final MethodNode methodNode, final IFilterOutput output) { 27 if (methodNode.tryCatchBlocks.isEmpty()) { 28 return; 29 } 30 final Matcher matcher = new Matcher(output); 31 for (TryCatchBlockNode t : methodNode.tryCatchBlocks) { 32 if ("java/lang/Throwable".equals(t.type)) { 33 for (Matcher.JavacPattern p : Matcher.JavacPattern.values()) { 34 matcher.start(t.handler); 35 if (matcher.matchJavac(p)) { 36 break; 37 } 38 } 39 } 40 } 41 } 42 43 /** 44 * javac from JDK 7 and 8 generates bytecode that is equivalent to the 45 * compilation of source code that is described in <a href= 46 * "http://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.20.3.1">JLS 47 * 14.20.3. try-with-resources</a>: 48 * 49 * <pre> 50 * Resource r = ...; 51 * Throwable primaryExc = null; 52 * try { 53 * ... 54 * } finally { 55 * if (r != null) { 56 * if (primaryExc != null) { 57 * try { 58 * r.close(); 59 * } catch (Throwable suppressedExc) { 60 * primaryExc.addSuppressed(suppressedExc); 61 * } 62 * } else { 63 * r.close(); 64 * } 65 * } 66 * } 67 * </pre> 68 * 69 * Case of multiple resources looks like multiple nested try-with-resources 70 * statements. javac from JDK 9 EA b160 does the same, but with some 71 * optimizations (see <a href= 72 * "https://bugs.openjdk.java.net/browse/JDK-7020499">JDK-7020499</a>): 73 * <ul> 74 * <li><code>null</code> check for resource is omitted when it is 75 * initialized using <code>new</code></li> 76 * <li>synthetic method <code>$closeResource</code> containing 77 * <code>null</code> check of primaryExc and calls to methods 78 * <code>addSuppressed</code> and <code>close</code> is used when number of 79 * copies of closing logic reaches threshold, <code>null</code> check of 80 * resource (if present) is done before call of this method</li> 81 * </ul> 82 * During matching association between resource and slot of variable is done 83 * on exceptional path and is used to find close of resource on normal path. 84 * Order of loading variables primaryExc and r is different in different 85 * cases, which implies that this order should be determined before 86 * association. So {@link JavacPattern} defines all possible variants that 87 * will be tried sequentially. 88 */ 89 static class Matcher extends AbstractMatcher { 90 private final IFilterOutput output; 91 92 private String expectedOwner; 93 94 private AbstractInsnNode start; 95 96 Matcher(final IFilterOutput output) { 97 this.output = output; 98 } 99 100 private enum JavacPattern { 101 /** 102 * resource is loaded after primaryExc, <code>null</code> check of 103 * resource is omitted, method <code>$closeResource</code> is used 104 */ 105 OPTIMAL, 106 /** 107 * resource is loaded before primaryExc and both are checked on 108 * <code>null</code> 109 */ 110 FULL, 111 /** 112 * resource is loaded after primaryExc, <code>null</code> check of 113 * resource is omitted 114 */ 115 OMITTED_NULL_CHECK, 116 /** 117 * resource is loaded before primaryExc and checked on 118 * <code>null</code>, method <code>$closeResource</code> is used 119 */ 120 METHOD, 121 } 122 123 private void start(final AbstractInsnNode start) { 124 this.start = start; 125 cursor = start.getPrevious(); 126 vars.clear(); 127 expectedOwner = null; 128 } 129 130 private boolean matchJavac(final JavacPattern p) { 131 // "catch (Throwable t)" 132 nextIsVar(Opcodes.ASTORE, "t1"); 133 // "primaryExc = t" 134 nextIsVar(Opcodes.ALOAD, "t1"); 135 nextIsVar(Opcodes.ASTORE, "primaryExc"); 136 // "throw t" 137 nextIsVar(Opcodes.ALOAD, "t1"); 138 nextIs(Opcodes.ATHROW); 139 140 // "catch (any t)" 141 nextIsVar(Opcodes.ASTORE, "t2"); 142 nextIsJavacClose(p, "e"); 143 // "throw t" 144 nextIsVar(Opcodes.ALOAD, "t2"); 145 nextIs(Opcodes.ATHROW); 146 if (cursor == null) { 147 return false; 148 } 149 final AbstractInsnNode end = cursor; 150 151 AbstractInsnNode startOnNonExceptionalPath = start.getPrevious(); 152 cursor = startOnNonExceptionalPath; 153 while (!nextIsJavacClose(p, "n")) { 154 startOnNonExceptionalPath = startOnNonExceptionalPath 155 .getPrevious(); 156 cursor = startOnNonExceptionalPath; 157 if (cursor == null) { 158 return false; 159 } 160 } 161 startOnNonExceptionalPath = startOnNonExceptionalPath.getNext(); 162 163 final AbstractInsnNode m = cursor; 164 next(); 165 if (cursor.getOpcode() != Opcodes.GOTO) { 166 cursor = m; 167 } 168 169 output.ignore(startOnNonExceptionalPath, cursor); 170 output.ignore(start, end); 171 return true; 172 } 173 174 /** 175 * On a first invocation will associate variables with names "r" and 176 * "primaryExc", on subsequent invocations will use those associations 177 * for checks. 178 */ 179 private boolean nextIsJavacClose(final JavacPattern p, 180 final String ctx) { 181 switch (p) { 182 case METHOD: 183 case FULL: 184 // "if (r != null)" 185 nextIsVar(Opcodes.ALOAD, "r"); 186 nextIs(Opcodes.IFNULL); 187 } 188 switch (p) { 189 case METHOD: 190 case OPTIMAL: 191 nextIsVar(Opcodes.ALOAD, "primaryExc"); 192 nextIsVar(Opcodes.ALOAD, "r"); 193 nextIs(Opcodes.INVOKESTATIC); 194 if (cursor != null) { 195 final MethodInsnNode m = (MethodInsnNode) cursor; 196 if ("$closeResource".equals(m.name) 197 && "(Ljava/lang/Throwable;Ljava/lang/AutoCloseable;)V" 198 .equals(m.desc)) { 199 return true; 200 } 201 cursor = null; 202 } 203 return false; 204 case FULL: 205 case OMITTED_NULL_CHECK: 206 nextIsVar(Opcodes.ALOAD, "primaryExc"); 207 // "if (primaryExc != null)" 208 nextIs(Opcodes.IFNULL); 209 // "r.close()" 210 nextIsClose(); 211 nextIs(Opcodes.GOTO); 212 // "catch (Throwable t)" 213 nextIsVar(Opcodes.ASTORE, ctx + "t"); 214 // "primaryExc.addSuppressed(t)" 215 nextIsVar(Opcodes.ALOAD, "primaryExc"); 216 nextIsVar(Opcodes.ALOAD, ctx + "t"); 217 nextIsAddSuppressed(); 218 nextIs(Opcodes.GOTO); 219 // "r.close()" 220 nextIsClose(); 221 return cursor != null; 222 default: 223 throw new AssertionError(); 224 } 225 } 226 227 private void nextIsClose() { 228 nextIsVar(Opcodes.ALOAD, "r"); 229 next(); 230 if (cursor == null) { 231 return; 232 } 233 if (cursor.getOpcode() != Opcodes.INVOKEINTERFACE 234 && cursor.getOpcode() != Opcodes.INVOKEVIRTUAL) { 235 cursor = null; 236 return; 237 } 238 final MethodInsnNode m = (MethodInsnNode) cursor; 239 if (!"close".equals(m.name) || !"()V".equals(m.desc)) { 240 cursor = null; 241 return; 242 } 243 final String actual = m.owner; 244 if (expectedOwner == null) { 245 expectedOwner = actual; 246 } else if (!expectedOwner.equals(actual)) { 247 cursor = null; 248 } 249 } 250 251 } 252 253 } 254