From 18a9a9d7b048e11981d2e3bf47af17e024e0ebef Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Mon, 17 Jun 2019 11:17:16 +0200 Subject: 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. --- .../java/com/replaymod/gradle/remap/PsiMapper.java | 396 ++++++++++++++++ .../com/replaymod/gradle/remap/Transformer.java | 524 +++------------------ 2 files changed, 457 insertions(+), 463 deletions(-) create mode 100644 src/main/java/com/replaymod/gradle/remap/PsiMapper.java (limited to 'src/main/java/com') 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 map; + private final Map mixinMappings = new HashMap<>(); + private final PsiFile file; + private boolean error; + private TreeMap changes = new TreeMap<>(Comparator.comparing(TextRange::getStartOffset)); + + PsiMapper(Map 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 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 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 mappings; if (args[0].isEmpty()) { mappings = new HashMap<>(); @@ -93,21 +107,9 @@ class Transformer { this.classpath = classpath; } - public Map remap(Map sources) throws BadLocationException, IOException { - ASTParser parser = ASTParser.newParser(AST.JLS8); - Map 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 remap(Map sources) throws IOException { Path tmpDir = Files.createTempDirectory("remap"); try { - Map filePathToName = new HashMap<>(); - String[] compilationUnits = new String[sources.size()]; - String[] encodings = new String[compilationUnits.length]; - int i = 0; for (Entry 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 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 results = new HashMap<>(); - for (Entry 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) 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 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) 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 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 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 { -- cgit