/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.jjs.impl; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.dev.PrecompileTaskOptions; import com.google.gwt.dev.cfg.ConfigurationProperties; import com.google.gwt.dev.jjs.ast.Context; import com.google.gwt.dev.jjs.ast.JClassType; import com.google.gwt.dev.jjs.ast.JConstructor; import com.google.gwt.dev.jjs.ast.JDeclaredType; import com.google.gwt.dev.jjs.ast.JExpression; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodCall; import com.google.gwt.dev.jjs.ast.JModVisitor; import com.google.gwt.dev.jjs.ast.JNewInstance; import com.google.gwt.dev.jjs.ast.JNullLiteral; import com.google.gwt.dev.jjs.ast.JParameter; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JVisitor; import com.google.gwt.dev.jjs.ast.RuntimeConstants; import com.google.gwt.thirdparty.guava.common.collect.Lists; import java.util.Collection; import java.util.List; /** * Performs optimizations on Enums. */ public class EnumNameObfuscator { private static final String ENUM_NAME_OBFUSCATION_PROPERTY = "compiler.enum.obfuscate.names"; private static final String ENUM_NAME_OBFUSCATION_BLACKLIST_PROPERTY = "compiler.enum.obfuscate.names.blacklist"; private static class EnumNameCallChecker extends JVisitor { private final JDeclaredType classType; private final JMethod enumNameMethod; private final JMethod enumToStringMethod; private final JDeclaredType enumType; private final JMethod enumValueOfMethod; private final TreeLogger logger; private final List<String> blacklistedEnums; private final JDeclaredType stringType; private EnumNameCallChecker(JProgram jprogram, TreeLogger logger, List<String> blacklistedEnums) { this.logger = logger; this.blacklistedEnums = blacklistedEnums; this.enumNameMethod = jprogram.getIndexedMethod(RuntimeConstants.ENUM_NAME); this.enumToStringMethod = jprogram.getIndexedMethod(RuntimeConstants.ENUM_TO_STRING); this.classType = jprogram.getFromTypeMap("java.lang.Class"); this.enumType = jprogram.getFromTypeMap("java.lang.Enum"); this.stringType = jprogram.getFromTypeMap("java.lang.String"); /* * Find the correct version of enumValueOfMethod. * * Note: it doesn't work to check against a ref returned by * jprogram.getIndexedMethod("Enum.valueOf"), since there are 2 different * versions of Enum.valueOf in our jre emulation library, and the indexed * ref won't reliably flag the public instance, which is the one we want * here (i.e. Enum.valueOf(Class<T>,String)). The other version is * protected, but is called by the generated constructors for sub-classes * of Enum, and we don't want to warn for those cases. */ JMethod foundMethod = null; List<JMethod> enumMethods = enumType.getMethods(); for (JMethod enumMethod : enumMethods) { List<JParameter> params = enumMethod.getParams(); if (enumMethod.getName().equals("valueOf") && params.size() == 2 && params.get(0).getType() == classType && params.get(1).getType() == stringType) { foundMethod = enumMethod; break; } } this.enumValueOfMethod = foundMethod; } @Override public void endVisit(JMethodCall x, Context ctx) { JMethod target = x.getTarget(); JDeclaredType type = target.getEnclosingType(); if (!(type instanceof JClassType)) { return; } JClassType cType = (JClassType) type; if (typeInBlacklist(blacklistedEnums, cType)) { return; } if (target == enumNameMethod || target == enumToStringMethod || target == enumValueOfMethod) { warn(x); return; } if (cType.isEnumOrSubclass() != null) { return; } /* * Check for calls to the auto-generated * EnumSubType.valueOf(String). Note, the check of the signature for * the single String arg version is to avoid flagging user-defined * overloaded versions of the method, which are presumably ok. */ List<JParameter> params = target.getParams(); if (target.getName().equals("valueOf") && params.size() == 1 && params.get(0).getType() == stringType) { warn(x); } } @Override public boolean visit(JClassType x, Context ctx) { if (x == enumType) { // don't traverse into Enum class itself, don't warn on internal method // calls return false; } return true; } private void warn(JMethodCall x) { /* * TODO: add a way to suppress warning with annotation if you know what * you're doing. */ logger.log(TreeLogger.WARN, "Call to Enum method " + x.getTarget().getName() + " when enum obfuscation is enabled: " + x.getSourceInfo().getFileName() + ":" + x.getSourceInfo().getStartLine()); } } private static class EnumNameReplacer extends JModVisitor { private final JProgram jprogram; private final JMethod makeEnumName; private List<String> blacklistedEnums; private boolean closureMode; private final TreeLogger logger; private EnumNameReplacer(JProgram jprogram, TreeLogger logger, List<String> blacklistedEnums, boolean closureMode) { this.logger = logger; this.jprogram = jprogram; this.blacklistedEnums = blacklistedEnums; this.closureMode = closureMode; this.makeEnumName = jprogram.getIndexedMethod(RuntimeConstants.UTIL_MAKE_ENUM_NAME); } private void exec() { accept(jprogram); } @Override public boolean visit(JClassType x, Context ctx) { if (x.isEnumOrSubclass() == null || typeInBlacklist(blacklistedEnums, x)) { return false; } trace(x); return true; } @Override public void endVisit(JNewInstance x, Context ctx) { JConstructor target = x.getTarget(); JClassType enclosingType = target.getEnclosingType(); if (enclosingType.isEnumOrSubclass() == null) { return; } if (typeInBlacklist(blacklistedEnums, enclosingType)) { return; } JNewInstance newEnum = new JNewInstance(x); List<JExpression> args = Lists.newArrayList(x.getArgs()); // replace first argument with null or the closure obfuscation function. args.set(0, closureMode ? new JMethodCall(x.getSourceInfo(), null, makeEnumName, args.get(0)) : JNullLiteral.INSTANCE); newEnum.addArgs(args); ctx.replaceMe(newEnum); } private void trace(JClassType x) { if (logger.isLoggable(TreeLogger.TRACE)) { logger.log(TreeLogger.TRACE, "Obfuscating enum " + x + x.getSourceInfo().getFileName() + ":" + x.getSourceInfo().getStartLine()); } } } private static boolean typeInBlacklist(Collection<String> blacklistedEnums, JClassType cType) { return blacklistedEnums.contains(cType.getName().replace('$', '.')); } public static void exec(JProgram jprogram, TreeLogger logger, ConfigurationProperties configurationProperties, PrecompileTaskOptions options) { if (!configurationProperties.getBoolean(ENUM_NAME_OBFUSCATION_PROPERTY, false)) { return; } boolean closureMode = options.isClosureCompilerFormatEnabled(); List<String> blacklistedEnums = configurationProperties.getCommaSeparatedStrings(ENUM_NAME_OBFUSCATION_BLACKLIST_PROPERTY); new EnumNameCallChecker(jprogram, logger, blacklistedEnums).accept(jprogram); new EnumNameReplacer(jprogram, logger, blacklistedEnums, closureMode).exec(); } }