diff --git a/src/org/jetbrains/java/decompiler/main/ClassWriter.java b/src/org/jetbrains/java/decompiler/main/ClassWriter.java index 72fefe4..bb5790b 100644 --- a/src/org/jetbrains/java/decompiler/main/ClassWriter.java +++ b/src/org/jetbrains/java/decompiler/main/ClassWriter.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.VarVersionPair; import org.jetbrains.java.decompiler.modules.renamer.PoolInterceptor; -import org.jetbrains.java.decompiler.struct.StructClass; -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.*; import org.jetbrains.java.decompiler.struct.attr.*; import org.jetbrains.java.decompiler.struct.consts.PrimitiveConstant; 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 java.util.*; +import java.util.stream.Collectors; public class ClassWriter { private final PoolInterceptor interceptor; @@ -162,11 +160,19 @@ public class ClassWriter { dummy_tracer.incrementCurrentSourceLine(buffer.countLines(start_class_def)); + List components = cl.getRecordComponents(); + for (StructField fd : cl.getFields()) { boolean hide = fd.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) || wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor())); 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); if (isEnum) { if (enumFields) { @@ -302,6 +308,13 @@ public class ClassWriter { flags &= ~CodeConstants.ACC_FINAL; } + List components = cl.getRecordComponents(); + + if (components != null) { + // records are implicitly final + flags &= ~CodeConstants.ACC_FINAL; + } + appendModifiers(buffer, flags, CLASS_ALLOWED, isInterface, CLASS_EXCLUDED); if (isEnum) { @@ -313,6 +326,9 @@ public class ClassWriter { } buffer.append("interface "); } + else if (components != null) { + buffer.append("record "); + } else { buffer.append("class "); } @@ -324,9 +340,22 @@ public class ClassWriter { 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(' '); - if (!isEnum && !isInterface && cl.superClass != null) { + if (!isEnum && !isInterface && components == null && cl.superClass != null) { VarType supertype = new VarType(cl.superClass.getString(), true); if (!VarType.VARTYPE_OBJECT.equals(supertype)) { buffer.append("extends "); @@ -362,6 +391,13 @@ public class ClassWriter { 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) { int start = buffer.length(); 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, ClassWrapper classWrapper, StructMethod mt, @@ -959,7 +1022,13 @@ public class ClassWriter { for (AnnotationExprent annotation : attribute.getAnnotations()) { String text = annotation.toJava(indent, BytecodeMappingTracer.DUMMY).toString(); filter.add(text); - buffer.append(text).appendLineSeparator(); + buffer.append(text); + if (indent < 0) { + buffer.append(' '); + } + else { + buffer.appendLineSeparator(); + } } } } diff --git a/src/org/jetbrains/java/decompiler/struct/StructClass.java b/src/org/jetbrains/java/decompiler/struct/StructClass.java index 0bb370c..7a46954 100644 --- a/src/org/jetbrains/java/decompiler/struct/StructClass.java +++ b/src/org/jetbrains/java/decompiler/struct/StructClass.java @@ -2,6 +2,8 @@ package org.jetbrains.java.decompiler.struct; 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.PrimitiveConstant; 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 java.io.IOException; +import java.util.List; /* class_file { @@ -132,6 +135,15 @@ public class StructClass extends StructMember { return pool; } + /** + * @return list of record components; null if this class is not a record + */ + public List getRecordComponents() { + StructRecordAttribute recordAttr = getAttribute(StructGeneralAttribute.ATTRIBUTE_RECORD); + if (recordAttr == null) return null; + return recordAttr.getComponents(); + } + public int[] getInterfaces() { return interfaces; } diff --git a/src/org/jetbrains/java/decompiler/struct/StructRecordComponent.java b/src/org/jetbrains/java/decompiler/struct/StructRecordComponent.java new file mode 100644 index 0000000..830078c --- /dev/null +++ b/src/org/jetbrains/java/decompiler/struct/StructRecordComponent.java @@ -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; + } +} diff --git a/src/org/jetbrains/java/decompiler/struct/attr/StructGeneralAttribute.java b/src/org/jetbrains/java/decompiler/struct/attr/StructGeneralAttribute.java index f421671..8fff5db 100644 --- a/src/org/jetbrains/java/decompiler/struct/attr/StructGeneralAttribute.java +++ b/src/org/jetbrains/java/decompiler/struct/attr/StructGeneralAttribute.java @@ -34,6 +34,7 @@ public class StructGeneralAttribute { public static final Key ATTRIBUTE_DEPRECATED = new Key<>("Deprecated"); public static final Key ATTRIBUTE_LINE_NUMBER_TABLE = new Key<>("LineNumberTable"); public static final Key ATTRIBUTE_METHOD_PARAMETERS = new Key<>("MethodParameters"); + public static final Key ATTRIBUTE_RECORD = new Key<>("Record"); public static class Key { private final String name; @@ -97,6 +98,9 @@ public class StructGeneralAttribute { else if (ATTRIBUTE_METHOD_PARAMETERS.getName().equals(name)) { attr = new StructMethodParametersAttribute(); } + else if (ATTRIBUTE_RECORD.getName().equals(name)) { + attr = new StructRecordAttribute(); + } else { // unsupported attribute return null; diff --git a/src/org/jetbrains/java/decompiler/struct/attr/StructRecordAttribute.java b/src/org/jetbrains/java/decompiler/struct/attr/StructRecordAttribute.java new file mode 100644 index 0000000..fdbb271 --- /dev/null +++ b/src/org/jetbrains/java/decompiler/struct/attr/StructRecordAttribute.java @@ -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 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 getComponents() { + return Collections.unmodifiableList(components); + } +} diff --git a/test/org/jetbrains/java/decompiler/SingleClassesTest.java b/test/org/jetbrains/java/decompiler/SingleClassesTest.java index 2f2e5e3..c720949 100644 --- a/test/org/jetbrains/java/decompiler/SingleClassesTest.java +++ b/test/org/jetbrains/java/decompiler/SingleClassesTest.java @@ -129,6 +129,11 @@ public class SingleClassesTest { @Test public void testSuspendLambda() { doTest("pkg/TestSuspendLambdaKt"); } @Test public void testNamedSuspendFun2Kt() { doTest("pkg/TestNamedSuspendFun2Kt"); } @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) { ConsoleDecompiler decompiler = fixture.getDecompiler(); diff --git a/testData/classes/records/TestRecordAnno.class b/testData/classes/records/TestRecordAnno.class new file mode 100644 index 0000000..6751bc0 Binary files /dev/null and b/testData/classes/records/TestRecordAnno.class differ diff --git a/testData/classes/records/TestRecordEmpty.class b/testData/classes/records/TestRecordEmpty.class new file mode 100644 index 0000000..b4b6080 Binary files /dev/null and b/testData/classes/records/TestRecordEmpty.class differ diff --git a/testData/classes/records/TestRecordGenericVararg.class b/testData/classes/records/TestRecordGenericVararg.class new file mode 100644 index 0000000..52bb9c6 Binary files /dev/null and b/testData/classes/records/TestRecordGenericVararg.class differ diff --git a/testData/classes/records/TestRecordSimple.class b/testData/classes/records/TestRecordSimple.class new file mode 100644 index 0000000..91c748e Binary files /dev/null and b/testData/classes/records/TestRecordSimple.class differ diff --git a/testData/classes/records/TestRecordVararg.class b/testData/classes/records/TestRecordVararg.class new file mode 100644 index 0000000..a720d7f Binary files /dev/null and b/testData/classes/records/TestRecordVararg.class differ diff --git a/testData/results/TestRecordAnno.dec b/testData/results/TestRecordAnno.dec new file mode 100644 index 0000000..1df3349 --- /dev/null +++ b/testData/results/TestRecordAnno.dec @@ -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(this); + } + + public final int hashCode() { + return this.hashCode(this); + } + + public final boolean equals(Object o) { + return this.equals(this, o); + } + + @TA + public int x() { + return this.x; + } + + @M + public int y() { + return this.y;// 5 + } +} + +class 'records/TestRecordAnno' { + method ' (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 diff --git a/testData/results/TestRecordEmpty.dec b/testData/results/TestRecordEmpty.dec new file mode 100644 index 0000000..6111ddf --- /dev/null +++ b/testData/results/TestRecordEmpty.dec @@ -0,0 +1,35 @@ +package records; + +public record TestRecordEmpty() { + public final String toString() { + return this.toString(this); + } + + public final int hashCode() { + return this.hashCode(this); + } + + public final boolean equals(Object o) { + return this.equals(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 diff --git a/testData/results/TestRecordGenericVararg.dec b/testData/results/TestRecordGenericVararg.dec new file mode 100644 index 0000000..1a52a25 --- /dev/null +++ b/testData/results/TestRecordGenericVararg.dec @@ -0,0 +1,66 @@ +package records; + +public record TestRecordGenericVararg(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(this); + } + + public final int hashCode() { + return this.hashCode(this); + } + + public final boolean equals(Object o) { + return this.equals(this, o); + } + + public T first() { + return this.first; + } + + public T[] other() { + return this.other;// 3 + } +} + +class 'records/TestRecordGenericVararg' { + method ' (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 diff --git a/testData/results/TestRecordSimple.dec b/testData/results/TestRecordSimple.dec new file mode 100644 index 0000000..fd1bf45 --- /dev/null +++ b/testData/results/TestRecordSimple.dec @@ -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(this); + } + + public final int hashCode() { + return this.hashCode(this); + } + + public final boolean equals(Object o) { + return this.equals(this, o); + } + + public int x() { + return this.x; + } + + public int y() { + return this.y;// 3 + } +} + +class 'records/TestRecordSimple' { + method ' (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 diff --git a/testData/results/TestRecordVararg.dec b/testData/results/TestRecordVararg.dec new file mode 100644 index 0000000..d0f4b66 --- /dev/null +++ b/testData/results/TestRecordVararg.dec @@ -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(this); + } + + public final int hashCode() { + return this.hashCode(this); + } + + public final boolean equals(Object o) { + return this.equals(this, o); + } + + public int x() { + return this.x; + } + + public int[][] y() { + return this.y;// 3 + } +} + +class 'records/TestRecordVararg' { + method ' (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 diff --git a/testData/src/records/TestRecordAnno.java b/testData/src/records/TestRecordAnno.java new file mode 100644 index 0000000..c61a5b5 --- /dev/null +++ b/testData/src/records/TestRecordAnno.java @@ -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 {} diff --git a/testData/src/records/TestRecordEmpty.java b/testData/src/records/TestRecordEmpty.java new file mode 100644 index 0000000..321165a --- /dev/null +++ b/testData/src/records/TestRecordEmpty.java @@ -0,0 +1,3 @@ +package records; + +public record TestRecordEmpty() {} \ No newline at end of file diff --git a/testData/src/records/TestRecordGenericVararg.java b/testData/src/records/TestRecordGenericVararg.java new file mode 100644 index 0000000..44e8fdc --- /dev/null +++ b/testData/src/records/TestRecordGenericVararg.java @@ -0,0 +1,6 @@ +package records; + +public record TestRecordGenericVararg(T first, T... other) { + @SafeVarargs + public TestRecordGenericVararg {} +} \ No newline at end of file diff --git a/testData/src/records/TestRecordSimple.java b/testData/src/records/TestRecordSimple.java new file mode 100644 index 0000000..3bc1e1a --- /dev/null +++ b/testData/src/records/TestRecordSimple.java @@ -0,0 +1,3 @@ +package records; + +public record TestRecordSimple(int x, int y) {} \ No newline at end of file diff --git a/testData/src/records/TestRecordVararg.java b/testData/src/records/TestRecordVararg.java new file mode 100644 index 0000000..8d23ed0 --- /dev/null +++ b/testData/src/records/TestRecordVararg.java @@ -0,0 +1,3 @@ +package records; + +public record TestRecordVararg(int x, int[]... y) {} \ No newline at end of file