/* * A Gradle plugin for the creation of Minecraft mods and MinecraftForge plugins. * Copyright (C) 2013 Minecraft Forge * * This library 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 2.1 of the License, or (at your option) any later version. * * This library 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 library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 * USA */ package net.minecraftforge.gradle.util.mcp; import java.util.Arrays; import java.util.List; import net.minecraftforge.gradle.common.Constants; import com.google.code.regexp.Matcher; import com.google.code.regexp.Pattern; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Lists; public class FFPatcher { static final String MODIFIERS = "public|protected|private|static|abstract|final|native|synchronized|transient|volatile|strictfp"; private static final Pattern SYNTHETICS = Pattern.compile("(?m)(\\s*// \\$FF: (synthetic|bridge) method(\\r\\n|\\n|\\r)){1,2}\\s*(?<modifiers>(?:(?:" + MODIFIERS + ") )*)(?<return>.+?) (?<method>.+?)\\((?<arguments>.*)\\)\\s*\\{(\\r\\n|\\n|\\r)\\s*return this\\.(?<method2>.+?)\\((?<arguments2>.*)\\);(\\r\\n|\\n|\\r)\\s*\\}"); //private static final Pattern TYPECAST = Pattern.compile("\\([\\w\\.]+\\)"); private static final Pattern ABSTRACT = Pattern.compile("(?m)^(?<indent>[ \\t\\f\\v]*)(?<modifiers>(?:(?:" + MODIFIERS + ") )*)(?<return>[^ ]+) (?<method>func_(?<number>\\d+)_[a-zA-Z_]+)\\((?<arguments>([^ ,]+ (\\.\\.\\. )?var\\d+,? ?)*)\\)(?: throws (?:[\\w$.]+,? ?)+)?;$"); // Remove TRAILING whitespace private static final String TRAILING = "(?m)[ \\t]+$"; //Remove repeated blank lines private static final String NEWLINES = "(?m)^(\\r\\n|\\r|\\n){2,}"; private static final String EMPTY_SUPER = "(?m)^[ \t]+super\\(\\);(\\r\\n|\\n|\\r)"; // strip TRAILING 0 from doubles and floats to fix decompile differences on OSX // 0.0010D => 0.001D // value, type private static final String TRAILINGZERO = "([0-9]+\\.[0-9]*[1-9])0+([DdFfEe])"; // new regexes private static final String CLASS_REGEX = "(?<modifiers>(?:(?:" + MODIFIERS + ") )*)(?<type>enum|class|interface) (?<name>[\\w$]+)(?: (extends|implements) (?:[\\w$.]+(?:, [\\w$.]+)*))* \\{"; private static final String ENUM_ENTRY_REGEX = "(?<name>[\\w$]+)\\(\"(?:[\\w$]+)\", [0-9]+(?:, (?<body>.*?))?\\)(?<end> *(?:;|,|\\{)$)"; private static final String CONSTRUCTOR_REGEX = "(?<modifiers>(?:(?:" + MODIFIERS + ") )*)%s\\((?<parameters>.*?)\\)(?<end>(?: throws (?<throws>[\\w$.]+(?:, [\\w$.]+)*))? *(?:\\{\\}| \\{))"; private static final String CONSTRUCTOR_CALL_REGEX = "(?<name>this|super)\\((?<body>.*?)\\)(?<end>;)"; private static final String VALUE_FIELD_REGEX = "private static final %s\\[\\] [$\\w\\d]+ = new %s\\[\\]\\{.*?\\};"; public static String processFile(String text) { StringBuffer out = new StringBuffer(); Matcher m = SYNTHETICS.matcher(text); while(m.find()) { m.appendReplacement(out, synthetic_replacement(m).replace("$", "\\$")); } m.appendTail(out); text = out.toString(); text = text.replaceAll(TRAILING, ""); text = text.replaceAll(TRAILINGZERO, "$1$2"); List<String> lines = Lists.newArrayList(Constants.lines(text)); processClass(lines, "", 0, "", ""); // mutates the list text = Joiner.on(Constants.NEWLINE).join(lines); text = text.replaceAll(NEWLINES, Constants.NEWLINE); text = text.replaceAll(EMPTY_SUPER, ""); // fix interfaces (added 1.7.10+) out = new StringBuffer(); m = ABSTRACT.matcher(text); while (m.find()) { m.appendReplacement(out, abstract_replacement(m).replace("$", "\\$")); } m.appendTail(out); return out.toString(); } private static int processClass(List<String> lines, String indent, int startIndex, String qualifiedName, String simpleName) { Pattern classPattern = Pattern.compile(indent + CLASS_REGEX); for (int i = startIndex; i < lines.size(); i++) { String line = lines.get(i); // who knows..... if (Strings.isNullOrEmpty(line)) continue; // ignore packages and imports else if (line.startsWith("package") || line.startsWith("import")) continue; Matcher matcher = classPattern.matcher(line); // found a class! if (matcher.find()) { String newIndent; String classPath; if (Strings.isNullOrEmpty(qualifiedName)) { classPath = matcher.group("name"); newIndent = indent; } else { classPath = qualifiedName + "." + matcher.group("name"); newIndent = indent+ " "; } // fund an enum class, parse it seperately if (matcher.group("type").equals("enum")) processEnum(lines, newIndent, i+1, classPath, matcher.group("name")); // nested class searching i = processClass(lines, newIndent, i+1, classPath, matcher.group("name")); } // class has finished if (line.startsWith(indent + "}")) return i; } return 0; } private static void processEnum(List<String> lines, String indent, int startIndex, String qualifiedName, String simpleName) { String newIndent = indent + " "; Pattern enumEntry = Pattern.compile("^" + newIndent + ENUM_ENTRY_REGEX); Pattern constructor = Pattern.compile("^" + newIndent + String.format(CONSTRUCTOR_REGEX, simpleName)); Pattern constructorCall = Pattern.compile("^" + newIndent + " " + CONSTRUCTOR_CALL_REGEX); String formatted = newIndent + String.format(VALUE_FIELD_REGEX, qualifiedName, qualifiedName); Pattern valueField = Pattern.compile("^" + formatted); String newLine; boolean prevSynthetic = false; for (int i = startIndex; i < lines.size(); i++) { newLine = null; String line = lines.get(i); // find and replace enum entries Matcher matcher = enumEntry.matcher(line); if (matcher.find()) { String body = matcher.group("body"); newLine = newIndent + matcher.group("name"); if (!Strings.isNullOrEmpty(body)) { String[] args = body.split(", "); if (line.endsWith("{")) { if (args[args.length - 1].equals("null")) { args = Arrays.copyOf(args, args.length - 1); } } body = Joiner.on(", ").join(args); } if (Strings.isNullOrEmpty(body)) newLine += matcher.group("end"); else newLine += "(" + body + ")" + matcher.group("end"); } // find and replace constructor matcher = constructor.matcher(line); if (matcher.find()) { StringBuilder tmp = new StringBuilder(); tmp.append(newIndent).append(matcher.group("modifiers")).append(simpleName).append("("); String[] args = matcher.group("parameters").split(", "); for(int x = 2; x < args.length; x++) tmp.append(args[x]).append(x < args.length - 1 ? ", " : ""); tmp.append(")"); tmp.append(matcher.group("end")); newLine = tmp.toString(); if (args.length <= 2 && newLine.endsWith("}")) newLine = ""; } // find constructor calls... matcher = constructorCall.matcher(line); if (matcher.find()) { String body = matcher.group("body"); if (!Strings.isNullOrEmpty(body)) { String[] args = body.split(", "); args = Arrays.copyOfRange(args, 2, args.length); body = Joiner.on(", ").join(args); } newLine = newIndent + " " + matcher.group("name") + "(" + body + ")" + matcher.group("end"); } if (prevSynthetic) { matcher = valueField.matcher(line); if (matcher.find()) newLine = ""; } if (line.contains("// $FF: synthetic field")) { newLine = ""; prevSynthetic = true; } else prevSynthetic = false; if (newLine != null) lines.set(i, newLine); // class has finished. if (line.startsWith(indent + "}")) break; } } private static String synthetic_replacement(Matcher match) { //This is designed to remove all the synthetic/bridge methods that the compiler will just generate again //First off this only works on methods that bounce to methods that are named exactly alike. if (!match.group("method").equals(match.group("method2"))) return match.group(); //Next, we normalize the arugment list, if the lists are the same then it's a simple bounce method. //MC's code strips generic information so the compiler doesn't know to regen typecast methods //Uncomment the two lines below if we ever inject generic info String arg1 = match.group("arguments"); String arg2 = match.group("arguments2"); //String arg1 = _REGEXP['typecast'].sub(r'', match.group('arguments')) //String arg2 = _REGEXP['typecast'].sub(r'', match.group('arguments2')) if (arg1.equals(arg2) && arg1.equals("")) return ""; String[] args = match.group("arguments").split(", "); for (int x = 0; x < args.length; x++) args[x] = args[x].split(" ")[1]; StringBuilder b = new StringBuilder(); b.append(args[0]); for (int x = 1; x < args.length; x++) b.append(", ").append(args[x]); arg1 = b.toString(); if (arg1.equals(arg2)) return ""; return match.group(); } private static String abstract_replacement(Matcher match) { String orig = match.group("arguments"); String number = match.group("number"); if (Strings.isNullOrEmpty(orig)) return match.group(); String[] args = orig.split(", "); StringBuilder fixed = new StringBuilder(); for (int x = 0; x < args.length; x++) { String[] p = args[x].split(" "); if (p.length == 3) //varargs { p[0] = p[0] + " " + p[1]; p[1] = p[2]; } fixed.append(p[0]).append(" p_").append(number).append('_').append(p[1].substring(3)).append('_'); if (x != args.length - 1) fixed.append(", "); } return match.group().replace(orig, fixed.toString()); } }