Skip to content

Commit 37737b0

Browse files
authored
Compact canonical constructor support (#484)
* Add support for compact constructors * Support private constructors * Cleanup * comment * Rename method
1 parent 9e7e264 commit 37737b0

File tree

5 files changed

+163
-130
lines changed

5 files changed

+163
-130
lines changed

src/org/jetbrains/java/decompiler/main/ClassWriter.java

Lines changed: 122 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,104 +1199,10 @@ public boolean writeMethod(ClassNode node, StructMethod mt, int methodIndex, Tex
11991199
}
12001200

12011201
buffer.appendMethod(toValidJavaIdentifier(name), true, cl.qualifiedName, mt.getName(), md);
1202-
buffer.append('(');
12031202

1204-
List<VarVersionPair> mask = methodWrapper.synthParameters;
1205-
1206-
int lastVisibleParameterIndex = -1;
1207-
for (int i = 0; i < md.params.length; i++) {
1208-
if (mask == null || mask.get(i) == null) {
1209-
lastVisibleParameterIndex = i;
1210-
}
1211-
}
1212-
if (lastVisibleParameterIndex != -1) {
1213-
buffer.pushNewlineGroup(indent, 1);
1214-
buffer.appendPossibleNewline();
1215-
}
1216-
1217-
List<StructMethodParametersAttribute.Entry> methodParameters = null;
1218-
if (DecompilerContext.getOption(IFernflowerPreferences.USE_METHOD_PARAMETERS)) {
1219-
StructMethodParametersAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_METHOD_PARAMETERS);
1220-
if (attr != null) {
1221-
methodParameters = attr.getEntries();
1222-
}
1223-
}
1224-
1225-
int index = isEnum && init ? 3 : thisVar ? 1 : 0;
1226-
int start = isEnum && init ? 2 : 0;
1227-
boolean hasDescriptor = descriptor != null;
1228-
//mask should now have the Outer.this in it... so this *shouldn't* be nessasary.
1229-
//if (init && !isEnum && ((node.access & CodeConstants.ACC_STATIC) == 0) && node.type == ClassNode.CLASS_MEMBER)
1230-
// index++;
1231-
1232-
buffer.pushNewlineGroup(indent, 0);
1233-
for (int i = start; i < md.params.length; i++) {
1234-
boolean real = mask == null || mask.get(i) == null;
1235-
VarType parameterType = real && hasDescriptor && paramCount < descriptor.parameterTypes.size() ? descriptor.parameterTypes.get(paramCount) : md.params[i];
1236-
if (real) {
1237-
if (paramCount > 0) {
1238-
buffer.append(",");
1239-
buffer.appendPossibleNewline(" ");
1240-
}
1241-
1242-
appendParameterAnnotations(buffer, mt, paramCount);
1243-
1244-
if (methodParameters != null && i < methodParameters.size()) {
1245-
appendModifiers(buffer, methodParameters.get(i).myAccessFlags, CodeConstants.ACC_FINAL, isInterface, 0);
1246-
}
1247-
else if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarTypeProcessor.FinalType.EXPLICIT_FINAL) {
1248-
buffer.append("final ");
1249-
}
1250-
1251-
String typeName;
1252-
boolean isVarArg = i == lastVisibleParameterIndex && mt.hasModifier(CodeConstants.ACC_VARARGS) && parameterType.arrayDim > 0;
1253-
if (isVarArg) {
1254-
parameterType = parameterType.decreaseArrayDim();
1255-
}
1256-
typeName = ExprProcessor.getCastTypeName(parameterType);
1257-
1258-
if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
1259-
DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
1260-
typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
1261-
}
1262-
buffer.appendCastTypeName(typeName, parameterType);
1263-
if (isVarArg) {
1264-
buffer.append("...");
1265-
}
1266-
1267-
buffer.append(' ');
1268-
1269-
String parameterName;
1270-
String clashingName = methodWrapper.varproc.getClashingName(new VarVersionPair(index, 0));
1271-
if (clashingName != null) {
1272-
parameterName = clashingName;
1273-
} else if (methodParameters != null && i < methodParameters.size() && methodParameters.get(i).myName != null) {
1274-
parameterName = methodParameters.get(i).myName;
1275-
} else {
1276-
parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
1277-
}
1278-
1279-
String newParameterName = methodWrapper.methodStruct.getVariableNamer().renameParameter(flags, parameterType, parameterName, index);
1280-
if ((flags & (CodeConstants.ACC_ABSTRACT | CodeConstants.ACC_NATIVE)) != 0 && Objects.equals(newParameterName, parameterName)) {
1281-
newParameterName = DecompilerContext.getStructContext().renameAbstractParameter(methodWrapper.methodStruct.getClassQualifiedName(), mt.getName(), mt.getDescriptor(), index - (((flags & CodeConstants.ACC_STATIC) == 0) ? 1 : 0), parameterName);
1282-
}
1283-
parameterName = newParameterName;
1284-
1285-
buffer.appendVariable(parameterName == null ? "param" + index : parameterName, // null iff decompiled with errors
1286-
true, true, cl.qualifiedName, mt.getName(), md, index, parameterName);
1287-
1288-
paramCount++;
1289-
}
1290-
1291-
index += parameterType.stackSize;
1292-
}
1293-
buffer.popNewlineGroup();
1294-
1295-
if (lastVisibleParameterIndex != -1) {
1296-
buffer.appendPossibleNewline("", true);
1297-
buffer.popNewlineGroup();
1203+
if (!methodWrapper.isCompactRecordConstructor) {
1204+
paramCount = writeMethodParameterHeader(mt, buffer, indent, methodWrapper, md, isEnum, init, thisVar, descriptor, paramCount, isInterface, flags, cl);
12981205
}
1299-
buffer.append(')');
13001206

13011207
StructExceptionsAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_EXCEPTIONS);
13021208
if ((descriptor != null && !descriptor.exceptionTypes.isEmpty()) || attr != null) {
@@ -1344,7 +1250,7 @@ else if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarT
13441250
} else {
13451251
TextBuffer code = root.toJava(indent + 1);
13461252
code.addBytecodeMapping(root.getDummyExit().bytecode);
1347-
hideMethod = code.length() == 0 && (clInit || dInit || hideConstructor(node, init, throwsExceptions, paramCount, flags));
1253+
hideMethod = code.length() == 0 && (clInit || dInit || hideConstructor(node, init, throwsExceptions, paramCount, flags, mt));
13481254
buffer.append(code, cl.qualifiedName, InterpreterUtil.makeUniqueKey(mt.getName(), mt.getDescriptor()));
13491255
}
13501256
}
@@ -1375,6 +1281,108 @@ else if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarT
13751281
return !hideMethod;
13761282
}
13771283

1284+
private static int writeMethodParameterHeader(StructMethod mt, TextBuffer buffer, int indent, MethodWrapper methodWrapper, MethodDescriptor md, boolean isEnum, boolean init, boolean thisVar, GenericMethodDescriptor descriptor, int paramCount, boolean isInterface, int flags, StructClass cl) {
1285+
buffer.append('(');
1286+
1287+
List<VarVersionPair> mask = methodWrapper.synthParameters;
1288+
1289+
int lastVisibleParameterIndex = -1;
1290+
for (int i = 0; i < md.params.length; i++) {
1291+
if (mask == null || mask.get(i) == null) {
1292+
lastVisibleParameterIndex = i;
1293+
}
1294+
}
1295+
if (lastVisibleParameterIndex != -1) {
1296+
buffer.pushNewlineGroup(indent, 1);
1297+
buffer.appendPossibleNewline();
1298+
}
1299+
1300+
List<StructMethodParametersAttribute.Entry> methodParameters = null;
1301+
if (DecompilerContext.getOption(IFernflowerPreferences.USE_METHOD_PARAMETERS)) {
1302+
StructMethodParametersAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_METHOD_PARAMETERS);
1303+
if (attr != null) {
1304+
methodParameters = attr.getEntries();
1305+
}
1306+
}
1307+
1308+
int index = isEnum && init ? 3 : thisVar ? 1 : 0;
1309+
int start = isEnum && init ? 2 : 0;
1310+
boolean hasDescriptor = descriptor != null;
1311+
//mask should now have the Outer.this in it... so this *shouldn't* be nessasary.
1312+
//if (init && !isEnum && ((node.access & CodeConstants.ACC_STATIC) == 0) && node.type == ClassNode.CLASS_MEMBER)
1313+
// index++;
1314+
1315+
buffer.pushNewlineGroup(indent, 0);
1316+
for (int i = start; i < md.params.length; i++) {
1317+
boolean real = mask == null || mask.get(i) == null;
1318+
VarType parameterType = real && hasDescriptor && paramCount < descriptor.parameterTypes.size() ? descriptor.parameterTypes.get(paramCount) : md.params[i];
1319+
if (real) {
1320+
if (paramCount > 0) {
1321+
buffer.append(",");
1322+
buffer.appendPossibleNewline(" ");
1323+
}
1324+
1325+
appendParameterAnnotations(buffer, mt, paramCount);
1326+
1327+
if (methodParameters != null && i < methodParameters.size()) {
1328+
appendModifiers(buffer, methodParameters.get(i).myAccessFlags, CodeConstants.ACC_FINAL, isInterface, 0);
1329+
}
1330+
else if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarTypeProcessor.FinalType.EXPLICIT_FINAL) {
1331+
buffer.append("final ");
1332+
}
1333+
1334+
String typeName;
1335+
boolean isVarArg = i == lastVisibleParameterIndex && mt.hasModifier(CodeConstants.ACC_VARARGS) && parameterType.arrayDim > 0;
1336+
if (isVarArg) {
1337+
parameterType = parameterType.decreaseArrayDim();
1338+
}
1339+
typeName = ExprProcessor.getCastTypeName(parameterType);
1340+
1341+
if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
1342+
DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
1343+
typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
1344+
}
1345+
buffer.appendCastTypeName(typeName, parameterType);
1346+
if (isVarArg) {
1347+
buffer.append("...");
1348+
}
1349+
1350+
buffer.append(' ');
1351+
1352+
String parameterName;
1353+
String clashingName = methodWrapper.varproc.getClashingName(new VarVersionPair(index, 0));
1354+
if (clashingName != null) {
1355+
parameterName = clashingName;
1356+
} else if (methodParameters != null && i < methodParameters.size() && methodParameters.get(i).myName != null) {
1357+
parameterName = methodParameters.get(i).myName;
1358+
} else {
1359+
parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
1360+
}
1361+
1362+
String newParameterName = methodWrapper.methodStruct.getVariableNamer().renameParameter(flags, parameterType, parameterName, index);
1363+
if ((flags & (CodeConstants.ACC_ABSTRACT | CodeConstants.ACC_NATIVE)) != 0 && Objects.equals(newParameterName, parameterName)) {
1364+
newParameterName = DecompilerContext.getStructContext().renameAbstractParameter(methodWrapper.methodStruct.getClassQualifiedName(), mt.getName(), mt.getDescriptor(), index - (((flags & CodeConstants.ACC_STATIC) == 0) ? 1 : 0), parameterName);
1365+
}
1366+
parameterName = newParameterName;
1367+
1368+
buffer.appendVariable(parameterName == null ? "param" + index : parameterName, // null iff decompiled with errors
1369+
true, true, cl.qualifiedName, mt.getName(), md, index, parameterName);
1370+
1371+
paramCount++;
1372+
}
1373+
1374+
index += parameterType.stackSize;
1375+
}
1376+
buffer.popNewlineGroup();
1377+
1378+
if (lastVisibleParameterIndex != -1) {
1379+
buffer.appendPossibleNewline("", true);
1380+
buffer.popNewlineGroup();
1381+
}
1382+
buffer.append(')');
1383+
return paramCount;
1384+
}
1385+
13781386
private static void dumpError(TextBuffer buffer, MethodWrapper wrapper, int indent) {
13791387
List<String> lines = new ArrayList<>();
13801388
lines.add("$VF: Couldn't be decompiled");
@@ -1561,7 +1569,7 @@ private static void appendConstant(StringBuilder sb, PooledConstant constant) {
15611569
}
15621570
}
15631571

1564-
private static boolean hideConstructor(ClassNode node, boolean init, boolean throwsExceptions, int paramCount, int methodAccessFlags) {
1572+
private static boolean hideConstructor(ClassNode node, boolean init, boolean throwsExceptions, int paramCount, int methodAccessFlags, StructMethod structMethod) {
15651573
if (!init || throwsExceptions || paramCount > 0 || !DecompilerContext.getOption(IFernflowerPreferences.HIDE_DEFAULT_CONSTRUCTOR)) {
15661574
return false;
15671575
}
@@ -1571,17 +1579,27 @@ private static boolean hideConstructor(ClassNode node, boolean init, boolean thr
15711579

15721580
int classAccessFlags = node.type == ClassNode.Type.ROOT ? cl.getAccessFlags() : node.access;
15731581
boolean isEnum = cl.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
1582+
boolean isRecord = cl.getRecordComponents() != null;
15741583

15751584
// default constructor requires same accessibility flags. Exception: enum constructor which is always private
1576-
if(!isEnum && ((classAccessFlags & ACCESSIBILITY_FLAGS) != (methodAccessFlags & ACCESSIBILITY_FLAGS))) {
1585+
// Another exception: record classes can sometimes be generated with a private constructor
1586+
if(!isEnum && !isRecord && ((classAccessFlags & ACCESSIBILITY_FLAGS) != (methodAccessFlags & ACCESSIBILITY_FLAGS))) {
15771587
return false;
15781588
}
15791589

1580-
int count = 0;
1581-
for (StructMethod mt : cl.getMethods()) {
1582-
if (CodeConstants.INIT_NAME.equals(mt.getName())) {
1583-
if (++count > 1) {
1584-
return false;
1590+
// Constructor with an annotation, we dont want to hide this.
1591+
if (isRecord && RecordHelper.hasAnnotations(structMethod)) {
1592+
return false;
1593+
}
1594+
1595+
// We should not run this check in records
1596+
if (!isRecord) {
1597+
int count = 0;
1598+
for (StructMethod mt : cl.getMethods()) {
1599+
if (CodeConstants.INIT_NAME.equals(mt.getName())) {
1600+
if (++count > 1) {
1601+
return false;
1602+
}
15851603
}
15861604
}
15871605
}

src/org/jetbrains/java/decompiler/main/RecordHelper.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ private static void recordComponentToJava(TextBuffer buffer, StructClass cl, Str
214214
buffer.appendField(cd.getName(), true, cl.qualifiedName, cd.getName(), cd.getDescriptor());
215215
}
216216

217-
private static boolean hasAnnotations(StructMethod mt) {
217+
public static boolean hasAnnotations(StructMethod mt) {
218218
return mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_RUNTIME_INVISIBLE_ANNOTATIONS) != null ||
219219
mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_RUNTIME_VISIBLE_ANNOTATIONS) != null;
220220
}
@@ -241,5 +241,36 @@ public static void fixupCanonicalConstructor(MethodWrapper mw, StructClass cl) {
241241

242242
varidx += md.params[i].stackSize;
243243
}
244+
245+
// Prune all field assignments from the canonical constructor
246+
if (isCompactCanonicalConstructor(mw)) {
247+
mw.getOrBuildGraph().iterateExprents(exprent -> {
248+
if (exprent instanceof AssignmentExprent assignmentExprent && assignmentExprent.getLeft() instanceof FieldExprent) {
249+
return 2;
250+
}
251+
252+
return 0;
253+
});
254+
mw.isCompactRecordConstructor = true;
255+
}
244256
}
257+
258+
// Ideally this is iterated backwards.
259+
// However, what we do is check that the last exprents are field invocations to local variables.
260+
// (And that the name of the lvt matches the field)
261+
private static boolean isCompactCanonicalConstructor(MethodWrapper mw) {
262+
DirectGraph graph = mw.getOrBuildGraph();
263+
boolean[] valid = new boolean[1];
264+
graph.iterateExprents(exprent -> {
265+
if (exprent instanceof AssignmentExprent assignmentExprent && assignmentExprent.getLeft() instanceof FieldExprent fieldExprent && assignmentExprent.getRight() instanceof VarExprent varExprent) {
266+
valid[0] = varExprent.getLVT() != null && fieldExprent.getName().equals(varExprent.getName());
267+
} else {
268+
valid[0] = false;
269+
}
270+
271+
return 0;
272+
});
273+
return valid[0];
274+
}
275+
245276
}

src/org/jetbrains/java/decompiler/main/rels/MethodWrapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class MethodWrapper {
2626
public Throwable decompileError;
2727
public Set<String> commentLines = null;
2828
public boolean addErrorComment = false;
29+
public boolean isCompactRecordConstructor = false;
2930

3031
public MethodWrapper(RootStatement root, VarProcessor varproc, StructMethod methodStruct, StructClass classStruct, CounterContainer counter) {
3132
this.root = root;
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package pkg;
22

33
public record TestRecordEmptyConstructor(int val) {
4-
public TestRecordEmptyConstructor(int val) {
4+
public TestRecordEmptyConstructor {
55
System.out.println(val);// 5
6-
this.val = val;// 4
76
}// 6
87
}
98

@@ -16,16 +15,12 @@ class 'pkg/TestRecordEmptyConstructor' {
1615
8 4
1716
9 4
1817
a 4
19-
b 5
20-
c 5
21-
d 5
22-
e 5
23-
f 5
24-
10 6
18+
10 5
2519
}
2620
}
2721

2822
Lines mapping:
29-
4 <-> 6
3023
5 <-> 5
31-
6 <-> 7
24+
6 <-> 6
25+
Not mapped:
26+
4

testData/results/pkg/TestRecordGenericVararg.dec

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,13 @@ package pkg;
22

33
public record TestRecordGenericVararg<T>(T first, T... other) {
44
@SafeVarargs
5-
public TestRecordGenericVararg(T first, T... other) {
6-
this.first = first;// 5
7-
this.other = other;
8-
}
5+
public TestRecordGenericVararg {
6+
}// 5
97
}
108

119
class 'pkg/TestRecordGenericVararg' {
1210
method '<init> (Ljava/lang/Object;[Ljava/lang/Object;)V' {
13-
4 5
14-
5 5
15-
6 5
16-
7 5
17-
8 5
18-
9 6
19-
a 6
20-
b 6
21-
c 6
22-
d 6
23-
e 7
11+
e 5
2412
}
2513
}
2614

0 commit comments

Comments
 (0)