From 0be10612c5641f583ac9c48ea83e053cdcf6d9e3 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Wed, 20 Dec 2023 20:39:08 +0100 Subject: [PATCH] More tests, fixed parameter indices. --- src/main/java/ApplyParchmentToSourceJar.java | 128 +---------- src/main/java/ClasspathSetup.java | 12 +- src/main/java/GatherReplacementsVisitor.java | 157 +------------ src/main/java/IntelliJEnvironment.java | 155 +++++++++++++ src/main/java/PsiHelper.java | 192 ++++++++++++++++ .../java/ApplyParchmentToSourceJarTest.java | 10 +- src/test/java/PsiHelperTest.java | 213 ++++++++++++++++++ .../resources/nested/expected/pkg/Outer.java | 3 + src/test/resources/nested/parchment.json | 15 ++ .../resources/nested/source/pkg/Outer.java | 3 + .../param_indices/expected/TestClass.java | 20 ++ .../param_indices/expected/TestEnum.java | 6 + .../resources/param_indices/parchment.json | 160 +++++++++++++ .../param_indices/source/TestClass.java | 20 ++ .../param_indices/source/TestEnum.java | 6 + 15 files changed, 819 insertions(+), 281 deletions(-) create mode 100644 src/main/java/IntelliJEnvironment.java create mode 100644 src/main/java/PsiHelper.java create mode 100644 src/test/java/PsiHelperTest.java create mode 100644 src/test/resources/param_indices/expected/TestClass.java create mode 100644 src/test/resources/param_indices/expected/TestEnum.java create mode 100644 src/test/resources/param_indices/parchment.json create mode 100644 src/test/resources/param_indices/source/TestClass.java create mode 100644 src/test/resources/param_indices/source/TestEnum.java diff --git a/src/main/java/ApplyParchmentToSourceJar.java b/src/main/java/ApplyParchmentToSourceJar.java index 7e05b73..23331c7 100644 --- a/src/main/java/ApplyParchmentToSourceJar.java +++ b/src/main/java/ApplyParchmentToSourceJar.java @@ -1,32 +1,4 @@ -import com.intellij.core.CoreApplicationEnvironment; -import com.intellij.core.JavaCoreApplicationEnvironment; -import com.intellij.core.JavaCoreProjectEnvironment; -import com.intellij.lang.jvm.facade.JvmElementProvider; -import com.intellij.mock.MockProject; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.PathManager; -import com.intellij.openapi.application.TransactionGuard; -import com.intellij.openapi.application.TransactionGuardImpl; -import com.intellij.openapi.roots.LanguageLevelProjectExtension; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileSystem; -import com.intellij.openapi.vfs.impl.ZipHandler; -import com.intellij.pom.java.InternalPersistentJavaLanguageLevelReaderService; -import com.intellij.pom.java.LanguageLevel; -import com.intellij.psi.JavaModuleSystem; -import com.intellij.psi.PsiElementFinder; -import com.intellij.psi.PsiManager; -import com.intellij.psi.PsiNameHelper; -import com.intellij.psi.augment.PsiAugmentProvider; -import com.intellij.psi.impl.PsiElementFinderImpl; -import com.intellij.psi.impl.PsiNameHelperImpl; -import com.intellij.psi.impl.PsiTreeChangePreprocessor; -import com.intellij.psi.impl.source.tree.JavaTreeGenerator; -import com.intellij.psi.impl.source.tree.TreeGenerator; -import com.intellij.psi.util.JavaClassSupers; -import modules.CoreJrtFileSystem; import namesanddocs.NameAndDocSourceLoader; import namesanddocs.NamesAndDocsDatabase; import org.jetbrains.annotations.NotNull; @@ -49,50 +21,16 @@ */ public class ApplyParchmentToSourceJar implements AutoCloseable { private final NamesAndDocsDatabase namesAndDocs; - - private final Path tempDir; - private final MockProject project; - private final JavaCoreProjectEnvironment javaEnv; - private final PsiManager psiManager; + private final IntelliJEnvironment ijEnv = new IntelliJEnvironment(); private int maxQueueDepth = 50; private boolean enableJavadoc = true; - private final Disposable rootDisposable; - public ApplyParchmentToSourceJar(Path javaHome, NamesAndDocsDatabase namesAndDocs) throws IOException { + public ApplyParchmentToSourceJar(NamesAndDocsDatabase namesAndDocs) throws IOException { this.namesAndDocs = namesAndDocs; - tempDir = Files.createTempDirectory("applyparchment"); - this.rootDisposable = Disposer.newDisposable(); - System.setProperty("idea.home.path", tempDir.toAbsolutePath().toString()); - - // IDEA requires a config directory, even if it's empty - PathManager.setExplicitConfigPath(tempDir.toAbsolutePath().toString()); - Registry.markAsLoaded(); // Avoids warnings about config not being loaded - - var appEnv = new JavaCoreApplicationEnvironment(rootDisposable) { - @Override - protected VirtualFileSystem createJrtFileSystem() { - return new CoreJrtFileSystem(); - } - }; - initAppExtensionsAndServices(appEnv); - - javaEnv = new JavaCoreProjectEnvironment(rootDisposable, appEnv); - - ClasspathSetup.addJdkModules(javaHome, javaEnv); - - project = javaEnv.getProject(); - - initProjectExtensionsAndServices(project); - - LanguageLevelProjectExtension.getInstance(project).setLanguageLevel(LanguageLevel.JDK_17); - - psiManager = PsiManager.getInstance(project); + ijEnv.addCurrentJdkToClassPath(); } - public static void main(String[] args) throws Exception { - System.setProperty("java.awt.headless", "true"); - Path inputPath = null, outputPath = null, namesAndDocsPath = null, librariesPath = null; boolean enableJavadoc = true; int queueDepth = 50; @@ -158,13 +96,10 @@ public static void main(String[] args) throws Exception { var namesAndDocs = NameAndDocSourceLoader.load(namesAndDocsPath); - // Add the Java Runtime we are currently running in - var javaHome = Paths.get(System.getProperty("java.home")); - - try (var applyParchment = new ApplyParchmentToSourceJar(javaHome, namesAndDocs)) { + try (var applyParchment = new ApplyParchmentToSourceJar(namesAndDocs)) { // Add external libraries to classpath if (librariesPath != null) { - ClasspathSetup.addLibraries(librariesPath, applyParchment.javaEnv); + ClasspathSetup.addLibraries(librariesPath, applyParchment.ijEnv); } applyParchment.setMaxQueueDepth(queueDepth); @@ -186,6 +121,8 @@ private static void printUsage(PrintStream out) { public void apply(Path inputPath, Path outputPath) throws IOException, InterruptedException { + var javaEnv = ijEnv.getProjectEnv(); + var sourceJarRoot = javaEnv.getEnvironment().getJarFileSystem().findFileByPath(inputPath + "!/"); if (sourceJarRoot == null) { throw new FileNotFoundException("Cannot find JAR-File " + inputPath); @@ -213,7 +150,7 @@ public void apply(Path inputPath, Path outputPath) throws IOException, Interrupt } void addJarToClassPath(Path jarFile) { - javaEnv.addJarToClassPath(jarFile.toFile()); + ijEnv.addJarToClassPath(jarFile); } byte[] transformSource(VirtualFile contentRoot, String path, byte[] originalContentBytes) { @@ -226,7 +163,7 @@ byte[] transformSource(VirtualFile contentRoot, String path, byte[] originalCont System.err.println("Can't transform " + path + " since IntelliJ doesn't see it in the source jar."); return originalContentBytes; } - var psiFile = psiManager.findFile(sourceFile); + var psiFile = ijEnv.getPsiManager().findFile(sourceFile); if (psiFile == null) { System.err.println("Can't transform " + path + " since IntelliJ can't load it."); return originalContentBytes; @@ -281,48 +218,6 @@ private static String applyReplacements(CharSequence originalContent, List l.startsWith("-e=")) .map(l -> l.substring(3)) - .map(File::new) + .map(Paths::get) .toList(); for (var libraryFile : libraryFiles) { - if (!libraryFile.exists()) { - throw new UncheckedIOException(new FileNotFoundException(libraryFile.getAbsolutePath())); + if (!Files.exists(libraryFile)) { + throw new UncheckedIOException(new FileNotFoundException(libraryFile.toString())); } - javaEnv.addJarToClassPath(libraryFile); + ijEnv.addJarToClassPath(libraryFile); System.out.println("Added " + libraryFile); } } diff --git a/src/main/java/GatherReplacementsVisitor.java b/src/main/java/GatherReplacementsVisitor.java index 87bb3b2..4c77bb4 100644 --- a/src/main/java/GatherReplacementsVisitor.java +++ b/src/main/java/GatherReplacementsVisitor.java @@ -1,47 +1,26 @@ -import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiJavaDocumentedElement; -import com.intellij.psi.PsiLambdaExpression; import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; -import com.intellij.psi.PsiType; -import com.intellij.psi.PsiTypes; import com.intellij.psi.PsiWhiteSpace; -import com.intellij.psi.SyntaxTraverser; import com.intellij.psi.search.GlobalSearchScope; -import com.intellij.psi.util.CachedValueProvider; -import com.intellij.psi.util.CachedValuesManager; -import com.intellij.psi.util.ClassUtil; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.util.containers.ObjectIntHashMap; import namesanddocs.NamesAndDocsDatabase; -import namesanddocs.NamesAndDocsForClass; -import namesanddocs.NamesAndDocsForMethod; import namesanddocs.NamesAndDocsForParameter; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; class GatherReplacementsVisitor extends PsiRecursiveElementVisitor { - /** - * Key for attaching mapping data to a PsiClass. Used to prevent multiple lookups for the same class. - */ - private static final Key> CLASS_DATA_KEY = Key.create("names_and_docs_for_class"); - private static final Key> METHOD_DATA_KEY = Key.create("names_and_docs_for_method"); - private final NamesAndDocsDatabase namesAndDocs; private final boolean enableJavadoc; private final List replacements; @@ -76,19 +55,19 @@ public void visitElement(@NotNull PsiElement element) { } // Add javadoc if available - var classData = getClassData(psiClass); + var classData = PsiHelper.getClassData(namesAndDocs, psiClass); if (classData != null) { applyJavadoc(psiClass, classData.getJavadoc(), replacements); } } else if (element instanceof PsiField psiField) { - var classData = getClassData(psiField.getContainingClass()); + var classData = PsiHelper.getClassData(namesAndDocs, psiField.getContainingClass()); var fieldData = classData != null ? classData.getField(psiField.getName()) : null; if (fieldData != null) { // Add javadoc if available applyJavadoc(psiField, fieldData.getJavadoc(), replacements); } } else if (element instanceof PsiMethod psiMethod) { - var methodData = getMethodData(psiMethod); + var methodData = PsiHelper.getMethodData(namesAndDocs, psiMethod); if (methodData != null) { // Add javadoc if available applyJavadoc(psiMethod, methodData.getJavadoc(), replacements); @@ -104,7 +83,7 @@ public void visitElement(@NotNull PsiElement element) { // Parchment stores parameter indices based on the index of the parameter in the actual compiled method // to account for synthetic parameter not found in the source-code, we must adjust the index accordingly. - var jvmIndex = getJvmIndex(psiParameter, i); + var jvmIndex = PsiHelper.getBinaryIndex(psiParameter, i); var paramData = methodData.getParameter(jvmIndex); if (paramData != null && paramData.getName() != null) { @@ -145,28 +124,6 @@ public void visitElement(@NotNull PsiElement element) { element.acceptChildren(this); } - private int getJvmIndex(PsiParameter psiParameter, int index) { - var declarationScope = psiParameter.getDeclarationScope(); - if (declarationScope instanceof PsiMethod psiMethod) { - - // Try to account for hidden parameters only present in bytecode since the - // mapping data refers to parameters using those indices - if (psiMethod.isConstructor() && psiMethod.getContainingClass() != null && psiMethod.getContainingClass().isEnum()) { - index += 2; - } else if (psiMethod.getContainingClass() != null && psiMethod.getContainingClass() != null - && !psiMethod.getContainingClass().hasModifierProperty(PsiModifier.STATIC)) { - index += 1; - } - - return index; - } else if (declarationScope instanceof PsiLambdaExpression psiLambda) { - // Naming lambdas doesn't really work - return index; - } else { - return -1; - } - } - private void applyJavadoc(PsiJavaDocumentedElement method, List javadoc, List replacements) { if (!enableJavadoc) { return; @@ -219,110 +176,4 @@ private void applyJavadoc(PsiJavaDocumentedElement method, List javadoc, } } - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - private NamesAndDocsForClass getClassData(@Nullable PsiClass psiClass) { - if (psiClass == null) { - return null; - } - var classData = psiClass.getUserData(CLASS_DATA_KEY); - if (classData != null) { - return classData.orElse(null); - } else { - var sb = new StringBuilder(); - getBinaryClassName(psiClass, sb); - if (sb.isEmpty()) { - classData = Optional.empty(); - } else { - classData = Optional.ofNullable(namesAndDocs.getClass(sb.toString())); - } - psiClass.putUserData(CLASS_DATA_KEY, classData); - return classData.orElse(null); - } - } - - @SuppressWarnings("OptionalAssignedToNull") - @Nullable - private NamesAndDocsForMethod getMethodData(@Nullable PsiMethod psiMethod) { - if (psiMethod == null) { - return null; - } - var methodData = psiMethod.getUserData(METHOD_DATA_KEY); - if (methodData != null) { - return methodData.orElse(null); - } else { - methodData = Optional.empty(); - var classData = getClassData(psiMethod.getContainingClass()); - if (classData != null) { - var methodName = psiMethod.getName(); - var methodSignature = getBinaryMethodSignature(psiMethod); - methodData = Optional.ofNullable(classData.getMethod(methodName, methodSignature)); - } - - psiMethod.putUserData(METHOD_DATA_KEY, methodData); - return methodData.orElse(null); - } - } - - public static String getBinaryMethodSignature(PsiMethod method) { - StringBuilder signature = new StringBuilder(); - signature.append("("); - for (PsiParameter param : method.getParameterList().getParameters()) { - var binaryPresentation = ClassUtil.getBinaryPresentation(param.getType()); - if (binaryPresentation.isEmpty()) { - System.err.println("Failed to create binary representation for type " + param.getType().getCanonicalText()); - binaryPresentation = "ERROR"; - } - signature.append(binaryPresentation); - } - signature.append(")"); - var returnType = Optional.ofNullable(method.getReturnType()).orElse(PsiTypes.voidType()); - var returnTypeRepresentation = ClassUtil.getBinaryPresentation(returnType); - if (returnTypeRepresentation.isEmpty()) { - System.err.println("Failed to create binary representation for type " + returnType.getCanonicalText()); - returnTypeRepresentation = "ERROR"; - } - signature.append(returnTypeRepresentation); - return signature.toString(); - } - - - /** - * An adapted version of {@link ClassUtil#formatClassName(PsiClass, StringBuilder)} where Inner-Classes - * use a $ separator while formatClassName separates InnerClasses with periods from their parent. - */ - private static void getBinaryClassName(@NotNull final PsiClass aClass, @NotNull StringBuilder buf) { - final String qName = ClassUtil.getJVMClassName(aClass); - if (qName != null) { - buf.append(qName.replace('.', '/')); - } else { - final PsiClass parentClass = PsiTreeUtil.getContextOfType(aClass, PsiClass.class, true); - if (parentClass != null) { - getBinaryClassName(parentClass, buf); - buf.append("$"); - buf.append(getNonQualifiedClassIdx(aClass, parentClass)); - final String name = aClass.getName(); - if (name != null) { - buf.append(name); - } - } - } - } - - private static int getNonQualifiedClassIdx(@NotNull final PsiClass psiClass, @NotNull final PsiClass containingClass) { - var indices = - CachedValuesManager.getCachedValue(containingClass, () -> { - var map = new ObjectIntHashMap(); - int index = 0; - for (PsiClass aClass : SyntaxTraverser.psiTraverser().withRoot(containingClass).postOrderDfsTraversal().filter(PsiClass.class)) { - if (aClass.getQualifiedName() == null) { - map.put(aClass, ++index); - } - } - return CachedValueProvider.Result.create(map, containingClass); - }); - - return indices.get(psiClass); - } - } diff --git a/src/main/java/IntelliJEnvironment.java b/src/main/java/IntelliJEnvironment.java new file mode 100644 index 0000000..7e3dc1c --- /dev/null +++ b/src/main/java/IntelliJEnvironment.java @@ -0,0 +1,155 @@ +import com.intellij.core.CoreApplicationEnvironment; +import com.intellij.core.JavaCoreApplicationEnvironment; +import com.intellij.core.JavaCoreProjectEnvironment; +import com.intellij.lang.java.JavaLanguage; +import com.intellij.lang.jvm.facade.JvmElementProvider; +import com.intellij.mock.MockProject; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.TransactionGuard; +import com.intellij.openapi.application.TransactionGuardImpl; +import com.intellij.openapi.roots.LanguageLevelProjectExtension; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.VirtualFileSystem; +import com.intellij.openapi.vfs.impl.ZipHandler; +import com.intellij.pom.java.InternalPersistentJavaLanguageLevelReaderService; +import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.JavaModuleSystem; +import com.intellij.psi.PsiElementFinder; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileFactory; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiNameHelper; +import com.intellij.psi.augment.PsiAugmentProvider; +import com.intellij.psi.impl.JavaClassSupersImpl; +import com.intellij.psi.impl.PsiElementFinderImpl; +import com.intellij.psi.impl.PsiNameHelperImpl; +import com.intellij.psi.impl.PsiTreeChangePreprocessor; +import com.intellij.psi.impl.source.tree.JavaTreeGenerator; +import com.intellij.psi.impl.source.tree.TreeGenerator; +import com.intellij.psi.util.JavaClassSupers; +import modules.CoreJrtFileSystem; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class IntelliJEnvironment implements AutoCloseable { + + private final Disposable rootDisposable; + private final Path tempDir; + private final MockProject project; + private final JavaCoreProjectEnvironment javaEnv; + private final PsiManager psiManager; + + public IntelliJEnvironment() throws IOException { + System.setProperty("java.awt.headless", "true"); + + tempDir = Files.createTempDirectory("applyparchment"); + this.rootDisposable = Disposer.newDisposable(); + System.setProperty("idea.home.path", tempDir.toAbsolutePath().toString()); + + // IDEA requires a config directory, even if it's empty + PathManager.setExplicitConfigPath(tempDir.toAbsolutePath().toString()); + Registry.markAsLoaded(); // Avoids warnings about config not being loaded + + var appEnv = new JavaCoreApplicationEnvironment(rootDisposable) { + @Override + protected VirtualFileSystem createJrtFileSystem() { + return new CoreJrtFileSystem(); + } + }; + initAppExtensionsAndServices(appEnv); + + javaEnv = new JavaCoreProjectEnvironment(rootDisposable, appEnv); + + project = javaEnv.getProject(); + + initProjectExtensionsAndServices(project); + + LanguageLevelProjectExtension.getInstance(project).setLanguageLevel(LanguageLevel.JDK_17); + + psiManager = PsiManager.getInstance(project); + } + + public PsiManager getPsiManager() { + return psiManager; + } + + public CoreApplicationEnvironment getAppEnv() { + return javaEnv.getEnvironment(); + } + + public JavaCoreProjectEnvironment getProjectEnv() { + return javaEnv; + } + + void addJarToClassPath(Path jarFile) { + javaEnv.addJarToClassPath(jarFile.toFile()); + } + + public void addCurrentJdkToClassPath() { + // Add the Java Runtime we are currently running in + var javaHome = Paths.get(System.getProperty("java.home")); + ClasspathSetup.addJdkModules(javaHome, javaEnv); + } + + @Override + public void close() throws IOException { + // Releases cached ZipFiles within IntelliJ, allowing the tempdir to be deleted + ZipHandler.clearFileAccessorCache(); + Disposer.dispose(rootDisposable); + Files.deleteIfExists(tempDir); + } + + PsiFile parseFileFromMemory(String filename, String fileContent) { + var fileFactory = PsiFileFactory.getInstance(project); + return fileFactory.createFileFromText(filename, JavaLanguage.INSTANCE, fileContent); + } + + /* + * When IntelliJ crashes after an update complaining about an extension point or extension not being available, + * check JavaPsiPlugin.xml for the name of that extension point. Then register it as it's defined in the XML + * by hand here. + * + * This method is responsible for anything in the XML that is: + * - A projectService + * - Extension points marked as area="IDEA_PROJECT" + * - Any extensions registered for extension points that are area="IDEA_PROJECT" + */ + private void initProjectExtensionsAndServices(MockProject project) { + project.registerService(PsiNameHelper.class, PsiNameHelperImpl.class); + + var projectExtensions = project.getExtensionArea(); + CoreApplicationEnvironment.registerExtensionPoint(projectExtensions, PsiTreeChangePreprocessor.EP.getName(), PsiTreeChangePreprocessor.class); + CoreApplicationEnvironment.registerExtensionPoint(projectExtensions, PsiElementFinder.EP.getName(), PsiElementFinder.class); + CoreApplicationEnvironment.registerExtensionPoint(projectExtensions, JvmElementProvider.EP_NAME, JvmElementProvider.class); + PsiElementFinder.EP.getPoint(project).registerExtension(new PsiElementFinderImpl(project), rootDisposable); + } + + /* + * When IntelliJ crashes after an update complaining about an extension point or extension not being available, + * check JavaPsiPlugin.xml for the name of that extension point. Then register it as it's defined in the XML + * by hand here. + * + * This method is responsible for anything in the XML that is: + * - An applicationService + * - Extension points not marked as area="IDEA_PROJECT" + * - Any extensions registered for extension points that are not marked area="IDEA_PROJECT" + */ + private void initAppExtensionsAndServices(JavaCoreApplicationEnvironment appEnv) { + // When any service or extension point is missing, check JavaPsiPlugin.xml in classpath and grab the definition + appEnv.registerApplicationService(JavaClassSupers.class, new JavaClassSupersImpl()); + appEnv.registerApplicationService(InternalPersistentJavaLanguageLevelReaderService.class, new InternalPersistentJavaLanguageLevelReaderService.DefaultImpl()); + appEnv.registerApplicationService(TransactionGuard.class, new TransactionGuardImpl()); + + var appExtensions = appEnv.getApplication().getExtensionArea(); + CoreApplicationEnvironment.registerExtensionPoint(appExtensions, PsiAugmentProvider.EP_NAME, PsiAugmentProvider.class); + CoreApplicationEnvironment.registerExtensionPoint(appExtensions, JavaModuleSystem.EP_NAME, JavaModuleSystem.class); + CoreApplicationEnvironment.registerExtensionPoint(appExtensions, TreeGenerator.EP_NAME, TreeGenerator.class); + appExtensions.getExtensionPoint(TreeGenerator.EP_NAME).registerExtension(new JavaTreeGenerator(), rootDisposable); + } + +} diff --git a/src/main/java/PsiHelper.java b/src/main/java/PsiHelper.java new file mode 100644 index 0000000..34ce59f --- /dev/null +++ b/src/main/java/PsiHelper.java @@ -0,0 +1,192 @@ +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiLambdaExpression; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiParameter; +import com.intellij.psi.PsiTypes; +import com.intellij.psi.SyntaxTraverser; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.ClassUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ObjectIntHashMap; +import namesanddocs.NamesAndDocsDatabase; +import namesanddocs.NamesAndDocsForClass; +import namesanddocs.NamesAndDocsForMethod; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.Optional; + +public final class PsiHelper { + // Keys for attaching mapping data to a PsiClass. Used to prevent multiple lookups for the same class/method. + private static final Key> CLASS_DATA_KEY = Key.create("names_and_docs_for_class"); + private static final Key> METHOD_DATA_KEY = Key.create("names_and_docs_for_method"); + + @SuppressWarnings("OptionalAssignedToNull") + @Nullable + public static NamesAndDocsForClass getClassData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiClass psiClass) { + if (psiClass == null) { + return null; + } + var classData = psiClass.getUserData(CLASS_DATA_KEY); + if (classData != null) { + return classData.orElse(null); + } else { + var sb = new StringBuilder(); + getBinaryClassName(psiClass, sb); + if (sb.isEmpty()) { + classData = Optional.empty(); + } else { + classData = Optional.ofNullable(namesAndDocs.getClass(sb.toString())); + } + psiClass.putUserData(CLASS_DATA_KEY, classData); + return classData.orElse(null); + } + } + + @SuppressWarnings("OptionalAssignedToNull") + @Nullable + public static NamesAndDocsForMethod getMethodData(NamesAndDocsDatabase namesAndDocs, @Nullable PsiMethod psiMethod) { + if (psiMethod == null) { + return null; + } + var methodData = psiMethod.getUserData(METHOD_DATA_KEY); + if (methodData != null) { + return methodData.orElse(null); + } else { + methodData = Optional.empty(); + var classData = getClassData(namesAndDocs, psiMethod.getContainingClass()); + if (classData != null) { + var methodName = getBinaryMethodName(psiMethod); + var methodSignature = getBinaryMethodSignature(psiMethod); + methodData = Optional.ofNullable(classData.getMethod(methodName, methodSignature)); + } + + psiMethod.putUserData(METHOD_DATA_KEY, methodData); + return methodData.orElse(null); + } + } + + public static String getBinaryMethodName(PsiMethod psiMethod) { + return psiMethod.isConstructor() ? "" : psiMethod.getName(); + } + + public static String getBinaryMethodSignature(PsiMethod method) { + StringBuilder signature = new StringBuilder(); + signature.append("("); + // Add implicit constructor parameters + // Private enumeration constructors have two hidden parameters (enun name+ordinal) + if (isEnumConstructor(method)) { + signature.append("Ljava/lang/String;I"); + } + // Non-Static inner class constructors have the enclosing class as their first argument + else if (isNonStaticInnerClassConstructor(method)) { + var parent = Objects.requireNonNull(Objects.requireNonNull(method.getContainingClass()).getContainingClass()); + signature.append("L"); + getBinaryClassName(parent, signature); + signature.append(";"); + } + + for (PsiParameter param : method.getParameterList().getParameters()) { + var binaryPresentation = ClassUtil.getBinaryPresentation(param.getType()); + if (binaryPresentation.isEmpty()) { + System.err.println("Failed to create binary representation for type " + param.getType().getCanonicalText()); + binaryPresentation = "ERROR"; + } + signature.append(binaryPresentation); + } + signature.append(")"); + var returnType = Optional.ofNullable(method.getReturnType()).orElse(PsiTypes.voidType()); + var returnTypeRepresentation = ClassUtil.getBinaryPresentation(returnType); + if (returnTypeRepresentation.isEmpty()) { + System.err.println("Failed to create binary representation for type " + returnType.getCanonicalText()); + returnTypeRepresentation = "ERROR"; + } + signature.append(returnTypeRepresentation); + return signature.toString(); + } + + + /** + * An adapted version of {@link ClassUtil#formatClassName(PsiClass, StringBuilder)} where Inner-Classes + * use a $ separator while formatClassName separates InnerClasses with periods from their parent. + */ + public static void getBinaryClassName(@NotNull final PsiClass aClass, @NotNull StringBuilder buf) { + final String qName = ClassUtil.getJVMClassName(aClass); + if (qName != null) { + buf.append(qName.replace('.', '/')); + } else { + final PsiClass parentClass = PsiTreeUtil.getContextOfType(aClass, PsiClass.class, true); + if (parentClass != null) { + getBinaryClassName(parentClass, buf); + buf.append("$"); + buf.append(getNonQualifiedClassIdx(aClass, parentClass)); + final String name = aClass.getName(); + if (name != null) { + buf.append(name); + } + } + } + } + + private static int getNonQualifiedClassIdx(@NotNull final PsiClass psiClass, @NotNull final PsiClass containingClass) { + var indices = + CachedValuesManager.getCachedValue(containingClass, () -> { + var map = new ObjectIntHashMap(); + int index = 0; + for (PsiClass aClass : SyntaxTraverser.psiTraverser().withRoot(containingClass).postOrderDfsTraversal().filter(PsiClass.class)) { + if (aClass.getQualifiedName() == null) { + map.put(aClass, ++index); + } + } + return CachedValueProvider.Result.create(map, containingClass); + }); + + return indices.get(psiClass); + } + + private static boolean isEnumConstructor(PsiMethod method) { + if (method.isConstructor()) { + var containingClass = method.getContainingClass(); + return containingClass != null && containingClass.isEnum(); + } + return false; + } + + private static boolean isNonStaticInnerClassConstructor(PsiMethod method) { + if (method.isConstructor()) { + var containingClass = method.getContainingClass(); + return containingClass != null + && containingClass.getContainingClass() != null + && !containingClass.hasModifierProperty(PsiModifier.STATIC); + } + return false; + } + + public static int getBinaryIndex(PsiParameter psiParameter, int index) { + var declarationScope = psiParameter.getDeclarationScope(); + if (declarationScope instanceof PsiMethod psiMethod) { + if (!psiMethod.hasModifierProperty(PsiModifier.STATIC)) { + index++; // this pointer + } + + // Try to account for hidden parameters only present in bytecode since the + // mapping data refers to parameters using those indices + if (isEnumConstructor(psiMethod)) { + index += 2; + } else if (isNonStaticInnerClassConstructor(psiMethod)) { + index += 1; + } + + return index; + } else if (declarationScope instanceof PsiLambdaExpression psiLambda) { + // Naming lambdas doesn't really work + return index; + } else { + return -1; + } + } +} diff --git a/src/test/java/ApplyParchmentToSourceJarTest.java b/src/test/java/ApplyParchmentToSourceJarTest.java index 67b915f..3f92ad4 100644 --- a/src/test/java/ApplyParchmentToSourceJarTest.java +++ b/src/test/java/ApplyParchmentToSourceJarTest.java @@ -29,6 +29,11 @@ void testExternalReferences() throws Exception { runTest("/external_refs"); } + @Test + void testParamIndices() throws Exception { + runTest("/param_indices"); + } + protected final void runTest(String testDir) throws Exception { var parchmentFile = Paths.get(getClass().getResource(testDir + "/parchment.json").toURI()); var sourceDir = parchmentFile.resolveSibling("source"); @@ -41,11 +46,8 @@ protected final void runTest(String testDir) throws Exception { }, null); } - // Add the Java Runtime we are currently running in - var javaHome = Paths.get(System.getProperty("java.home")); - var ouptutFile = tempDir.resolve("output.jar"); - try (var remapper = new ApplyParchmentToSourceJar(javaHome, NameAndDocSourceLoader.load(parchmentFile))) { + try (var remapper = new ApplyParchmentToSourceJar(NameAndDocSourceLoader.load(parchmentFile))) { // For testing external references, add JUnit-API so it can be referenced var junitJarPath = Paths.get(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI()); remapper.addJarToClassPath(junitJarPath); diff --git a/src/test/java/PsiHelperTest.java b/src/test/java/PsiHelperTest.java new file mode 100644 index 0000000..768e8e7 --- /dev/null +++ b/src/test/java/PsiHelperTest.java @@ -0,0 +1,213 @@ +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PsiHelperTest { + static IntelliJEnvironment ijEnv; + + @BeforeAll + static void setUp() throws IOException { + ijEnv = new IntelliJEnvironment(); + } + + @AfterAll + static void tearDown() throws IOException { + ijEnv.close(); + } + + @Nested + class AnonymousClass { + @Test + void testGetBinaryName() { + var method = parseSingleMethod(""" + class Outer { + private static final Object f = new Object() { + void m (int i) {} + }; + } + """); + + var cn = new StringBuilder(); + PsiHelper.getBinaryClassName(method.getContainingClass(), cn); + assertEquals("Outer$1", cn.toString()); + } + } + + @Nested + class InstanceMethod { + private PsiMethod ctor = parseSingleMethod(""" + class TestClass { + void m(int p) {} + } + """); + + @Test + void testName() { + assertEquals("m", PsiHelper.getBinaryMethodName(ctor)); + } + + @Test + void testSignature() { + assertEquals("(I)V", PsiHelper.getBinaryMethodSignature(ctor)); + } + + @Test + void testMethodParameterIndex() { + var firstParam = ctor.getParameterList().getParameter(0); + int index = PsiHelper.getBinaryIndex(firstParam, 0); + // Binary parameters are: + // 0) this + // 1) first method parameter + assertEquals(1, index); + } + } + + @Nested + class StaticMethod { + private PsiMethod ctor = parseSingleMethod(""" + class TestClass { + static void m(int p) {} + } + """); + + @Test + void testName() { + assertEquals("m", PsiHelper.getBinaryMethodName(ctor)); + } + + @Test + void testSignature() { + assertEquals("(I)V", PsiHelper.getBinaryMethodSignature(ctor)); + } + + @Test + void testMethodParameterIndex() { + var firstParam = ctor.getParameterList().getParameter(0); + int index = PsiHelper.getBinaryIndex(firstParam, 0); + // Binary parameters are: + // 0) first method parameter + assertEquals(0, index); + } + } + + @Nested + class EnumConstructor { + private PsiMethod ctor = parseSingleMethod(""" + enum TestEnum { + LITERAL; + TestEnum(int p) {} + } + """); + + @Test + void testName() { + assertEquals("", PsiHelper.getBinaryMethodName(ctor)); + } + + @Test + void testSignature() { + assertEquals("(Ljava/lang/String;II)V", PsiHelper.getBinaryMethodSignature(ctor)); + } + + @Test + void testMethodParameterIndex() { + var firstParam = ctor.getParameterList().getParameter(0); + int index = PsiHelper.getBinaryIndex(firstParam, 0); + // Binary parameters are: + // 0) this + // 1) enum literal name + // 2) enum literal ordinal + // 3) first method parameter + assertEquals(3, index); + } + } + + @Nested + class InnerClassConstructor { + private PsiMethod ctor = parseSingleMethod(""" + class Outer { + class Inner { + Inner(int p) {} + } + } + """); + + @Test + void testName() { + assertEquals("", PsiHelper.getBinaryMethodName(ctor)); + } + + @Test + void testSignature() { + assertEquals("(LOuter;I)V", PsiHelper.getBinaryMethodSignature(ctor)); + } + + @Test + void testMethodParameterIndex() { + var firstParam = ctor.getParameterList().getParameter(0); + int index = PsiHelper.getBinaryIndex(firstParam, 0); + // Binary parameters are: + // 0) this + // 1) outer class pointer + // 2) first method parameter + assertEquals(2, index); + } + } + + @Nested + class StaticInnerClassConstructor { + private PsiMethod ctor = parseSingleMethod(""" + class Outer { + static class Inner { + Inner(int p) {} + } + } + """); + + @Test + void testName() { + assertEquals("", PsiHelper.getBinaryMethodName(ctor)); + } + + @Test + void testSignature() { + assertEquals("(I)V", PsiHelper.getBinaryMethodSignature(ctor)); + } + + @Test + void testMethodParameterIndex() { + var firstParam = ctor.getParameterList().getParameter(0); + int index = PsiHelper.getBinaryIndex(firstParam, 0); + // Binary parameters are: + // 0) this + // 1) first method parameter + assertEquals(1, index); + } + } + + private PsiMethod parseSingleMethod(@Language("JAVA") String javaCode) { + return parseSingleElement(javaCode, PsiMethod.class); + } + + private PsiClass parseSingleClass(@Language("JAVA") String javaCode) { + return parseSingleElement(javaCode, PsiClass.class); + } + + private T parseSingleElement(@Language("JAVA") String javaCode, Class type) { + var file = ijEnv.parseFileFromMemory("Test.java", javaCode); + + var elements = PsiTreeUtil.collectElementsOfType(file, type); + assertEquals(1, elements.size()); + return elements.iterator().next(); + } +} diff --git a/src/test/resources/nested/expected/pkg/Outer.java b/src/test/resources/nested/expected/pkg/Outer.java index 7ebefd5..4fb710f 100644 --- a/src/test/resources/nested/expected/pkg/Outer.java +++ b/src/test/resources/nested/expected/pkg/Outer.java @@ -2,6 +2,9 @@ class Outer { void m(InnerClass innerClass) { System.out.println(innerClass); + new Object() { + void m(int mapped) {} + } } class InnerClass { void m(SamePkgClass samePkgClass) { diff --git a/src/test/resources/nested/parchment.json b/src/test/resources/nested/parchment.json index f11dd15..a13042d 100644 --- a/src/test/resources/nested/parchment.json +++ b/src/test/resources/nested/parchment.json @@ -16,6 +16,21 @@ } ] }, + { + "name": "pkg/Outer$1", + "methods": [ + { + "name": "m", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + } + ] + }, { "name": "pkg/Outer$InnerClass", "methods": [ diff --git a/src/test/resources/nested/source/pkg/Outer.java b/src/test/resources/nested/source/pkg/Outer.java index b95c964..f74ced1 100644 --- a/src/test/resources/nested/source/pkg/Outer.java +++ b/src/test/resources/nested/source/pkg/Outer.java @@ -2,6 +2,9 @@ class Outer { void m(InnerClass p_1) { System.out.println(p_1); + new Object() { + void m(int p_2) {} + } } class InnerClass { void m(SamePkgClass p_1) { diff --git a/src/test/resources/param_indices/expected/TestClass.java b/src/test/resources/param_indices/expected/TestClass.java new file mode 100644 index 0000000..8765bba --- /dev/null +++ b/src/test/resources/param_indices/expected/TestClass.java @@ -0,0 +1,20 @@ +public class TestClass { + public TestClass(int mapped) {} + public void instanceMethod(int mapped) {} + public static void staticMethod(int mapped) {} + public class InnerClass { + public InnerClass(int mapped) {} + public void instanceMethod(int mapped) {} + public static void staticMethod(int mapped) {} + public static class InnerStaticInnerClass { + public InnerStaticInnerClass(int mapped) {} + public void instanceMethod(int mapped) {} + public static void staticMethod(int mapped) {} + } + } + public static class StaticInnerClass { + public StaticInnerClass(int mapped) {} + public void instanceMethod(int mapped) {} + public static void staticMethod(int mapped) {} + } +} diff --git a/src/test/resources/param_indices/expected/TestEnum.java b/src/test/resources/param_indices/expected/TestEnum.java new file mode 100644 index 0000000..487b487 --- /dev/null +++ b/src/test/resources/param_indices/expected/TestEnum.java @@ -0,0 +1,6 @@ +public enum TestEnum { + // Enum constructors take two hidden arguments (the constant name and ordinal) + // Which are part of the method signature. This is in addition to the this pointer. + private TestEnum(int mapped) { + } +} diff --git a/src/test/resources/param_indices/parchment.json b/src/test/resources/param_indices/parchment.json new file mode 100644 index 0000000..5c34f8d --- /dev/null +++ b/src/test/resources/param_indices/parchment.json @@ -0,0 +1,160 @@ +{ + "version": "1.1.0", + "classes": [ + { + "name": "TestEnum", + "methods": [ + { + "name": "", + "descriptor": "(Ljava/lang/String;II)V", + "parameters": [ + { + "index": 3, + "name": "mapped" + } + ] + } + ] + }, + { + "name": "TestClass", + "methods": [ + { + "name": "", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "instanceMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "staticMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 0, + "name": "mapped" + } + ] + } + ] + }, + { + "name": "TestClass$InnerClass", + "methods": [ + { + "name": "", + "descriptor": "(LTestClass;I)V", + "parameters": [ + { + "index": 2, + "name": "mapped" + } + ] + }, + { + "name": "instanceMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "staticMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 0, + "name": "mapped" + } + ] + } + ] + }, + { + "name": "TestClass$InnerClass$InnerStaticInnerClass", + "methods": [ + { + "name": "", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "instanceMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "staticMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 0, + "name": "mapped" + } + ] + } + ] + }, + { + "name": "TestClass$StaticInnerClass", + "methods": [ + { + "name": "", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "instanceMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 1, + "name": "mapped" + } + ] + }, + { + "name": "staticMethod", + "descriptor": "(I)V", + "parameters": [ + { + "index": 0, + "name": "mapped" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/param_indices/source/TestClass.java b/src/test/resources/param_indices/source/TestClass.java new file mode 100644 index 0000000..906aac4 --- /dev/null +++ b/src/test/resources/param_indices/source/TestClass.java @@ -0,0 +1,20 @@ +public class TestClass { + public TestClass(int p) {} + public void instanceMethod(int p) {} + public static void staticMethod(int p) {} + public class InnerClass { + public InnerClass(int p) {} + public void instanceMethod(int p) {} + public static void staticMethod(int p) {} + public static class InnerStaticInnerClass { + public InnerStaticInnerClass(int p) {} + public void instanceMethod(int p) {} + public static void staticMethod(int p) {} + } + } + public static class StaticInnerClass { + public StaticInnerClass(int p) {} + public void instanceMethod(int p) {} + public static void staticMethod(int p) {} + } +} diff --git a/src/test/resources/param_indices/source/TestEnum.java b/src/test/resources/param_indices/source/TestEnum.java new file mode 100644 index 0000000..352d47a --- /dev/null +++ b/src/test/resources/param_indices/source/TestEnum.java @@ -0,0 +1,6 @@ +public enum TestEnum { + // Enum constructors take two hidden arguments (the constant name and ordinal) + // Which are part of the method signature. This is in addition to the this pointer. + private TestEnum(int p) { + } +}