33import com .google .common .collect .BiMap ;
44import com .google .common .collect .HashBiMap ;
55import com .google .common .collect .HashMultimap ;
6+ import com .google .common .collect .ImmutableSet ;
67import com .google .common .collect .Multimap ;
78import org .jspecify .annotations .Nullable ;
89import org .objectweb .asm .ClassVisitor ;
2324import org .quiltmc .enigma .api .translation .representation .entry .MethodEntry ;
2425
2526import java .util .HashSet ;
27+ import java .util .List ;
2628import java .util .Set ;
2729import java .util .stream .Stream ;
2830
31+ // TODO add tests
32+ // TODO javadoc, including getter uncertainty
2933final class RecordIndexingVisitor extends ClassVisitor {
34+ private static final int REQUIRED_GETTER_ACCESS = Opcodes .ACC_PUBLIC ;
35+ private static final int ILLEGAL_GETTER_ACCESS = Opcodes .ACC_SYNTHETIC | Opcodes .ACC_BRIDGE | Opcodes .ACC_STATIC ;
36+
37+ private static final ImmutableSet <String > ILLEGAL_GETTER_NAMES = ImmutableSet
38+ .of ("clone" , "finalize" , "getClass" , "hashCode" , "notify" , "notifyAll" , "toString" , "wait" );
39+
40+ // visitation state fields; cleared in visitEnd()
3041 private ClassEntry clazz ;
3142 private final Set <RecordComponentNode > recordComponents = new HashSet <>();
32- private final Set <FieldNode > fields = new HashSet <>();
33- private final Set <MethodNode > methods = new HashSet <>();
34-
35- private final BiMap <FieldEntry , MethodEntry > gettersByField ;
36- private final Multimap <ClassEntry , FieldEntry > fieldsByClass = HashMultimap .create ();
37- private final Multimap <ClassEntry , MethodEntry > methodsByClass = HashMultimap .create ();
43+ // this is a multimap because inner classes' fields go in the same map as their outer class's
44+ private final Multimap <String , FieldNode > fieldsByName = HashMultimap .create ();
45+ private final Multimap <String , MethodNode > methodsByDescriptor = HashMultimap .create ();
46+
47+ // index fields; contents publicly queryable
48+ private final Multimap <ClassEntry , FieldEntry > componentFieldsByClass = HashMultimap .create ();
49+ // holds methods that are at least probably getters for their field keys; superset of definiteComponentGettersByField
50+ private final BiMap <FieldEntry , MethodEntry > componentGettersByField = HashBiMap .create ();
51+ // holds methods that are definitely the getters for their field keys
52+ private final BiMap <FieldEntry , MethodEntry > definiteComponentGettersByField = HashBiMap .create ();
53+ // holds methods that are at least probably getters; superset of definiteComponentGettersByClass
54+ private final Multimap <ClassEntry , MethodEntry > componentGettersByClass = HashMultimap .create ();
55+ // holds methods that are definitely component getters
56+ private final Multimap <ClassEntry , MethodEntry > definiteComponentGettersByClass = HashMultimap .create ();
3857
3958 RecordIndexingVisitor () {
4059 super (Enigma .ASM_VERSION );
41- this .gettersByField = HashBiMap .create ();
4260 }
4361
4462 @ Nullable
4563 public MethodEntry getComponentGetter (FieldEntry componentField ) {
46- return this .gettersByField .get (componentField );
64+ return this .componentGettersByField .get (componentField );
4765 }
4866
4967 @ Nullable
5068 public FieldEntry getComponentField (MethodEntry componentGetter ) {
51- return this .gettersByField .inverse ().get (componentGetter );
69+ return this .componentGettersByField .inverse ().get (componentGetter );
70+ }
71+
72+ // TODO javadoc, prevent directly naming method (always match field)
73+ @ Nullable
74+ public MethodEntry getDefiniteComponentGetter (FieldEntry componentField ) {
75+ return this .definiteComponentGettersByField .get (componentField );
76+ }
77+
78+ // TODO javadoc
79+ @ Nullable
80+ public FieldEntry getDefiniteComponentField (MethodEntry componentGetter ) {
81+ return this .definiteComponentGettersByField .inverse ().get (componentGetter );
5282 }
5383
5484 public Stream <FieldEntry > streamComponentFields (ClassEntry recordEntry ) {
55- return this .fieldsByClass .get (recordEntry ).stream ();
85+ return this .componentFieldsByClass .get (recordEntry ).stream ();
5686 }
5787
5888 public Stream <MethodEntry > streamComponentMethods (ClassEntry recordEntry ) {
59- return this .methodsByClass .get (recordEntry ).stream ();
89+ return this .componentGettersByClass .get (recordEntry ).stream ();
90+ }
91+
92+ // TODO javadoc
93+ public Stream <MethodEntry > streamDefiniteComponentMethods (ClassEntry recordEntry ) {
94+ return this .definiteComponentGettersByClass .get (recordEntry ).stream ();
6095 }
6196
6297 @ Override
@@ -74,8 +109,8 @@ public RecordComponentVisitor visitRecordComponent(final String name, final Stri
74109 @ Override
75110 public FieldVisitor visitField (final int access , final String name , final String descriptor , final String signature , final Object value ) {
76111 if (this .clazz != null && ((access & Opcodes .ACC_PRIVATE ) != 0 ) && this .recordComponents .stream ().anyMatch (component -> component .name .equals (name ))) {
77- FieldNode node = new FieldNode (this .api , access , name , descriptor , signature , value );
78- this .fields . add ( node );
112+ final FieldNode node = new FieldNode (this .api , access , name , descriptor , signature , value );
113+ this .fieldsByName . put ( node . name , node );
79114 return node ;
80115 }
81116
@@ -85,8 +120,8 @@ public FieldVisitor visitField(final int access, final String name, final String
85120 @ Override
86121 public MethodVisitor visitMethod (final int access , final String name , final String descriptor , final String signature , final String [] exceptions ) {
87122 if (this .clazz != null && ((access & Opcodes .ACC_PUBLIC ) != 0 )) {
88- MethodNode node = new MethodNode (this .api , access , name , descriptor , signature , exceptions );
89- this .methods . add ( node );
123+ final MethodNode node = new MethodNode (this .api , access , name , descriptor , signature , exceptions );
124+ this .methodsByDescriptor . put ( node . desc , node );
90125 return node ;
91126 }
92127
@@ -103,8 +138,8 @@ public void visitEnd() {
103138 } finally {
104139 this .clazz = null ;
105140 this .recordComponents .clear ();
106- this .fields .clear ();
107- this .methods .clear ();
141+ this .fieldsByName .clear ();
142+ this .methodsByDescriptor .clear ();
108143 }
109144 }
110145
@@ -113,43 +148,74 @@ private void collectResults() {
113148 return ;
114149 }
115150
116- for (RecordComponentNode component : this .recordComponents ) {
117- FieldNode field = null ;
118- for (FieldNode node : this .fields ) {
119- if (node .name .equals (component .name ) && node .desc .equals (component .descriptor )) {
120- field = node ;
121- break ;
122- }
123- }
124-
125- if (field == null ) {
126- throw new RuntimeException ("Field not found for record component: " + component .name );
127- }
128-
129- for (MethodNode method : this .methods ) {
130- InsnList instructions = method .instructions ;
131-
132- // match bytecode to exact expected bytecode for a getter
133- // only check important instructions (ignore new frame instructions, etc.)
134- if (
135- instructions .size () == 6
136- && instructions .get (2 ).getOpcode () == Opcodes .ALOAD
137- && instructions .get (3 ) instanceof FieldInsnNode fieldInsn
138- && fieldInsn .getOpcode () == Opcodes .GETFIELD
139- && fieldInsn .owner .equals (this .clazz .getFullName ())
140- && fieldInsn .desc .equals (field .desc )
141- && fieldInsn .name .equals (field .name )
142- && instructions .get (4 ).getOpcode () >= Opcodes .IRETURN
143- && instructions .get (4 ).getOpcode () <= Opcodes .ARETURN
144- ) {
145- final FieldEntry fieldEntry = new FieldEntry (this .clazz , field .name , new TypeDescriptor (field .desc ));
146- final MethodEntry methodEntry = new MethodEntry (this .clazz , method .name , new MethodDescriptor (method .desc ));
147-
148- this .gettersByField .put (fieldEntry , methodEntry );
149- this .fieldsByClass .put (this .clazz , fieldEntry );
150- this .methodsByClass .put (this .clazz , methodEntry );
151- }
152- }
151+ this .recordComponents .stream ()
152+ .map (component -> this .fieldsByName .get (component .name ).stream ()
153+ .filter (field -> field .desc .equals (component .descriptor ))
154+ .findAny ()
155+ .orElseThrow (() -> new IllegalStateException (
156+ "Field not found for record component: " + component .name
157+ ))
158+ )
159+ .forEach (field -> {
160+ final List <MethodNode > potentialGetters = this .methodsByDescriptor
161+ .get ("()" + field .desc )
162+ .stream ()
163+ .filter (method -> (method .access & REQUIRED_GETTER_ACCESS ) == REQUIRED_GETTER_ACCESS )
164+ .filter (method -> (method .access & ILLEGAL_GETTER_ACCESS ) == 0 )
165+ .filter (method -> !ILLEGAL_GETTER_NAMES .contains (method .name ))
166+ .toList ();
167+
168+ if (potentialGetters .isEmpty ()) {
169+ throw new IllegalStateException ("No potential getters for field: " + field );
170+ } else {
171+ final FieldEntry fieldEntry =
172+ new FieldEntry (this .clazz , field .name , new TypeDescriptor (field .desc ));
173+ // index the field even if a corresponding getter can't be found
174+ this .componentFieldsByClass .put (this .clazz , fieldEntry );
175+
176+ if (potentialGetters .size () == 1 ) {
177+ this .indexGetter (potentialGetters .get (0 ), fieldEntry , true );
178+ } else {
179+ // If there are multiple methods with the getter's descriptor and access, it's impossible to
180+ // tell which is the getter because obfuscation can mismatch getter/field names.
181+ // This matching produces as few false-positives as possible by matching name, descriptor,
182+ // and the bytecode of a default (non-overriden) getter method.
183+ // It can still give a false-positive if a non-getter method's obfuscated name matches the
184+ // field's, and that non-getter the has expected descriptor and bytecode of the getter.
185+ // It also has false-negatives for getter overrides with non-default bytecode.
186+ potentialGetters .stream ()
187+ .filter (method -> method .name .equals (field .name ))
188+ // match bytecode to exact expected bytecode for a getter
189+ // only check important instructions (ignore new frame instructions, etc.)
190+ .filter (method -> {
191+ final InsnList instructions = method .instructions ;
192+ return instructions .size () == 6
193+ && instructions .get (2 ).getOpcode () == Opcodes .ALOAD
194+ && instructions .get (3 ) instanceof FieldInsnNode fieldInsn
195+ && fieldInsn .getOpcode () == Opcodes .GETFIELD
196+ && fieldInsn .owner .equals (this .clazz .getFullName ())
197+ && fieldInsn .desc .equals (field .desc )
198+ && fieldInsn .name .equals (field .name )
199+ && instructions .get (4 ).getOpcode () >= Opcodes .IRETURN
200+ && instructions .get (4 ).getOpcode () <= Opcodes .ARETURN ;
201+ })
202+ .findAny ()
203+ .ifPresent (getter -> this .indexGetter (getter , fieldEntry , false ));
204+ }
205+ }
206+ });
207+ }
208+
209+ private void indexGetter (MethodNode getterNode , FieldEntry fieldEntry , boolean definite ) {
210+ final MethodEntry getterEntry =
211+ new MethodEntry (this .clazz , getterNode .name , new MethodDescriptor (getterNode .desc ));
212+
213+ this .componentGettersByField .put (fieldEntry , getterEntry );
214+ this .componentGettersByClass .put (this .clazz , getterEntry );
215+
216+ if (definite ) {
217+ this .definiteComponentGettersByField .put (fieldEntry , getterEntry );
218+ this .definiteComponentGettersByClass .put (this .clazz , getterEntry );
153219 }
154220 }
155221}
0 commit comments