[java-decompiler] IDEA-246839 Support java records in decompiler

Also: support preview levels in ClsFileImpl
Also fixes: IDEA-247551 Exception on first opening of record .class file

GitOrigin-RevId: 4362d669d1c16b8230d6d8ab803465b6a7476803
master
Tagir Valeev 4 years ago committed by intellij-monorepo-bot
parent 0749965bc9
commit 1651445c90
  1. 81
      src/org/jetbrains/java/decompiler/main/ClassWriter.java
  2. 12
      src/org/jetbrains/java/decompiler/struct/StructClass.java
  3. 47
      src/org/jetbrains/java/decompiler/struct/StructRecordComponent.java
  4. 4
      src/org/jetbrains/java/decompiler/struct/attr/StructGeneralAttribute.java
  5. 38
      src/org/jetbrains/java/decompiler/struct/attr/StructRecordAttribute.java
  6. 5
      test/org/jetbrains/java/decompiler/SingleClassesTest.java
  7. BIN
      testData/classes/records/TestRecordAnno.class
  8. BIN
      testData/classes/records/TestRecordEmpty.class
  9. BIN
      testData/classes/records/TestRecordGenericVararg.class
  10. BIN
      testData/classes/records/TestRecordSimple.class
  11. BIN
      testData/classes/records/TestRecordVararg.class
  12. 66
      testData/results/TestRecordAnno.dec
  13. 35
      testData/results/TestRecordEmpty.dec
  14. 66
      testData/results/TestRecordGenericVararg.dec
  15. 64
      testData/results/TestRecordSimple.dec
  16. 64
      testData/results/TestRecordVararg.dec
  17. 17
      testData/src/records/TestRecordAnno.java
  18. 3
      testData/src/records/TestRecordEmpty.java
  19. 6
      testData/src/records/TestRecordGenericVararg.java
  20. 3
      testData/src/records/TestRecordSimple.java
  21. 3
      testData/src/records/TestRecordVararg.java

@ -14,10 +14,7 @@ import org.jetbrains.java.decompiler.modules.decompiler.stats.RootStatement;
import org.jetbrains.java.decompiler.modules.decompiler.vars.VarTypeProcessor; import org.jetbrains.java.decompiler.modules.decompiler.vars.VarTypeProcessor;
import org.jetbrains.java.decompiler.modules.decompiler.vars.VarVersionPair; import org.jetbrains.java.decompiler.modules.decompiler.vars.VarVersionPair;
import org.jetbrains.java.decompiler.modules.renamer.PoolInterceptor; import org.jetbrains.java.decompiler.modules.renamer.PoolInterceptor;
import org.jetbrains.java.decompiler.struct.StructClass; import org.jetbrains.java.decompiler.struct.*;
import org.jetbrains.java.decompiler.struct.StructField;
import org.jetbrains.java.decompiler.struct.StructMember;
import org.jetbrains.java.decompiler.struct.StructMethod;
import org.jetbrains.java.decompiler.struct.attr.*; import org.jetbrains.java.decompiler.struct.attr.*;
import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant; import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant;
import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor;
@ -28,6 +25,7 @@ import org.jetbrains.java.decompiler.util.InterpreterUtil;
import org.jetbrains.java.decompiler.util.TextBuffer; import org.jetbrains.java.decompiler.util.TextBuffer;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
public class ClassWriter { public class ClassWriter {
private final PoolInterceptor interceptor; private final PoolInterceptor interceptor;
@ -162,11 +160,19 @@ public class ClassWriter {
dummy_tracer.incrementCurrentSourceLine(buffer.countLines(start_class_def)); dummy_tracer.incrementCurrentSourceLine(buffer.countLines(start_class_def));
List<StructRecordComponent> components = cl.getRecordComponents();
for (StructField fd : cl.getFields()) { for (StructField fd : cl.getFields()) {
boolean hide = fd.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) || boolean hide = fd.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor())); wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
if (hide) continue; if (hide) continue;
if (components != null && fd.getAccessFlags() == (CodeConstants.ACC_FINAL | CodeConstants.ACC_PRIVATE) &&
components.stream().anyMatch(c -> c.getName().equals(fd.getName()) && c.getDescriptor().equals(fd.getDescriptor()))) {
// Record component field: skip it
continue;
}
boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM); boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
if (isEnum) { if (isEnum) {
if (enumFields) { if (enumFields) {
@ -302,6 +308,13 @@ public class ClassWriter {
flags &= ~CodeConstants.ACC_FINAL; flags &= ~CodeConstants.ACC_FINAL;
} }
List<StructRecordComponent> components = cl.getRecordComponents();
if (components != null) {
// records are implicitly final
flags &= ~CodeConstants.ACC_FINAL;
}
appendModifiers(buffer, flags, CLASS_ALLOWED, isInterface, CLASS_EXCLUDED); appendModifiers(buffer, flags, CLASS_ALLOWED, isInterface, CLASS_EXCLUDED);
if (isEnum) { if (isEnum) {
@ -313,6 +326,9 @@ public class ClassWriter {
} }
buffer.append("interface "); buffer.append("interface ");
} }
else if (components != null) {
buffer.append("record ");
}
else { else {
buffer.append("class "); buffer.append("class ");
} }
@ -324,9 +340,22 @@ public class ClassWriter {
appendTypeParameters(buffer, descriptor.fparameters, descriptor.fbounds); appendTypeParameters(buffer, descriptor.fparameters, descriptor.fbounds);
} }
if (components != null) {
buffer.append('(');
for (int i = 0; i < components.size(); i++) {
StructRecordComponent cd = components.get(i);
if (i > 0) {
buffer.append(", ");
}
boolean varArgComponent = i == components.size() - 1 && isVarArgRecord(cl);
recordComponentToJava(cd, buffer, varArgComponent);
}
buffer.append(')');
}
buffer.append(' '); buffer.append(' ');
if (!isEnum && !isInterface && cl.superClass != null) { if (!isEnum && !isInterface && components == null && cl.superClass != null) {
VarType supertype = new VarType(cl.superClass.getString(), true); VarType supertype = new VarType(cl.superClass.getString(), true);
if (!VarType.VARTYPE_OBJECT.equals(supertype)) { if (!VarType.VARTYPE_OBJECT.equals(supertype)) {
buffer.append("extends "); buffer.append("extends ");
@ -362,6 +391,13 @@ public class ClassWriter {
buffer.append('{').appendLineSeparator(); buffer.append('{').appendLineSeparator();
} }
private boolean isVarArgRecord(StructClass cl) {
String canonicalConstructorDescriptor =
cl.getRecordComponents().stream().map(c -> c.getDescriptor()).collect(Collectors.joining("", "(", ")V"));
StructMethod ctor = cl.getMethod(CodeConstants.INIT_NAME, canonicalConstructorDescriptor);
return ctor != null && ctor.hasModifier(CodeConstants.ACC_VARARGS);
}
private void fieldToJava(ClassWrapper wrapper, StructClass cl, StructField fd, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) { private void fieldToJava(ClassWrapper wrapper, StructClass cl, StructField fd, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {
int start = buffer.length(); int start = buffer.length();
boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE); boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE);
@ -452,6 +488,33 @@ public class ClassWriter {
} }
} }
private static void recordComponentToJava(StructRecordComponent cd, TextBuffer buffer, boolean varArgComponent) {
appendAnnotations(buffer, -1, cd, TypeAnnotation.FIELD);
VarType fieldType = new VarType(cd.getDescriptor(), false);
GenericFieldDescriptor descriptor = null;
if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
StructGenericSignatureAttribute attr = cd.getAttribute(StructGeneralAttribute.ATTRIBUTE_SIGNATURE);
if (attr != null) {
descriptor = GenericMain.parseFieldSignature(attr.getSignature());
}
}
if (descriptor != null) {
buffer.append(GenericMain.getGenericCastTypeName(varArgComponent ? descriptor.type.decreaseArrayDim() : descriptor.type));
}
else {
buffer.append(ExprProcessor.getCastTypeName(varArgComponent ? fieldType.decreaseArrayDim() : fieldType));
}
if (varArgComponent) {
buffer.append("...");
}
buffer.append(' ');
buffer.append(cd.getName());
}
private static void methodLambdaToJava(ClassNode lambdaNode, private static void methodLambdaToJava(ClassNode lambdaNode,
ClassWrapper classWrapper, ClassWrapper classWrapper,
StructMethod mt, StructMethod mt,
@ -959,7 +1022,13 @@ public class ClassWriter {
for (AnnotationExprent annotation : attribute.getAnnotations()) { for (AnnotationExprent annotation : attribute.getAnnotations()) {
String text = annotation.toJava(indent, BytecodeMappingTracer.DUMMY).toString(); String text = annotation.toJava(indent, BytecodeMappingTracer.DUMMY).toString();
filter.add(text); filter.add(text);
buffer.append(text).appendLineSeparator(); buffer.append(text);
if (indent < 0) {
buffer.append(' ');
}
else {
buffer.appendLineSeparator();
}
} }
} }
} }

@ -2,6 +2,8 @@
package org.jetbrains.java.decompiler.struct; package org.jetbrains.java.decompiler.struct;
import org.jetbrains.java.decompiler.code.CodeConstants; import org.jetbrains.java.decompiler.code.CodeConstants;
import org.jetbrains.java.decompiler.struct.attr.StructGeneralAttribute;
import org.jetbrains.java.decompiler.struct.attr.StructRecordAttribute;
import org.jetbrains.java.decompiler.struct.consts.ConstantPool; import org.jetbrains.java.decompiler.struct.consts.ConstantPool;
import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant; import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant;
import org.jetbrains.java.decompiler.struct.lazy.LazyLoader; import org.jetbrains.java.decompiler.struct.lazy.LazyLoader;
@ -10,6 +12,7 @@ import org.jetbrains.java.decompiler.util.InterpreterUtil;
import org.jetbrains.java.decompiler.util.VBStyleCollection; import org.jetbrains.java.decompiler.util.VBStyleCollection;
import java.io.IOException; import java.io.IOException;
import java.util.List;
/* /*
class_file { class_file {
@ -132,6 +135,15 @@ public class StructClass extends StructMember {
return pool; return pool;
} }
/**
* @return list of record components; null if this class is not a record
*/
public List<StructRecordComponent> getRecordComponents() {
StructRecordAttribute recordAttr = getAttribute(StructGeneralAttribute.ATTRIBUTE_RECORD);
if (recordAttr == null) return null;
return recordAttr.getComponents();
}
public int[] getInterfaces() { public int[] getInterfaces() {
return interfaces; return interfaces;
} }

@ -0,0 +1,47 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.java.decompiler.struct;
import org.jetbrains.java.decompiler.struct.consts.ConstantPool;
import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant;
import org.jetbrains.java.decompiler.util.DataInputFullStream;
import java.io.IOException;
/*
record_component_info {
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
*/
public class StructRecordComponent extends StructMember {
private final String name;
private final String descriptor;
public StructRecordComponent(DataInputFullStream in, ConstantPool pool) throws IOException {
accessFlags = 0;
int nameIndex = in.readUnsignedShort();
int descriptorIndex = in.readUnsignedShort();
name = ((PrimitiveConstant)pool.getConstant(nameIndex)).getString();
descriptor = ((PrimitiveConstant)pool.getConstant(descriptorIndex)).getString();
attributes = readAttributes(in, pool);
}
public String getName() {
return name;
}
public String getDescriptor() {
return descriptor;
}
@Override
public String toString() {
return name;
}
}

@ -34,6 +34,7 @@ public class StructGeneralAttribute {
public static final Key<StructGeneralAttribute> ATTRIBUTE_DEPRECATED = new Key<>("Deprecated"); public static final Key<StructGeneralAttribute> ATTRIBUTE_DEPRECATED = new Key<>("Deprecated");
public static final Key<StructLineNumberTableAttribute> ATTRIBUTE_LINE_NUMBER_TABLE = new Key<>("LineNumberTable"); public static final Key<StructLineNumberTableAttribute> ATTRIBUTE_LINE_NUMBER_TABLE = new Key<>("LineNumberTable");
public static final Key<StructMethodParametersAttribute> ATTRIBUTE_METHOD_PARAMETERS = new Key<>("MethodParameters"); public static final Key<StructMethodParametersAttribute> ATTRIBUTE_METHOD_PARAMETERS = new Key<>("MethodParameters");
public static final Key<StructRecordAttribute> ATTRIBUTE_RECORD = new Key<>("Record");
public static class Key<T extends StructGeneralAttribute> { public static class Key<T extends StructGeneralAttribute> {
private final String name; private final String name;
@ -97,6 +98,9 @@ public class StructGeneralAttribute {
else if (ATTRIBUTE_METHOD_PARAMETERS.getName().equals(name)) { else if (ATTRIBUTE_METHOD_PARAMETERS.getName().equals(name)) {
attr = new StructMethodParametersAttribute(); attr = new StructMethodParametersAttribute();
} }
else if (ATTRIBUTE_RECORD.getName().equals(name)) {
attr = new StructRecordAttribute();
}
else { else {
// unsupported attribute // unsupported attribute
return null; return null;

@ -0,0 +1,38 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.java.decompiler.struct.attr;
import org.jetbrains.java.decompiler.struct.StructRecordComponent;
import org.jetbrains.java.decompiler.struct.consts.ConstantPool;
import org.jetbrains.java.decompiler.util.DataInputFullStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/*
Record_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 components_count;
record_component_info components[components_count];
}
*/
public class StructRecordAttribute extends StructGeneralAttribute {
List<StructRecordComponent> components;
@Override
public void initContent(DataInputFullStream data,
ConstantPool pool) throws IOException {
int componentCount = data.readUnsignedShort();
StructRecordComponent[] components = new StructRecordComponent[componentCount];
for (int i = 0; i < componentCount; i++) {
components[i] = new StructRecordComponent(data, pool);
}
this.components = Arrays.asList(components);
}
public List<StructRecordComponent> getComponents() {
return Collections.unmodifiableList(components);
}
}

@ -129,6 +129,11 @@ public class SingleClassesTest {
@Test public void testSuspendLambda() { doTest("pkg/TestSuspendLambdaKt"); } @Test public void testSuspendLambda() { doTest("pkg/TestSuspendLambdaKt"); }
@Test public void testNamedSuspendFun2Kt() { doTest("pkg/TestNamedSuspendFun2Kt"); } @Test public void testNamedSuspendFun2Kt() { doTest("pkg/TestNamedSuspendFun2Kt"); }
@Test public void testGenericArgs() { doTest("pkg/TestGenericArgs"); } @Test public void testGenericArgs() { doTest("pkg/TestGenericArgs"); }
@Test public void testRecordEmpty() { doTest("records/TestRecordEmpty"); }
@Test public void testRecordSimple() { doTest("records/TestRecordSimple"); }
@Test public void testRecordVararg() { doTest("records/TestRecordVararg"); }
@Test public void testRecordGenericVararg() { doTest("records/TestRecordGenericVararg"); }
@Test public void testRecordAnno() { doTest("records/TestRecordAnno"); }
private void doTest(String testFile, String... companionFiles) { private void doTest(String testFile, String... companionFiles) {
ConsoleDecompiler decompiler = fixture.getDecompiler(); ConsoleDecompiler decompiler = fixture.getDecompiler();

@ -0,0 +1,66 @@
package records;
public record TestRecordAnno(@RC @TA int x, int y) {
public TestRecordAnno(@TA int x, @P int y) {
this.x = x;
this.y = y;
}
public final String toString() {
return this.toString<invokedynamic>(this);
}
public final int hashCode() {
return this.hashCode<invokedynamic>(this);
}
public final boolean equals(Object o) {
return this.equals<invokedynamic>(this, o);
}
@TA
public int x() {
return this.x;
}
@M
public int y() {
return this.y;// 5
}
}
class 'records/TestRecordAnno' {
method '<init> (II)V' {
6 4
b 5
e 6
}
method 'toString ()Ljava/lang/String;' {
1 9
6 9
}
method 'hashCode ()I' {
1 13
6 13
}
method 'equals (Ljava/lang/Object;)Z' {
2 17
7 17
}
method 'x ()I' {
1 22
4 22
}
method 'y ()I' {
1 27
4 27
}
}
Lines mapping:
5 <-> 28

@ -0,0 +1,35 @@
package records;
public record TestRecordEmpty() {
public final String toString() {
return this.toString<invokedynamic>(this);
}
public final int hashCode() {
return this.hashCode<invokedynamic>(this);
}
public final boolean equals(Object o) {
return this.equals<invokedynamic>(this, o);// 3
}
}
class 'records/TestRecordEmpty' {
method 'toString ()Ljava/lang/String;' {
1 4
6 4
}
method 'hashCode ()I' {
1 8
6 8
}
method 'equals (Ljava/lang/Object;)Z' {
2 12
7 12
}
}
Lines mapping:
3 <-> 13

@ -0,0 +1,66 @@
package records;
public record TestRecordGenericVararg<T>(T first, T... other) {
@SafeVarargs
public TestRecordGenericVararg(T first, T... other) {
this.first = first;// 5
this.other = other;
}
public final String toString() {
return this.toString<invokedynamic>(this);
}
public final int hashCode() {
return this.hashCode<invokedynamic>(this);
}
public final boolean equals(Object o) {
return this.equals<invokedynamic>(this, o);
}
public T first() {
return this.first;
}
public T[] other() {
return this.other;// 3
}
}
class 'records/TestRecordGenericVararg' {
method '<init> (Ljava/lang/Object;[Ljava/lang/Object;)V' {
6 5
b 6
e 7
}
method 'toString ()Ljava/lang/String;' {
1 10
6 10
}
method 'hashCode ()I' {
1 14
6 14
}
method 'equals (Ljava/lang/Object;)Z' {
2 18
7 18
}
method 'first ()Ljava/lang/Object;' {
1 22
4 22
}
method 'other ()[Ljava/lang/Object;' {
1 26
4 26
}
}
Lines mapping:
3 <-> 27
5 <-> 6

@ -0,0 +1,64 @@
package records;
public record TestRecordSimple(int x, int y) {
public TestRecordSimple(int x, int y) {
this.x = x;
this.y = y;
}
public final String toString() {
return this.toString<invokedynamic>(this);
}
public final int hashCode() {
return this.hashCode<invokedynamic>(this);
}
public final boolean equals(Object o) {
return this.equals<invokedynamic>(this, o);
}
public int x() {
return this.x;
}
public int y() {
return this.y;// 3
}
}
class 'records/TestRecordSimple' {
method '<init> (II)V' {
6 4
b 5
e 6
}
method 'toString ()Ljava/lang/String;' {
1 9
6 9
}
method 'hashCode ()I' {
1 13
6 13
}
method 'equals (Ljava/lang/Object;)Z' {
2 17
7 17
}
method 'x ()I' {
1 21
4 21
}
method 'y ()I' {
1 25
4 25
}
}
Lines mapping:
3 <-> 26

@ -0,0 +1,64 @@
package records;
public record TestRecordVararg(int x, int[]... y) {
public TestRecordVararg(int x, int[]... y) {
this.x = x;
this.y = y;
}
public final String toString() {
return this.toString<invokedynamic>(this);
}
public final int hashCode() {
return this.hashCode<invokedynamic>(this);
}
public final boolean equals(Object o) {
return this.equals<invokedynamic>(this, o);
}
public int x() {
return this.x;
}
public int[][] y() {
return this.y;// 3
}
}
class 'records/TestRecordVararg' {
method '<init> (I[[I)V' {
6 4
b 5
e 6
}
method 'toString ()Ljava/lang/String;' {
1 9
6 9
}
method 'hashCode ()I' {
1 13
6 13
}
method 'equals (Ljava/lang/Object;)Z' {
2 17
7 17
}
method 'x ()I' {
1 21
4 21
}
method 'y ()[[I' {
1 25
4 25
}
}
Lines mapping:
3 <-> 26

@ -0,0 +1,17 @@
package records;
import java.lang.annotation.*;
public record TestRecordAnno(@TA @RC int x, @M @P int y) {}
@Target(ElementType.TYPE_USE)
@interface TA {}
@Target(ElementType.RECORD_COMPONENT)
@interface RC {}
@Target(ElementType.METHOD)
@interface M {}
@Target(ElementType.PARAMETER)
@interface P {}

@ -0,0 +1,3 @@
package records;
public record TestRecordEmpty() {}

@ -0,0 +1,6 @@
package records;
public record TestRecordGenericVararg<T>(T first, T... other) {
@SafeVarargs
public TestRecordGenericVararg {}
}

@ -0,0 +1,3 @@
package records;
public record TestRecordSimple(int x, int y) {}

@ -0,0 +1,3 @@
package records;
public record TestRecordVararg(int x, int[]... y) {}
Loading…
Cancel
Save