diff options
author | Jonas Herzig <me@johni0702.de> | 2019-06-17 11:17:16 +0200 |
---|---|---|
committer | Jonas Herzig <me@johni0702.de> | 2019-06-17 11:30:42 +0200 |
commit | 18a9a9d7b048e11981d2e3bf47af17e024e0ebef (patch) | |
tree | 784850cf83955f7b2ed6fa9b9aa4fccc046a5c4b /src | |
parent | b6acd65e44395ecefb9bf5b0e9069eddb17356fe (diff) | |
download | Remap-18a9a9d7b048e11981d2e3bf47af17e024e0ebef.tar.gz Remap-18a9a9d7b048e11981d2e3bf47af17e024e0ebef.tar.bz2 Remap-18a9a9d7b048e11981d2e3bf47af17e024e0ebef.zip |
Re-write to use Intellij's PSI (via kotlin-compiler) instead of ECJ
Primarily for potential kotlin support.
Secondarily, this allows us to drop the EPL (kotlin-compiler is ASL2.0).
This also fixes an issue where redirect/inject methods with names
identical to remapped names in the target class would get renamed.
This also seems to implement the implicit member reference check (to
prevent accidental name shadowing after remapping) more thoroughly, at
least it finds some valid cases which the previous implementation has
ignored.
Diffstat (limited to 'src')
-rw-r--r-- | src/main/java/com/replaymod/gradle/remap/PsiMapper.java | 396 | ||||
-rw-r--r-- | src/main/java/com/replaymod/gradle/remap/Transformer.java | 524 |
2 files changed, 457 insertions, 463 deletions
diff --git a/src/main/java/com/replaymod/gradle/remap/PsiMapper.java b/src/main/java/com/replaymod/gradle/remap/PsiMapper.java new file mode 100644 index 0000000..17d0783 --- /dev/null +++ b/src/main/java/com/replaymod/gradle/remap/PsiMapper.java @@ -0,0 +1,396 @@ +package com.replaymod.gradle.remap; + +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.JavaRecursiveElementVisitor; +import com.intellij.psi.PsiAnnotation; +import com.intellij.psi.PsiAnnotationMemberValue; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassObjectAccessExpression; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiIdentifier; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiNameValuePair; +import com.intellij.psi.PsiPackage; +import com.intellij.psi.PsiQualifiedNamedElement; +import com.intellij.psi.PsiSwitchLabelStatement; +import com.intellij.psi.PsiTypeElement; +import com.replaymod.gradle.remap.Transformer.Mapping; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +class PsiMapper { + private static final String CLASS_MIXIN = "org.spongepowered.asm.mixin.Mixin"; + private static final String CLASS_ACCESSOR = "org.spongepowered.asm.mixin.gen.Accessor"; + private static final String CLASS_AT = "org.spongepowered.asm.mixin.injection.At"; + private static final String CLASS_INJECT = "org.spongepowered.asm.mixin.injection.Inject"; + private static final String CLASS_REDIRECT = "org.spongepowered.asm.mixin.injection.Redirect"; + + private final Map<String, Mapping> map; + private final Map<String, Mapping> mixinMappings = new HashMap<>(); + private final PsiFile file; + private boolean error; + private TreeMap<TextRange, String> changes = new TreeMap<>(Comparator.comparing(TextRange::getStartOffset)); + + PsiMapper(Map<String, Mapping> map, PsiFile file) { + this.map = map; + this.file = file; + } + + private void replace(PsiElement e, String with) { + changes.put(e.getTextRange(), with); + } + + private void replaceIdentifier(PsiElement parent, String with) { + for (PsiElement child : parent.getChildren()) { + if (child instanceof PsiIdentifier) { + replace(child, with); + return; + } + } + } + + private boolean valid(PsiElement e) { + TextRange range = e.getTextRange(); + TextRange before = changes.ceilingKey(range); + return before == null || !before.intersects(range); + } + + private String getResult(String text) { + if (error) { + return null; + } + for (Map.Entry<TextRange, String> change : changes.descendingMap().entrySet()) { + text = change.getKey().replace(text, change.getValue()); + } + return text; + } + + private static boolean isSwitchCase(PsiElement e) { + if (e instanceof PsiSwitchLabelStatement) { + return true; + } + PsiElement parent = e.getParent(); + return parent != null && isSwitchCase(parent); + } + + private void map(PsiElement expr, PsiField field) { + PsiClass declaringClass = field.getContainingClass(); + if (declaringClass == null) return; + String name = declaringClass.getQualifiedName(); + if (name == null) return; + Mapping mapping = this.mixinMappings.get(name); + if (mapping == null) { + mapping = map.get(name); + } + if (mapping == null) return; + String mapped = mapping.fields.get(field.getName()); + if (mapped == null || mapped.equals(field.getName())) return; + replaceIdentifier(expr, mapped); + + if (expr instanceof PsiJavaCodeReferenceElement + && !((PsiJavaCodeReferenceElement) expr).isQualified() // qualified access is fine + && !isSwitchCase(expr) // referencing constants in case statements is fine + ) { + int line = StringUtil.offsetToLineNumber(file.getText(), expr.getTextOffset()); + System.err.println(file.getName() + ":" + line + ": Implicit member reference to remapped field \"" + field.getName() + "\". " + + "This can cause issues if the remapped reference becomes shadowed by a local variable and is therefore forbidden. " + + "Use \"this." + field.getName() + "\" instead."); + error = true; + } + } + + private void map(PsiElement expr, PsiMethod method) { + PsiClass declaringClass = method.getContainingClass(); + if (declaringClass == null) return; + ArrayDeque<PsiClass> parentQueue = new ArrayDeque<>(); + parentQueue.offer(declaringClass); + Mapping mapping = null; + + String name = declaringClass.getQualifiedName(); + if (name != null) { + mapping = mixinMappings.get(name); + } + while (true) { + if (mapping != null) { + String mapped = mapping.methods.get(method.getName()); + if (mapped != null) { + if (!mapped.equals(method.getName())) { + replaceIdentifier(expr, mapped); + } + return; + } + mapping = null; + } + while (mapping == null) { + declaringClass = parentQueue.poll(); + if (declaringClass == null) return; + + PsiClass superClass = declaringClass.getSuperClass(); + if (superClass != null) { + parentQueue.offer(superClass); + } + for (PsiClass anInterface : declaringClass.getInterfaces()) { + parentQueue.offer(anInterface); + } + + name = declaringClass.getQualifiedName(); + if (name == null) continue; + mapping = map.get(name); + } + } + } + + private void map(PsiElement expr, PsiQualifiedNamedElement resolved) { + String name = resolved.getQualifiedName(); + if (name == null) return; + Mapping mapping = map.get(name); + if (mapping == null) return; + String mapped = mapping.newName; + if (mapped.equals(name)) return; + + if (expr.getText().equals(name)) { + replace(expr, mapped); + return; + } + replaceIdentifier(expr, mapped.substring(mapped.lastIndexOf('.') + 1)); + } + + private void map(PsiElement expr, PsiElement resolved) { + if (resolved instanceof PsiField) { + map(expr, (PsiField) resolved); + } else if (resolved instanceof PsiMethod) { + map(expr, (PsiMethod) resolved); + } else if (resolved instanceof PsiClass || resolved instanceof PsiPackage) { + map(expr, (PsiQualifiedNamedElement) resolved); + } + } + + // Note: Supports only Mixins with a single target (ignores others) and only ones specified via class literals + private PsiClass getMixinTarget(PsiAnnotation annotation) { + for (PsiNameValuePair pair : annotation.getParameterList().getAttributes()) { + String name = pair.getName(); + if (name != null && !"value".equals(name)) continue; + PsiAnnotationMemberValue value = pair.getValue(); + if (!(value instanceof PsiClassObjectAccessExpression)) continue; + PsiTypeElement type = ((PsiClassObjectAccessExpression) value).getOperand(); + PsiJavaCodeReferenceElement reference = type.getInnermostComponentReferenceElement(); + if (reference == null) continue; + return (PsiClass) reference.resolve(); + } + return null; + } + + private void remapAccessors(Mapping mapping) { + file.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitMethod(PsiMethod method) { + PsiAnnotation annotation = method.getAnnotation(CLASS_ACCESSOR); + if (annotation == null) return; + + String targetByName = method.getName(); + if (targetByName.startsWith("is")) { + targetByName = targetByName.substring(2); + } else if (targetByName.startsWith("get") || targetByName.startsWith("set")) { + targetByName = targetByName.substring(3); + } else { + targetByName = null; + } + if (targetByName != null) { + targetByName = targetByName.substring(0, 1).toLowerCase() + targetByName.substring(1); + } + + String target = Arrays.stream(annotation.getParameterList().getAttributes()) + .filter(it -> it.getName() == null || it.getName().equals("value")) + .map(PsiNameValuePair::getLiteralValue) + .findAny() + .orElse(targetByName); + + if (target == null) { + throw new IllegalArgumentException("Cannot determine accessor target for " + method); + } + + String mapped = mapping.fields.get(target); + if (mapped != null && !mapped.equals(target)) { + // Update accessor target + String parameterList; + if (mapped.equals(targetByName)) { + // Mapped name matches implied target, can just remove the explict target + parameterList = ""; + } else { + // Mapped name does not match implied target, need to set the target as annotation value + parameterList = "(\"" + StringUtil.escapeStringCharacters(mapped) + "\")"; + } + replace(annotation.getParameterList(), parameterList); + } + } + }); + } + + private void remapInjectsAndRedirects(Mapping mapping) { + file.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitMethod(PsiMethod method) { + PsiAnnotation annotation = method.getAnnotation(CLASS_INJECT); + if (annotation == null) { + annotation = method.getAnnotation(CLASS_REDIRECT); + } + if (annotation == null) return; + + for (PsiNameValuePair attribute : annotation.getParameterList().getAttributes()) { + if (!"method".equals(attribute.getName())) continue; + // Note: mixin supports multiple targets, we do not (yet) + String literalValue = attribute.getLiteralValue(); + if (literalValue == null) continue; + String mapped = mapping.methods.get(literalValue); + if (mapped != null && !mapped.equals(literalValue)) { + PsiAnnotationMemberValue value = attribute.getValue(); + assert value != null; + replace(value, '"' + mapped + '"'); + } + } + } + }); + } + + private Mapping remapInternalType(String internalType, StringBuilder result) { + if (internalType.charAt(0) == 'L') { + String type = internalType.substring(1, internalType.length() - 1).replace('/', '.'); + Mapping mapping = map.get(type); + if (mapping != null) { + result.append('L').append(mapping.newName.replace('.', '/')).append(';'); + return mapping; + } + } + result.append(internalType); + return null; + } + + private String remapFullyQualifiedMethodOrField(String signature) { + int ownerEnd = signature.indexOf(';'); + int argsBegin = signature.indexOf('('); + int argsEnd = signature.indexOf(')'); + boolean method = argsBegin != -1; + if (!method) { + argsBegin = argsEnd = signature.indexOf(':'); + } + String owner = signature.substring(0, ownerEnd + 1); + String name = signature.substring(ownerEnd + 1, argsBegin); + String returnType = signature.substring(argsEnd + 1); + + StringBuilder builder = new StringBuilder(signature.length() + 32); + Mapping mapping = remapInternalType(owner, builder); + String mapped = null; + if (mapping != null) { + mapped = (method ? mapping.methods : mapping.fields).get(name); + } + builder.append(mapped != null ? mapped : name); + if (method) { + builder.append('('); + String args = signature.substring(argsBegin + 1, argsEnd); + for (int i = 0; i < args.length(); i++) { + char c = args.charAt(i); + if (c != 'L') { + builder.append(c); + continue; + } + int end = args.indexOf(';', i); + String arg = args.substring(i, end + 1); + remapInternalType(arg, builder); + i = end; + } + builder.append(')'); + } else { + builder.append(':'); + } + remapInternalType(returnType, builder); + return builder.toString(); + } + + private void remapAtTargets() { + file.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitAnnotation(PsiAnnotation annotation) { + if (!CLASS_AT.equals(annotation.getQualifiedName())) { + super.visitAnnotation(annotation); + return; + } + + for (PsiNameValuePair attribute : annotation.getParameterList().getAttributes()) { + if (!"target".equals(attribute.getName())) continue; + String signature = attribute.getLiteralValue(); + if (signature == null) continue; + String newSignature = remapFullyQualifiedMethodOrField(signature); + if (!newSignature.equals(signature)) { + PsiAnnotationMemberValue value = attribute.getValue(); + assert value != null; + replace(value, '"' + newSignature + '"'); + } + } + } + }); + } + + String remapFile() { + file.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitClass(PsiClass psiClass) { + PsiAnnotation annotation = psiClass.getAnnotation(CLASS_MIXIN); + if (annotation == null) return; + + remapAtTargets(); + + PsiClass target = getMixinTarget(annotation); + if (target == null) return; + + Mapping mapping = map.get(target.getQualifiedName()); + if (mapping == null) return; + + mixinMappings.put(psiClass.getQualifiedName(), mapping); + + if (!mapping.fields.isEmpty()) { + remapAccessors(mapping); + } + if (!mapping.methods.isEmpty()) { + remapInjectsAndRedirects(mapping); + } + } + }); + + file.accept(new JavaRecursiveElementVisitor() { + @Override + public void visitField(PsiField field) { + if (valid(field)) { + map(field, field); + } + super.visitField(field); + } + + @Override + public void visitMethod(PsiMethod method) { + if (valid(method)) { + map(method, method); + } + super.visitMethod(method); + } + + @Override + public void visitReferenceElement(PsiJavaCodeReferenceElement reference) { + if (valid(reference)) { + map(reference, reference.resolve()); + } + super.visitReferenceElement(reference); + } + }); + + return getResult(file.getText()); + } +} diff --git a/src/main/java/com/replaymod/gradle/remap/Transformer.java b/src/main/java/com/replaymod/gradle/remap/Transformer.java index e41867f..1cbd425 100644 --- a/src/main/java/com/replaymod/gradle/remap/Transformer.java +++ b/src/main/java/com/replaymod/gradle/remap/Transformer.java @@ -1,11 +1,28 @@ package com.replaymod.gradle.remap; -import org.eclipse.jdt.core.JavaCore; -import org.eclipse.jdt.core.compiler.IProblem; -import org.eclipse.jdt.core.dom.*; -import org.eclipse.jface.text.BadLocationException; -import org.eclipse.jface.text.Document; -import org.eclipse.text.edits.TextEdit; +import com.intellij.codeInsight.CustomExceptionHandler; +import com.intellij.mock.MockProject; +import com.intellij.openapi.extensions.ExtensionPoint; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.StandardFileSystems; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.local.CoreLocalFileSystem; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; +import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot; +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer; +import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector; +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; +import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace; +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM; +import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot; +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot; +import org.jetbrains.kotlin.config.CommonConfigurationKeys; +import org.jetbrains.kotlin.config.CompilerConfiguration; import java.io.BufferedReader; import java.io.File; @@ -15,14 +32,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.ArrayDeque; -import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -32,7 +46,7 @@ class Transformer { private String[] classpath; private boolean fail; - public static void main(String[] args) throws IOException, BadLocationException { + public static void main(String[] args) throws IOException { Map<String, Mapping> mappings; if (args[0].isEmpty()) { mappings = new HashMap<>(); @@ -93,21 +107,9 @@ class Transformer { this.classpath = classpath; } - public Map<String, String> remap(Map<String, String> sources) throws BadLocationException, IOException { - ASTParser parser = ASTParser.newParser(AST.JLS8); - Map<String, String> options = JavaCore.getDefaultOptions(); - JavaCore.setComplianceOptions("1.8", options); - parser.setCompilerOptions(options); - parser.setEnvironment(classpath, null, null, true); - parser.setResolveBindings(true); - parser.setBindingsRecovery(true); - + public Map<String, String> remap(Map<String, String> sources) throws IOException { Path tmpDir = Files.createTempDirectory("remap"); try { - Map<String, String> filePathToName = new HashMap<>(); - String[] compilationUnits = new String[sources.size()]; - String[] encodings = new String[compilationUnits.length]; - int i = 0; for (Entry<String, String> entry : sources.entrySet()) { String unitName = entry.getKey(); String source = entry.getValue(); @@ -115,456 +117,52 @@ class Transformer { Path path = tmpDir.resolve(unitName); Files.createDirectories(path.getParent()); Files.write(path, source.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); - String filePath = path.toString(); - filePathToName.put(filePath, unitName); - compilationUnits[i] = filePath; - encodings[i] = "UTF-8"; - - i++; } - Map<String, CompilationUnit> cus = new HashMap<>(); - parser.createASTs(compilationUnits, encodings, new String[0], new FileASTRequestor() { - @Override - public void acceptAST(String sourceFilePath, CompilationUnit cu) { - String unitName = filePathToName.get(sourceFilePath); - for (IProblem problem : cu.getProblems()) { - if (problem.isError()) { - System.err.println(unitName + ":" + problem.getSourceLineNumber() + ": " + problem.getMessage()); - } - } - cus.put(unitName, cu); - } - }, null); - Map<String, String> results = new HashMap<>(); - for (Entry<String, CompilationUnit> entry : cus.entrySet()) { - String unitName = entry.getKey(); - CompilationUnit cu = entry.getValue(); + CompilerConfiguration config = new CompilerConfiguration(); + config.put(CommonConfigurationKeys.MODULE_NAME, "main"); + config.add(CLIConfigurationKeys.CONTENT_ROOTS, new JavaSourceRoot(tmpDir.toFile(), "")); + config.add(CLIConfigurationKeys.CONTENT_ROOTS, new KotlinSourceRoot(tmpDir.toAbsolutePath().toString(), false)); + config.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, new PrintingMessageCollector(System.err, MessageRenderer.GRADLE_STYLE, true)); - cu.recordModifications(); - if (remapClass(unitName, cu)) { - Document document = new Document(sources.get(unitName)); - TextEdit edit = cu.rewrite(document, JavaCore.getDefaultOptions()); - edit.apply(document); - results.put(unitName, document.get()); - } else { - results.put(unitName, sources.get(unitName)); - } - } - return results; - } finally { - Files.walk(tmpDir).map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete); - } - } + KotlinCoreEnvironment environment = KotlinCoreEnvironment.createForProduction( + Disposer.newDisposable(), + config, + EnvironmentConfigFiles.JVM_CONFIG_FILES + ); + Extensions.getRootArea().registerExtensionPoint(CustomExceptionHandler.KEY.getName(), CustomExceptionHandler.class.getName(), ExtensionPoint.Kind.INTERFACE); - private static final String CLASS_MIXIN = "org.spongepowered.asm.mixin.Mixin"; - private static final String CLASS_ACCESSOR = "org.spongepowered.asm.mixin.gen.Accessor"; - private static final String CLASS_AT = "org.spongepowered.asm.mixin.injection.At"; - private static final String CLASS_INJECT = "org.spongepowered.asm.mixin.injection.Inject"; - private static final String CLASS_REDIRECT = "org.spongepowered.asm.mixin.injection.Redirect"; + MockProject project = (MockProject) environment.getProject(); - // Note: Supports only Mixins with a single target (ignores others) and only ones specified via class literals - private ITypeBinding getMixinTarget(IAnnotationBinding annotation) { - for (IMemberValuePairBinding pair : annotation.getDeclaredMemberValuePairs()) { - if (pair.getName().equals("value")) { - return (ITypeBinding) ((Object[]) pair.getValue())[0]; - } - } - return null; - } - - private boolean remapAccessors(CompilationUnit cu, Mapping mapping) { - AtomicBoolean changed = new AtomicBoolean(false); - AST ast = cu.getAST(); + environment.updateClasspath(Stream.of(getClasspath()).map(it -> new JvmClasspathRoot(new File(it))).collect(Collectors.toList())); - cu.accept(new ASTVisitor() { - @Override - public boolean visit(MethodDeclaration node) { - int annotationIndex = -1; - Annotation annotationNode = null; - IAnnotationBinding annotation = null; - for (int i = 0; i < node.modifiers().size(); i++) { - Object obj = node.modifiers().get(i); - if (!(obj instanceof Annotation)) { - continue; - } - annotationNode = (Annotation) obj; - annotation = annotationNode.resolveAnnotationBinding(); - if (annotation != null && annotation.getAnnotationType().getQualifiedName().equals(CLASS_ACCESSOR)) { - annotationIndex = i; - break; - } - } - if (annotationIndex == -1) return false; + TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( + project, + Collections.emptyList(), + new NoScopeRecordCliBindingTrace(), + environment.getConfiguration(), + environment::createPackagePartProvider + ); - String targetByName = node.getName().getIdentifier(); - if (targetByName.startsWith("is")) { - targetByName = targetByName.substring(2); - } else if (targetByName.startsWith("get") || targetByName.startsWith("set")) { - targetByName = targetByName.substring(3); - } else { - targetByName = null; - } - if (targetByName != null) { - targetByName = targetByName.substring(0, 1).toLowerCase() + targetByName.substring(1); - } - - String target = Arrays.stream(annotation.getDeclaredMemberValuePairs()) - .filter(it -> it.getName().equals("value")) - .map(it -> (String) it.getValue()) - .findAny() - .orElse(targetByName); - - if (target == null) { - throw new IllegalArgumentException("Cannot determine accessor target for " + node); - } - - String mapped = mapping.fields.get(target); - if (mapped != null && !mapped.equals(target)) { - - Annotation newAnnotation; - - // Update accessor target - if (mapped.equals(targetByName)) { - // Mapped name matches implied target, can just remove the explict target - newAnnotation = ast.newMarkerAnnotation(); - } else { - // Mapped name does not match implied target, need to set the target as annotation value - SingleMemberAnnotation singleMemberAnnotation = ast.newSingleMemberAnnotation(); - StringLiteral value = ast.newStringLiteral(); - value.setLiteralValue(mapped); - singleMemberAnnotation.setValue(value); - newAnnotation = singleMemberAnnotation; - } - - newAnnotation.setTypeName(ast.newName(annotationNode.getTypeName().getFullyQualifiedName())); - //noinspection unchecked - node.modifiers().set(annotationIndex, newAnnotation); - - changed.set(true); - } - - return false; - } - }); - - return changed.get(); - } - - private boolean remapInjectsAndRedirects(CompilationUnit cu, Mapping mapping) { - AtomicBoolean changed = new AtomicBoolean(false); - - cu.accept(new ASTVisitor() { - @Override - public boolean visit(MethodDeclaration node) { - NormalAnnotation annotationNode = null; - for (Object obj : node.modifiers()) { - if (!(obj instanceof NormalAnnotation)) { - continue; - } - annotationNode = (NormalAnnotation) obj; - IAnnotationBinding annotation = annotationNode.resolveAnnotationBinding(); - if (annotation != null) { - String qualifiedName = annotation.getAnnotationType().getQualifiedName(); - if (qualifiedName.equals(CLASS_INJECT) || qualifiedName.equals(CLASS_REDIRECT)) { - break; - } - } - annotationNode = null; - } - if (annotationNode == null) return false; - - //noinspection unchecked - for (MemberValuePair pair : (List<MemberValuePair>) annotationNode.values()) { - if (!pair.getName().getIdentifier().equals("method")) continue; - - Object expr = pair.getValue(); - // Note: mixin supports multiple targets, we do not (yet) - if (!(expr instanceof StringLiteral)) continue; - StringLiteral methodNode = (StringLiteral) expr; - String method = methodNode.getLiteralValue(); - String mapped = mapping.methods.get(method); - if (mapped != null && !mapped.equals(method)) { - methodNode.setLiteralValue(mapped); - changed.set(true); - } - } - - return false; - } - }); - - return changed.get(); - } - - private Mapping remapInternalType(String internalType, StringBuilder result) { - if (internalType.charAt(0) == 'L') { - String type = internalType.substring(1, internalType.length() - 1).replace('/', '.'); - Mapping mapping = map.get(type); - if (mapping != null) { - result.append('L').append(mapping.newName.replace('.', '/')).append(';'); - return mapping; - } - } - result.append(internalType); - return null; - } - - private String remapFullyQualifiedMethodOrField(String signature) { - int ownerEnd = signature.indexOf(';'); - int argsBegin = signature.indexOf('('); - int argsEnd = signature.indexOf(')'); - boolean method = argsBegin != -1; - if (!method) { - argsBegin = argsEnd = signature.indexOf(':'); - } - String owner = signature.substring(0, ownerEnd + 1); - String name = signature.substring(ownerEnd + 1, argsBegin); - String returnType = signature.substring(argsEnd + 1); - - StringBuilder builder = new StringBuilder(signature.length() + 32); - Mapping mapping = remapInternalType(owner, builder); - String mapped = null; - if (mapping != null) { - mapped = (method ? mapping.methods : mapping.fields).get(name); - } - builder.append(mapped != null ? mapped : name); - if (method) { - builder.append('('); - String args = signature.substring(argsBegin + 1, argsEnd); - for (int i = 0; i < args.length(); i++) { - char c = args.charAt(i); - if (c != 'L') { - builder.append(c); + CoreLocalFileSystem vfs = (CoreLocalFileSystem) VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL); + Map<String, String> results = new HashMap<>(); + for (String name : sources.keySet()) { + VirtualFile file = vfs.findFileByIoFile(tmpDir.resolve(name).toFile()); + assert file != null; + PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + assert psiFile != null; + + String mapped = new PsiMapper(map, psiFile).remapFile(); + if (mapped == null) { + fail = true; continue; } - int end = args.indexOf(';', i); - String arg = args.substring(i, end + 1); - remapInternalType(arg, builder); - i = end; + results.put(name, mapped); } - builder.append(')'); - } else { - builder.append(':'); + return results; + } finally { + Files.walk(tmpDir).map(Path::toFile).sorted(Comparator.reverseOrder()).forEach(File::delete); } - remapInternalType(returnType, builder); - return builder.toString(); - } - - private boolean remapAtTargets(CompilationUnit cu) { - AtomicBoolean changed = new AtomicBoolean(false); - - cu.accept(new ASTVisitor() { - @Override - public boolean visit(NormalAnnotation node) { - IAnnotationBinding annotation = node.resolveAnnotationBinding(); - if (annotation == null) return true; - String qualifiedName = annotation.getAnnotationType().getQualifiedName(); - if (!qualifiedName.equals(CLASS_AT)) return true; - - //noinspection unchecked - for (MemberValuePair pair : (List<MemberValuePair>) node.values()) { - if (!pair.getName().getIdentifier().equals("target")) continue; - - StringLiteral value = (StringLiteral) pair.getValue(); - String signature = value.getLiteralValue(); - String newSignature = remapFullyQualifiedMethodOrField(signature); - if (!newSignature.equals(signature)) { - value.setLiteralValue(newSignature); - changed.set(true); - } - } - - return false; - } - }); - - return changed.get(); - } - - private static String stripGenerics(String name) { - int paramIndex = name.indexOf('<'); - return paramIndex != -1 ? name.substring(0, paramIndex) : name; - } - - private boolean remapClass(String unitName, CompilationUnit cu) { - AtomicBoolean changed = new AtomicBoolean(false); - Map<String, Mapping> mixinMappings = new HashMap<>(); - - cu.accept(new ASTVisitor() { - @Override - public boolean visit(TypeDeclaration node) { - ITypeBinding type = node.resolveBinding(); - if (type == null) return false; - - IAnnotationBinding binding = null; - for (Object modifier : node.modifiers()) { - if (modifier instanceof Annotation) { - binding = ((Annotation) modifier).resolveAnnotationBinding(); - if (binding != null && !binding.getAnnotationType().getQualifiedName().equals(CLASS_MIXIN)) { - binding = null; - } - } - } - if (binding == null) return false; - - if (remapAtTargets(cu)) { - changed.set(true); - } - - ITypeBinding target = getMixinTarget(binding); - if (target == null) return false; - - Mapping mapping = map.get(target.getQualifiedName()); - if (mapping == null) return false; - - mixinMappings.put(type.getQualifiedName(), mapping); - - if (!mapping.fields.isEmpty()) { - if (remapAccessors(cu, mapping)) { - changed.set(true); - } - } - if (!mapping.methods.isEmpty()) { - if (remapInjectsAndRedirects(cu, mapping)) { - changed.set(true); - } - } - - return false; - } - }); - - cu.accept(new ASTVisitor() { - @Override - public boolean visit(ImportDeclaration node) { - String name = node.getName().getFullyQualifiedName(); - Mapping mapping = map.get(name); - String mapped = mapping == null ? null : mapping.newName; - if (mapped != null && !mapped.equals(name)) { - node.setName(node.getAST().newName(mapped)); - changed.set(true); - } - return false; - } - }); - - cu.accept(new ASTVisitor() { - @Override - public boolean visit(ImportDeclaration node) { - return false; - } - - @Override - public boolean visit(QualifiedName node) { - String name = node.getFullyQualifiedName(); - Mapping mapping = map.get(name); - String mapped = mapping == null ? null : mapping.newName; - if (mapped != null && !mapped.equals(name)) { - node.setQualifier(node.getAST().newName(mapped.substring(0, mapped.lastIndexOf('.')))); - node.setName(node.getAST().newSimpleName(mapped.substring(mapped.lastIndexOf('.') + 1))); - changed.set(true); - return false; - } else { - return true; - } - } - - @Override - public boolean visit(SimpleName node) { - return visitName(node.resolveBinding(), node); - } - - private boolean visitName(IBinding binding, SimpleName node) { - String mapped; - if (binding instanceof IVariableBinding) { - ITypeBinding declaringClass = ((IVariableBinding) binding).getDeclaringClass(); - if (declaringClass == null) return true; - String name = stripGenerics(declaringClass.getQualifiedName()); - if (name.isEmpty()) return true; - Mapping mapping = mixinMappings.get(name); - if (mapping == null) { - mapping = map.get(name); - } - if (mapping == null) return true; - mapped = mapping.fields.get(node.getIdentifier()); - if (mapped != null) { - ASTNode parent = node.getParent(); - if (!(parent instanceof FieldAccess // qualified access is fine - || parent instanceof QualifiedName // qualified access is fine - || parent instanceof VariableDeclarationFragment // shadow member declarations are fine - || parent instanceof SwitchCase) // referencing constants in case statements is fine - ) { - System.err.println(unitName + ": Implicit member reference to remapped field \"" + node.getIdentifier() + "\". " + - "This can cause issues if the remapped reference becomes shadowed by a local variable and is therefore forbidden. " + - "Use \"this." + node.getIdentifier() + "\" instead."); - fail = true; - } - } - } else if (binding instanceof IMethodBinding) { - ITypeBinding declaringClass = ((IMethodBinding) binding).getDeclaringClass(); - if (declaringClass == null) return true; - ArrayDeque<ITypeBinding> parentQueue = new ArrayDeque<>(); - parentQueue.offer(declaringClass); - Mapping mapping = null; - - String name = stripGenerics(declaringClass.getQualifiedName()); - if (!name.isEmpty()) { - mapping = mixinMappings.get(name); - } - while (true) { - if (mapping != null) { - mapped = mapping.methods.get(node.getIdentifier()); - if (mapped != null) { - break; - } - mapping = null; - } - while (mapping == null) { - declaringClass = parentQueue.poll(); - if (declaringClass == null) return true; - - ITypeBinding superClass = declaringClass.getSuperclass(); - if (superClass != null) { - parentQueue.offer(superClass); - } - for (ITypeBinding anInterface : declaringClass.getInterfaces()) { - parentQueue.offer(anInterface); - } - - name = stripGenerics(declaringClass.getQualifiedName()); - if (name.isEmpty()) continue; - mapping = map.get(name); - } - } - } else if (binding instanceof ITypeBinding) { - String name = stripGenerics(((ITypeBinding) binding).getQualifiedName()); - if (name.isEmpty()) return true; - Mapping mapping = map.get(name); - if (mapping == null) return true; - mapped = mapping.newName; - mapped = mapped.substring(mapped.lastIndexOf('.') + 1); - } else { - return true; - } - - if (mapped != null && !mapped.equals(node.getIdentifier())) { - node.setIdentifier(mapped); - changed.set(true); - } - return true; - } - - @Override - public boolean visit(MethodDeclaration node) { - if (node.getBody() != null && node.getLength() == node.getBody().getLength()) { - // Body exists but is same length as overall definition? -> method was probably generated by lombok - return false; - } - return super.visit(node); - } - }); - return changed.get(); } public static class Mapping { |