[ByteCodeViewer] IDEA-372346 Fix local classes not working

Allow local classes to work with bytecode viewer.

Add a method in ClassUtil that takes into account anonymous and local classes
and move duplicated logic into that method.

Presumably, everything that used JavaAnonymousClassesHelper wanted local classes too.

Also deprecate ClassUtil#getJVMClassName

#IDEA-372346 fixed

closes https://github.com/JetBrains/intellij-community/pull/3044

GitOrigin-RevId: 7d300719c079d3de5a7cb589d50431326526faa0
This commit is contained in:
joe
2025-05-08 22:35:37 +01:00
committed by intellij-monorepo-bot
parent 06a9b9bde5
commit 75ecbf846d
9 changed files with 184 additions and 10 deletions

View File

@@ -479,7 +479,7 @@ public final class JVMNameUtil {
}
public static @Nullable String getClassVMName(@Nullable PsiClass containingClass) {
// no support for local classes for now
// no support for local classes for now. TODO: use JavaLocalClassesHelper
if (containingClass == null) return null;
if (containingClass instanceof PsiAnonymousClass) {
String parentName = getClassVMName(PsiTreeUtil.getParentOfType(containingClass, PsiClass.class));

View File

@@ -103,6 +103,7 @@ final class DiscoveredTestsTreeModel extends BaseTreeModel<Object> implements In
return myInvoker;
}
// TODO: this method can be replaced with ClassUtil.getBinaryClassName so it handles local classes.
public static @Nullable String getClassName(@NotNull PsiClass c) {
if (c instanceof PsiAnonymousClass) {
PsiClass containingClass = PsiTreeUtil.getParentOfType(c, PsiClass.class);

View File

@@ -0,0 +1,67 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.util;
import com.intellij.openapi.util.Key;
import com.intellij.psi.*;
import com.intellij.psi.util.*;
import com.intellij.util.containers.ObjectIntHashMap;
import com.intellij.util.containers.ObjectIntMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
public final class JavaLocalClassesHelper {
private static final Key<ParameterizedCachedValue<Map<PsiClass, String>, PsiClass>>
LOCAL_CLASS_NAME = Key.create("LOCAL_CLASS_NAME");
private static final LocalClassProvider LOCAL_CLASS_PROVIDER = new LocalClassProvider();
/**
* Returns the part of the class name suitable for being appended to the containing class' binary name to produce the local class' binary
* name, see {@link ClassUtil#getBinaryClassName} and JLS 13.1.
*
* <p>For example, if the binary name of a local class is {@code com.example.Foo$1Local}, then this method will return the {@code $1Local}
* part.
*/
public static @Nullable String getName(@NotNull PsiClass cls) {
if (!PsiUtil.isLocalClass(cls)) {
throw new IllegalArgumentException("class " + cls + " must be a local class");
}
final PsiClass upper = PsiTreeUtil.getParentOfType(cls, PsiClass.class);
if (upper == null) {
return null;
}
ParameterizedCachedValue<Map<PsiClass, String>, PsiClass> value = upper.getUserData(LOCAL_CLASS_NAME);
if (value == null) {
value = CachedValuesManager.getManager(upper.getProject()).createParameterizedCachedValue(LOCAL_CLASS_PROVIDER, false);
upper.putUserData(LOCAL_CLASS_NAME, value);
}
return value.getValue(upper).get(cls);
}
private static final class LocalClassProvider implements ParameterizedCachedValueProvider<Map<PsiClass, String>, PsiClass> {
@Override
public CachedValueProvider.Result<Map<PsiClass, String>> compute(final PsiClass upper) {
final Map<PsiClass, String> map = new HashMap<>();
upper.accept(new JavaRecursiveElementWalkingVisitor() {
final ObjectIntMap<String> indexByName = new ObjectIntHashMap<>();
@Override
public void visitClass(@NotNull PsiClass aClass) {
if (aClass == upper) {
super.visitClass(aClass);
} else if (PsiUtil.isLocalClass(aClass)) {
String name = aClass.getName();
if (name != null) {
int index = indexByName.getOrDefault(name, 0) + 1;
indexByName.put(name, index);
map.put(aClass, "$" + index + name);
}
}
}
});
return CachedValueProvider.Result.create(map, upper);
}
}
}

View File

@@ -1,6 +1,8 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.util;
import com.intellij.ide.util.JavaAnonymousClassesHelper;
import com.intellij.ide.util.JavaLocalClassesHelper;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NlsSafe;
@@ -213,6 +215,12 @@ public final class ClassUtil {
return Character.isDigit(name.charAt(0));
}
/**
* Returns the binary class name for top-level and nested classes.
*
* @deprecated Does not work for anonymous classes and local classes. Use {@link #getBinaryClassName} instead.
*/
@Deprecated
public static @Nullable @NlsSafe String getJVMClassName(@NotNull PsiClass aClass) {
final PsiClass containingClass = aClass.getContainingClass();
if (containingClass != null) {
@@ -225,6 +233,42 @@ public final class ClassUtil {
return aClass.getQualifiedName();
}
/**
* Returns the binary class name, i.e. the string that would be returned by {@link Class#getName}. See JLS 13.1.
*
* <ul>
* <li><b>Top-level classes</b> return the qualified name of the class, e.g. {@code com.example.Foo}</li>
* <li><b>Nested classes</b>, i.e. named classes nested directly within the body of another class, return the binary name of the outer
* class, followed by a {@code $} plus the name of the nested class, e.g. {@code com.example.Foo$Inner}</li>
* <li><b>Anonymous classes</b>, i.e. unnamed classes created with a {@code new} expression, return the binary name of the class they
* are within, followed by a {@code $} plus a number indexing which anonymous class within the outer class we are referring to, e.g.
* {@code com.example.Foo$1}</li>
* <li><b>Local classes</b>, i.e. named classes within a method, return the binary name of the class they are within, followed by a
* {@code $} plus a number and then the local class' name. The number indexes which local class with that same name within the outer
* class we are referring to (multiple local classes within the same class may have the same name if they are in different methods).
* E.g. {@code com.example.Foo$1Local}</li>
* </ul>
*/
public static @Nullable @NlsSafe String getBinaryClassName(@NotNull PsiClass aClass) {
if (PsiUtil.isLocalOrAnonymousClass(aClass)) {
PsiClass parentClass = PsiTreeUtil.getParentOfType(aClass, PsiClass.class);
if (parentClass == null) {
return null;
}
String parentName = getBinaryClassName(parentClass);
if (parentName == null) {
return null;
}
if (aClass instanceof PsiAnonymousClass) {
return parentName + JavaAnonymousClassesHelper.getName((PsiAnonymousClass) aClass);
} else {
return parentName + JavaLocalClassesHelper.getName(aClass);
}
}
return getJVMClassName(aClass);
}
/**
* Looks for inner and anonymous classes by internal name ('pkg/Top$Inner').
*/

View File

@@ -0,0 +1,9 @@
class MultipleNames {
void method1() {
class A {}
}
void method2() {
class <caret>B {}
}
}

View File

@@ -0,0 +1,9 @@
class SameName {
void method1() {
class Local {}
}
void method2() {
class <caret>Local {}
}
}

View File

@@ -0,0 +1,5 @@
class Simple {
void method() {
class <caret>Local {}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.ide.util;
import com.intellij.JavaTestUtil;
import com.intellij.ide.util.JavaLocalClassesHelper;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.util.ObjectUtils;
public class JavaLocalClassesHelperTest extends LightJavaCodeInsightFixtureTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
myFixture.configureByFile(getTestName(false) + ".java");
}
public void testSimple() {
doTest("$1Local");
}
public void testMultipleNames() {
doTest("$1B");
}
public void testSameName() {
doTest("$2Local");
}
private void doTest(String expectedName) {
final PsiElement element = PsiUtilBase.getElementAtCaret(myFixture.getEditor()).getParent().getParent();
PsiDeclarationStatement declaration = ObjectUtils.tryCast(element, PsiDeclarationStatement.class);
PsiClass aClass = declaration == null ? null : ObjectUtils.tryCast(declaration.getDeclaredElements()[0], PsiClass.class);
assert aClass != null && PsiUtil.isLocalClass(aClass) : "There should be local class at caret but " + element + " found";
assertEquals(expectedName, JavaLocalClassesHelper.getName(aClass));
}
@Override
protected String getBasePath() {
return JavaTestUtil.getRelativeJavaTestDataPath() + "/local/";
}
}

View File

@@ -2,7 +2,6 @@
package com.intellij.byteCodeViewer
import com.intellij.ide.highlighter.JavaClassFileType
import com.intellij.ide.util.JavaAnonymousClassesHelper
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.roots.CompilerModuleExtension
@@ -30,7 +29,7 @@ object ByteCodeViewerManager {
val fileClass = aClass.containingClassFileClass()
val file = fileClass.originalElement.containingFile.virtualFile ?: return null
val fileIndex = ProjectFileIndex.getInstance(aClass.project)
val jvmClassName = getJVMClassName(aClass) ?: return null
val jvmClassName = ClassUtil.getBinaryClassName(aClass) ?: return null
return if (FileTypeRegistry.getInstance().isFileOfType(file, JavaClassFileType.INSTANCE)) {
// compiled class; looking for the right .class file (inner class 'A.B' is "contained" in 'A.class', but we need 'A$B.class')
file.parent.findChild(StringUtil.getShortName(jvmClassName) + ".class")
@@ -52,13 +51,6 @@ object ByteCodeViewerManager {
return findClassFile(aClass)?.contentsToByteArray(false)
}
private fun getJVMClassName(aClass: PsiClass): String? {
if (aClass !is PsiAnonymousClass) return ClassUtil.getJVMClassName(aClass)
val containingClass = PsiTreeUtil.getParentOfType<PsiClass?>(aClass, PsiClass::class.java)
if (containingClass != null) return getJVMClassName(containingClass) + JavaAnonymousClassesHelper.getName(aClass)
return null
}
@JvmStatic
fun getContainingClass(psiElement: PsiElement): PsiClass? {
for (searcher in CLASS_SEARCHER_EP.extensionList) {