package net.minecraftforge.forge.tasks import org.gradle.api.DefaultTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.TaskAction import java.nio.file.Files import java.nio.file.Paths import java.util.regex.Pattern public class CheckPatches extends DefaultTask { @InputDirectory File patchDir @Input boolean autoFix = false @TaskAction protected void exec() { def hasS2SArtifact = [ Paths.get("patches/minecraft/net/minecraft/client/renderer/ViewFrustum.java.patch"), Paths.get("patches/minecraft/net/minecraft/data/BlockModelDefinition.java.patch") ] def verified = true; project.fileTree(patchDir).each { patch -> def patchPath = project.rootDir.toPath().relativize(patch.toPath()) verified &= verifyPatch(patch, autoFix, patchPath.toString(), hasS2SArtifact.contains(patchPath)) } if (!verified) throw new RuntimeException('One or more patches failed verification. Check the log for errors.') } def verifyPatch(patch, fix, patchPath, hasS2SArtifact) { def hunk_start_pattern = Pattern.compile('^@@ -[0-9,]* \\+[0-9,]* @@$') def white_space_pattern = Pattern.compile('^[+\\-]\\s*$') def import_pattern = Pattern.compile('^[+\\-]\\s*import.*') def field_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^=;]*)(=.*)?;\\s*$') def method_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final)?([^(]*)[(]([^)]*)?[)]\\s*[{]\\s*$') def class_pattern = Pattern.compile('^[+\\-][\\s]*((public|protected|private)[\\s]*)?(static[\\s]*)?(final[\\s]*)?(class|interface)([^{]*)[{]\\s*$') def accessMap = [("private"):0, (null):1, ("protected"):2, ("public"):3] def hasProblem = false; def lines = patch.readLines() def hunksStart = 0 def onlyWhiteSpace = false def didFix = false def newLines = [] def whiteSpaceErrors = [] // First two lines are file name ++/-- and we do not care newLines.add(lines[0] + '\n') newLines.add(lines[1] + '\n') int i = 2 for (; i < lines.size(); ++i) { def line = lines[i] newLines.add(line + '\n') if (hunk_start_pattern.matcher(line).find()) { if (onlyWhiteSpace) { if (fix || hasS2SArtifact) { logger.lifecycle("Removing white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath) def toRemove = i - hunksStart; while (toRemove-- > 0) newLines.remove(newLines.size() - 1) didFix = true } else { logger.lifecycle("Patch contains only white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath) hasProblem = true } } else { if (!whiteSpaceErrors.empty) hasProblem = true whiteSpaceErrors.each { error -> logger.lifecycle(error) } whiteSpaceErrors.clear() } hunksStart = i onlyWhiteSpace = true continue } if (line.startsWithAny('+','-')) { def prefixChange = false def prevLine = lines[i - 1] if (line.charAt(0) == (char)'+' && prevLine.charAt(0) == (char)'-') { def prevTrim = prevLine.substring(1).replaceAll("\\s", "") def currTrim = line.substring(1).replaceAll("\\s", "") if (prevTrim.equals(currTrim)) { prefixChange = true } def pMatcher = field_pattern.matcher(prevLine) def cMatcher = field_pattern.matcher(line) if (pMatcher.find() && cMatcher.find() && pMatcher.group(6) == cMatcher.group(6) && // = ... pMatcher.group(5) == cMatcher.group(5) && // field name pMatcher.group(3) == cMatcher.group(3) && // static (accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) { logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath) hasProblem = true } pMatcher = method_pattern.matcher(prevLine) cMatcher = method_pattern.matcher(line) if (pMatcher.find() && cMatcher.find() && pMatcher.group(6) == cMatcher.group(6) && // params pMatcher.group(5) == cMatcher.group(5) && // void name pMatcher.group(3) == cMatcher.group(3) && // static (accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) { logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath) hasProblem = true } pMatcher = class_pattern.matcher(prevLine) cMatcher = class_pattern.matcher(line) if (pMatcher.find() && cMatcher.find() && pMatcher.group(6) == cMatcher.group(6) && // ClassName<> extends ... pMatcher.group(5) == cMatcher.group(5) && // class | interface pMatcher.group(3) == cMatcher.group(3) && // static (accessMap[pMatcher.group(2)] < accessMap[cMatcher.group(2)] || pMatcher.group(4) != cMatcher.group(4))) { logger.lifecycle("Patch contains access changes or final removal at line {}, file: {}", i + 1, patchPath) hasProblem = true } } if (line.charAt(0) == (char)'-' && i + 1 < lines.size()) { def nextLine = lines[i + 1] if (nextLine.charAt(0) == (char)'+') { def nextTrim = nextLine.substring(1).replaceAll("\\s", "") def currTrim = line.substring(1).replaceAll("\\s", "") if (nextTrim.equals(currTrim)) { prefixChange = true } } } def isWhiteSpaceChange = white_space_pattern.matcher(line).find() if (!prefixChange && !isWhiteSpaceChange) { onlyWhiteSpace = hasS2SArtifact && import_pattern.matcher(line).find() } else if (isWhiteSpaceChange) { def prevLineChange = prevLine.startsWithAny('+','-') def nextLineChange = i + 1 < lines.size() && lines[i + 1].startsWithAny('+','-') if (!prevLineChange && !nextLineChange) { whiteSpaceErrors.add(String.format("Patch contains white space change in valid hunk at line %d (cannot auto fix), file: %s", i + 1, patchPath)) } } if (line.contains("\t")) { if (!fix) { logger.lifecycle("Patch contains tabs on line {}, file: {}", i + 1, patchPath) hasProblem = true } else { logger.lifecycle("Fixing tabs on line {}, file: {}", i + 1, patchPath) line = line.replaceAll('\t', ' ') newLines.remove(newLines.size() - 1) newLines.add(line + '\n') didFix = true } } if (import_pattern.matcher(line).find() && !hasS2SArtifact) { logger.lifecycle("Patch contains import change on line {}, file: {}", i + 1, patchPath) hasProblem = true } } } if (onlyWhiteSpace) { if (fix || hasS2SArtifact) { logger.lifecycle("Removing white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath) def toRemove = i - hunksStart; while (toRemove-- > 0) newLines.remove(newLines.size() - 1) didFix = true } else { logger.lifecycle("Patch contains only white space hunk starting at line {}, file: {}", hunksStart + 1, patchPath) hasProblem = true } } else { if (!whiteSpaceErrors.empty) hasProblem = true whiteSpaceErrors.each { error -> logger.lifecycle(error) } } if (didFix) { if (newLines.size() <= 2) { logger.lifecycle("Patch is now empty removing, file: {}", patchPath) Files.delete(patch.toPath()) } else { if (!hasS2SArtifact) logger.lifecycle("*** Updating patch file. Please run setup then genPatches again. ***") patch.withWriter('UTF-8') { writer -> newLines.each { l -> writer.write(l) } } } } return !hasProblem } }