Home | History | Annotate | Download | only in filter
      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