From 7c60f385f2d18dcd90c5803e78296991c561fc7c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 9 Sep 2026 20:01:05 +0800 Subject: [PATCH 1/4] Fix application of ATs to anonymous class members --- .../accesstransformers/ApplyATsVisitor.java | 36 +++++++++++-------- .../anonymous_classes/accesstransformer.cfg | 1 + .../anonymous_classes/expected/example/C.java | 12 +++++++ .../anonymous_classes/expected_report.json | 1 + .../anonymous_classes/source/example/C.java | 12 +++++++ .../net/neoforged/jst/tests/EmbeddedTest.java | 5 +++ 6 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg create mode 100644 tests/data/accesstransformer/anonymous_classes/expected/example/C.java create mode 100644 tests/data/accesstransformer/anonymous_classes/expected_report.json create mode 100644 tests/data/accesstransformer/anonymous_classes/source/example/C.java diff --git a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java index 3fc6748..e25c5e5 100644 --- a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java +++ b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java @@ -65,21 +65,14 @@ public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Ma public void visitElement(@NotNull PsiElement element) { if (element instanceof PsiClass psiClass) { if (psiClass.getQualifiedName() != null) { - String className = ClassUtil.getJVMClassName(psiClass); - if (!ats.containsClassTarget(className)) { - // Skip this class and its children, but not the inner classes - for (PsiClass innerClass : psiClass.getInnerClasses()) { - visitElement(innerClass); - } - return; - } + String className = getJVMClassName(psiClass); var classAt = pendingATs.remove(new Target.ClassTarget(className)); apply(classAt, psiClass, psiClass); // We also remove any possible inner class ATs declared for that class as all class targets targeting inner classes // generate a InnerClassTarget AT if (psiClass.getParent() instanceof PsiClass parent) { - pendingATs.remove(new Target.InnerClassTarget(ClassUtil.getJVMClassName(parent), className)); + pendingATs.remove(new Target.InnerClassTarget(getJVMClassName(parent), className)); } checkImplicitConstructor(psiClass, className, classAt); @@ -107,20 +100,33 @@ public void visitElement(@NotNull PsiElement element) { } else if (element instanceof PsiField field) { final var cls = field.getContainingClass(); if (cls != null && cls.getQualifiedName() != null) { - String className = ClassUtil.getJVMClassName(cls); + String className = getJVMClassName(cls); apply(pendingATs.remove(new Target.FieldTarget(className, field.getName())), field, cls); } } else if (element instanceof PsiMethod method) { final var cls = method.getContainingClass(); - if (cls != null && cls.getQualifiedName() != null) { - String className = ClassUtil.getJVMClassName(cls); - apply(pendingATs.remove(method(className, method)), method, cls); + if (cls != null) { + var className = getJVMClassName(cls); + if (!className.isEmpty()) { + apply(pendingATs.remove(method(className, method)), method, cls); + } } } element.acceptChildren(this); } + // This returns a JVM class name like ClassUtil#getJVMClassName, but accounts for anonymous classes + private String getJVMClassName(PsiClass aClass) { + // Quick-path + final String qName = ClassUtil.getJVMClassName(aClass); + if (qName != null) return qName; + + var sb = new StringBuilder(); + PsiHelper.getBinaryClassName(aClass, sb); // This returns a binary class name with '/'s; convert it to a JVM class name with '.'s + return sb.toString().replace('/', '.'); + } + private void apply(@Nullable Transformation at, PsiModifierListOwner owner, PsiClass containingClass) { if (at == null) return; if (!at.isValid()) { @@ -132,7 +138,7 @@ private void apply(@Nullable Transformation at, PsiModifierListOwner owner, PsiC @Override public String toString() { if (owner instanceof PsiClass cls) { - return ClassUtil.getJVMClassName(cls); + return getJVMClassName(cls); } String memberName; if (owner instanceof PsiMethod mtd && mtd.isConstructor()) { @@ -140,7 +146,7 @@ public String toString() { } else { memberName = ((NavigationItem) owner).getName(); } - return memberName + " of " + ClassUtil.getJVMClassName(containingClass); + return memberName + " of " + getJVMClassName(containingClass); } }; logger.debug("Applying AT %s to %s", at, targetInfo); diff --git a/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg b/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg new file mode 100644 index 0000000..cf9755f --- /dev/null +++ b/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg @@ -0,0 +1 @@ +public example.C$1 method()V diff --git a/tests/data/accesstransformer/anonymous_classes/expected/example/C.java b/tests/data/accesstransformer/anonymous_classes/expected/example/C.java new file mode 100644 index 0000000..6e6d37d --- /dev/null +++ b/tests/data/accesstransformer/anonymous_classes/expected/example/C.java @@ -0,0 +1,12 @@ +package example; + +public class C { + static C INSTANCE = new C() { + @Override + public void method() { + } + }; + + protected void method() { + } +} diff --git a/tests/data/accesstransformer/anonymous_classes/expected_report.json b/tests/data/accesstransformer/anonymous_classes/expected_report.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/tests/data/accesstransformer/anonymous_classes/expected_report.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/data/accesstransformer/anonymous_classes/source/example/C.java b/tests/data/accesstransformer/anonymous_classes/source/example/C.java new file mode 100644 index 0000000..97643b1 --- /dev/null +++ b/tests/data/accesstransformer/anonymous_classes/source/example/C.java @@ -0,0 +1,12 @@ +package example; + +public class C { + static C INSTANCE = new C() { + @Override + protected void method() { + } + }; + + protected void method() { + } +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index 57515a6..785dc82 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -308,6 +308,11 @@ void testIllegal() throws Exception { void testFolderClasspathEntries() throws Exception { runATTest("folder_classpath_entry", "--classpath=" + testDataRoot.resolve("accesstransformer/folder_classpath_entry/deps")); } + + @Test + void testAnonymousClasses() throws Exception { + runATTest("anonymous_classes"); + } } @Nested From 2199404833bfbb9eed452f00bd6c7942eb6c7183 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 9 Sep 2026 20:06:43 +0800 Subject: [PATCH 2/4] Fix wildcard ATs for anonymous classes --- .../accesstransformers/ApplyATsVisitor.java | 52 +++++++++---------- .../anonymous_classes/accesstransformer.cfg | 1 + .../anonymous_classes/expected/example/C.java | 15 +++++- .../anonymous_classes/source/example/C.java | 15 +++++- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java index e25c5e5..736eaf5 100644 --- a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java +++ b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java @@ -64,37 +64,35 @@ public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Ma @Override public void visitElement(@NotNull PsiElement element) { if (element instanceof PsiClass psiClass) { - if (psiClass.getQualifiedName() != null) { - String className = getJVMClassName(psiClass); - - var classAt = pendingATs.remove(new Target.ClassTarget(className)); - apply(classAt, psiClass, psiClass); - // We also remove any possible inner class ATs declared for that class as all class targets targeting inner classes - // generate a InnerClassTarget AT - if (psiClass.getParent() instanceof PsiClass parent) { - pendingATs.remove(new Target.InnerClassTarget(getJVMClassName(parent), className)); - } + String className = getJVMClassName(psiClass); + + var classAt = pendingATs.remove(new Target.ClassTarget(className)); + apply(classAt, psiClass, psiClass); + // We also remove any possible inner class ATs declared for that class as all class targets targeting inner classes + // generate a InnerClassTarget AT + if (psiClass.getParent() instanceof PsiClass parent) { + pendingATs.remove(new Target.InnerClassTarget(getJVMClassName(parent), className)); + } - checkImplicitConstructor(psiClass, className, classAt); + checkImplicitConstructor(psiClass, className, classAt); - var fieldWildcard = pendingATs.remove(new Target.WildcardFieldTarget(className)); - if (fieldWildcard != null) { - for (PsiField field : psiClass.getFields()) { - // Apply a merged state if an explicit AT for the field already exists - var newState = merge(fieldWildcard, pendingATs.remove(new Target.FieldTarget(className, field.getName()))); - logger.debug("Applying field wildcard AT %s to %s in %s", newState, field.getName(), className); - apply(newState, field, psiClass); - } + var fieldWildcard = pendingATs.remove(new Target.WildcardFieldTarget(className)); + if (fieldWildcard != null) { + for (PsiField field : psiClass.getFields()) { + // Apply a merged state if an explicit AT for the field already exists + var newState = merge(fieldWildcard, pendingATs.remove(new Target.FieldTarget(className, field.getName()))); + logger.debug("Applying field wildcard AT %s to %s in %s", newState, field.getName(), className); + apply(newState, field, psiClass); } + } - var methodWildcard = pendingATs.remove(new Target.WildcardMethodTarget(className)); - if (methodWildcard != null) { - for (PsiMethod method : psiClass.getMethods()) { - // Apply a merged state if an explicit AT for the method already exists - var newState = merge(methodWildcard, pendingATs.remove(method(className, method))); - logger.debug("Applying method wildcard AT %s to %s in %s", newState, method.getName(), className); - apply(newState, method, psiClass); - } + var methodWildcard = pendingATs.remove(new Target.WildcardMethodTarget(className)); + if (methodWildcard != null) { + for (PsiMethod method : psiClass.getMethods()) { + // Apply a merged state if an explicit AT for the method already exists + var newState = merge(methodWildcard, pendingATs.remove(method(className, method))); + logger.debug("Applying method wildcard AT %s to %s in %s", newState, method.getName(), className); + apply(newState, method, psiClass); } } } else if (element instanceof PsiField field) { diff --git a/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg b/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg index cf9755f..677b740 100644 --- a/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg +++ b/tests/data/accesstransformer/anonymous_classes/accesstransformer.cfg @@ -1 +1,2 @@ public example.C$1 method()V +public example.C$2 *() diff --git a/tests/data/accesstransformer/anonymous_classes/expected/example/C.java b/tests/data/accesstransformer/anonymous_classes/expected/example/C.java index 6e6d37d..32e829a 100644 --- a/tests/data/accesstransformer/anonymous_classes/expected/example/C.java +++ b/tests/data/accesstransformer/anonymous_classes/expected/example/C.java @@ -1,12 +1,25 @@ package example; public class C { - static C INSTANCE = new C() { + static C REGULAR = new C() { @Override public void method() { } }; + static C WILDCARD = new C() { + @Override + public void method() { + } + + @Override + public void method2() { + } + }; + protected void method() { } + + protected void method2() { + } } diff --git a/tests/data/accesstransformer/anonymous_classes/source/example/C.java b/tests/data/accesstransformer/anonymous_classes/source/example/C.java index 97643b1..244ddcc 100644 --- a/tests/data/accesstransformer/anonymous_classes/source/example/C.java +++ b/tests/data/accesstransformer/anonymous_classes/source/example/C.java @@ -1,12 +1,25 @@ package example; public class C { - static C INSTANCE = new C() { + static C REGULAR = new C() { @Override protected void method() { } }; + static C WILDCARD = new C() { + @Override + protected void method() { + } + + @Override + protected void method2() { + } + }; + protected void method() { } + + protected void method2() { + } } From 817f32b283152fe016fbb8b0909d4e0aedc3cb97 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 10 Sep 2026 02:26:54 +0800 Subject: [PATCH 3/4] Add skip check for non-targeted classes --- .../accesstransformers/ApplyATsVisitor.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java index 736eaf5..8f2f5c3 100644 --- a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java +++ b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java @@ -14,6 +14,9 @@ import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.ClassUtil; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; import net.neoforged.accesstransformer.parser.AccessTransformerFiles; import net.neoforged.accesstransformer.parser.Target; import net.neoforged.accesstransformer.parser.Transformation; @@ -51,6 +54,7 @@ class ApplyATsVisitor extends PsiRecursiveElementVisitor { private final Map pendingATs; private final Logger logger; private final ProblemReporter problemReporter; + private final Set allTargetedClasses = new HashSet<>();; boolean errored = false; public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Map pendingATs, Logger logger, ProblemReporter problemReporter) { @@ -59,12 +63,37 @@ public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Ma this.logger = logger; this.pendingATs = pendingATs; this.problemReporter = problemReporter; + + // To skip any class that has no access transformer targeting any part of it (incl. inner classes or anonymous classes), + // take the set of targeted classes and create a new set that consists of those classes and all of their parent classes + // (Example: "example.C$Inner$1" results in "example.C", "example.C$Inner", and "example.C$Inner$1") + // + // This means we may have to iterate over elements that might not be targeted at all. (In the previous example, that'd + // be methods and fields in "example.C" and "example.C$Inner" if the only target is "example.C$Inner$1") + // However, that is the price we pay to ensure any targeted class is actually visited, even anonymous ones. + Deque queue = new ArrayDeque<>(ats.getTargets()); + String name; + while ((name = queue.poll()) != null) { + allTargetedClasses.add(name); + int lastIndex = name.lastIndexOf('$'); + if (lastIndex != -1) { + // Strip the last-most '$'-separated element and add the resulting string back to the queue + queue.push(name.substring(0, lastIndex)); + } + } } @Override public void visitElement(@NotNull PsiElement element) { if (element instanceof PsiClass psiClass) { String className = getJVMClassName(psiClass); + if (!allTargetedClasses.contains(className)) { + // Skip this class and its children, but not the inner classes + for (PsiClass innerClass : psiClass.getInnerClasses()) { + visitElement(innerClass); + } + return; + } var classAt = pendingATs.remove(new Target.ClassTarget(className)); apply(classAt, psiClass, psiClass); From f9275998fe28b9a2d30a2bd093cfa79af916c93e Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 10 Sep 2026 03:08:38 +0800 Subject: [PATCH 4/4] Simplify targeted classes set generation --- .../accesstransformers/ApplyATsVisitor.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java index 8f2f5c3..93f9269 100644 --- a/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java +++ b/accesstransformers/src/main/java/net/neoforged/jst/accesstransformers/ApplyATsVisitor.java @@ -14,8 +14,6 @@ import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.ClassUtil; -import java.util.ArrayDeque; -import java.util.Deque; import java.util.HashSet; import net.neoforged.accesstransformer.parser.AccessTransformerFiles; import net.neoforged.accesstransformer.parser.Target; @@ -71,15 +69,13 @@ public ApplyATsVisitor(AccessTransformerFiles ats, Replacements replacements, Ma // This means we may have to iterate over elements that might not be targeted at all. (In the previous example, that'd // be methods and fields in "example.C" and "example.C$Inner" if the only target is "example.C$Inner$1") // However, that is the price we pay to ensure any targeted class is actually visited, even anonymous ones. - Deque queue = new ArrayDeque<>(ats.getTargets()); - String name; - while ((name = queue.poll()) != null) { - allTargetedClasses.add(name); - int lastIndex = name.lastIndexOf('$'); - if (lastIndex != -1) { - // Strip the last-most '$'-separated element and add the resulting string back to the queue - queue.push(name.substring(0, lastIndex)); - } + for (String target : ats.getTargets()) { + int lastIndex = target.length(); + do { + target = target.substring(0, lastIndex); + allTargetedClasses.add(target); + lastIndex = target.lastIndexOf('$'); + } while (lastIndex != -1); } }