diff --git a/core/src/main/java/org/jboss/jandex/Indexer.java b/core/src/main/java/org/jboss/jandex/Indexer.java index 4d4bb084..47e66a03 100644 --- a/core/src/main/java/org/jboss/jandex/Indexer.java +++ b/core/src/main/java/org/jboss/jandex/Indexer.java @@ -396,8 +396,7 @@ void returnConstantAnnoAttributes(byte[] attributes) { private List methods; private List fields; private List recordComponents; - private byte[][] debugParameterNames; - private byte[][] methodParameterNames; + private IdentityHashMap methodParams; private List modulePackages; private DotName moduleMainClass; @@ -453,6 +452,8 @@ private void initClassFields() { // in bytecode, record components are stored as class attributes, // and if the attribute is missing, processRecordComponents isn't called at all recordComponents = new ArrayList(); + + methodParams = new IdentityHashMap<>(); } private void processMethodInfo(DataInputStream data) throws IOException { @@ -475,16 +476,17 @@ private void processMethodInfo(DataInputStream data) throws IOException { if (parameters.length == 0 && Arrays.equals(Utils.INIT_METHOD_NAME, name)) { currentClass.setHasNoArgsConstructor(true); } - methodParameterNames = debugParameterNames = null; + MethodParamList parameterList = new MethodParamList(method); + methodParams.put(method, parameterList); processAttributes(data, method); method.setAnnotations(elementAnnotations); elementAnnotations.clear(); + parameterList.finish(); - // Prefer method parameter names over debug info - if (methodParameterNames != null) - method.methodInternal().setParameterNames(methodParameterNames); - else if (debugParameterNames != null) - method.methodInternal().setParameterNames(debugParameterNames); + byte[][] parameterNames = parameterList.getNames(); + if (parameterNames != null) { + method.methodInternal().setParameterNames(parameterNames); + } methods.add(method); } @@ -782,30 +784,20 @@ private void processMethodParameters(DataInputStream data, MethodInfo target) th // the one we just read (for extra safety, do NOT do this for compliant methods) numParameters = target.parametersCount(); } - byte[][] parameterNames = numParameters > 0 ? new byte[numParameters][] : MethodInternal.EMPTY_PARAMETER_NAMES; - int filledParameters = 0; for (int i = 0; i < numParameters; i++) { int nameIndex = data.readUnsignedShort(); byte[] parameterName = nameIndex == 0 ? null : decodeUtf8EntryAsBytes(nameIndex); int flags = data.readUnsignedShort(); - // skip synthetic/mandated params to get the same param name index as annotations (which do not count them) - if ((flags & (Modifiers.SYNTHETIC | Modifiers.MANDATED)) != 0) { - continue; - } + boolean syntheticOrMandated = (flags & (Modifiers.SYNTHETIC | Modifiers.MANDATED)) != 0; - parameterNames[filledParameters++] = parameterName; + if (methodParams.containsKey(target)) { // should always be there, just being extra careful + methodParams.get(target).appendProper(parameterName, syntheticOrMandated); + } } - - byte[][] realParameterNames = filledParameters > 0 ? new byte[filledParameters][] - : MethodInternal.EMPTY_PARAMETER_NAMES; - if (filledParameters > 0) - System.arraycopy(parameterNames, 0, realParameterNames, 0, filledParameters); - this.methodParameterNames = realParameterNames; } private void processLocalVariableTable(DataInputStream data, MethodInfo target) throws IOException { int numVariables = data.readUnsignedShort(); - byte[][] variableNames = numVariables > 0 ? new byte[numVariables][] : MethodInternal.EMPTY_PARAMETER_NAMES; int numParameters = 0; for (int i = 0; i < numVariables; i++) { int startPc = data.readUnsignedShort(); @@ -815,32 +807,38 @@ private void processLocalVariableTable(DataInputStream data, MethodInfo target) int index = data.readUnsignedShort(); // parameters have startPc == 0 - if (startPc != 0) + if (startPc != 0) { continue; + } + byte[] parameterName = nameIndex == 0 ? null : decodeUtf8EntryAsBytes(nameIndex); + // ignore "this" if (numParameters == 0 && parameterName != null && parameterName.length == 4 && parameterName[0] == 0x74 && parameterName[1] == 0x68 && parameterName[2] == 0x69 - && parameterName[3] == 0x73) + && parameterName[3] == 0x73) { continue; - // ignore "this$*" that javac adds (not ECJ) + } + + // treat "this$*" that javac adds (not ecj) as synthetic (or mandated) + boolean synthetic = false; if (numParameters == 0 && parameterName != null && parameterName.length > 5 && parameterName[0] == 0x74 && parameterName[1] == 0x68 && parameterName[2] == 0x69 && parameterName[3] == 0x73 - && parameterName[4] == 0x24) - continue; + && parameterName[4] == 0x24) { + synthetic = true; + } + + if (methodParams.containsKey(target)) { // should always be there, just being extra careful + methodParams.get(target).appendDebug(parameterName, synthetic); + } - // here we rely on the parameters being in the right order - variableNames[numParameters++] = parameterName; + numParameters++; } - byte[][] parameterNames = numParameters > 0 ? new byte[numParameters][] : MethodInternal.EMPTY_PARAMETER_NAMES; - if (numParameters > 0) - System.arraycopy(variableNames, 0, parameterNames, 0, numParameters); - this.debugParameterNames = parameterNames; } private void processEnclosingMethod(DataInputStream data, ClassInfo target) throws IOException { @@ -984,9 +982,14 @@ private void adjustMethodParameters() { } alreadyProcessedMethods.put(method, null); - if (isInnerConstructor(method) && !signaturePresent.containsKey(method)) { - // inner class constructor without signature, list of parameter types is derived - // from the descriptor, which includes 1 mandated or synthetic parameter at the beginning + if (signaturePresent.containsKey(method)) { + // if the method has a signature, we derived the parameter list from it, + // and it never includes mandated or synthetic parameters + continue; + } + + if (isInnerConstructor(method)) { + // inner class constructor, descriptor includes 1 mandated or synthetic parameter at the beginning DotName enclosingClass = null; if (method.declaringClass().enclosingClass() != null) { enclosingClass = method.declaringClass().enclosingClass(); @@ -1000,10 +1003,8 @@ private void adjustMethodParameters() { System.arraycopy(parameterTypes, 1, newParameterTypes, 0, parameterTypes.length - 1); method.setParameters(intern(newParameterTypes)); } - } else if (isEnumConstructor(method) && !signaturePresent.containsKey(method)) { - // enum constructor without signature, list of parameter types is derived from the descriptor, - // which includes 2 synthetic parameters at the beginning -- let's remove those - // (javac typically emits the signature, while ecj does not) + } else if (isEnumConstructor(method)) { + // enum constructor, descriptor includes 2 synthetic parameters at the beginning Type[] parameterTypes = method.methodInternal().parameterTypesArray(); if (parameterTypes.length >= 2 && parameterTypes[0].kind() == Type.Kind.CLASS @@ -1023,6 +1024,48 @@ private void adjustMethodParameters() { } } + private boolean isInnerConstructor(MethodInfo method) { + if (!method.isConstructor()) { + return false; + } + + ClassInfo klass = method.declaringClass(); + + // there's no indication in the class file whether a local or anonymous class was declared in static context, + // so we use some heuristics here + if (klass.nestingType() == ClassInfo.NestingType.LOCAL + || klass.nestingType() == ClassInfo.NestingType.ANONYMOUS) { + + // synthetic or mandated parameter at the beginning of the parameter list is the enclosing instance, + // the constructor belongs to a "non-static" class + // + // this works for: + // - javac with `-g` or `-parameters` + // - javac since JDK 21 (see https://bugs.openjdk.org/browse/JDK-8292275) + // - ecj with `-parameters` (see also `processLocalVariableTable`) + MethodParamList parameters = methodParams.get(method); + if (parameters != null && parameters.firstIsEnclosingInstance()) { + return true; + } + + // synthetic field named `this$*` is the enclosing instance, + // the constructor belongs to a "non-static" class + // + // this works for: + // - javac until JDK 18 (see https://bugs.openjdk.org/browse/JDK-8271623) + // - ecj + for (FieldInfo field : fields) { + if (Modifiers.isSynthetic(field.flags()) && field.name().startsWith("this$")) { + return true; + } + } + + return false; + } + + return klass.nestingType() == ClassInfo.NestingType.INNER && !Modifier.isStatic(klass.flags()); + } + private static boolean isEnumConstructor(MethodInfo method) { return method.declaringClass().isEnum() && method.isConstructor(); } @@ -1102,29 +1145,6 @@ private static void setTypeParameters(AnnotationTarget target, Type[] typeParame } - private boolean isInnerConstructor(MethodInfo method) { - if (!method.isConstructor()) { - return false; - } - - ClassInfo klass = method.declaringClass(); - - // there's no indication in the class file whether a local or anonymous class was declared in static context, - // so we use a heuristic: a class was not declared in static context if it has a synthetic field named `this$N` - // (this seems to work both for javac and ecj) - if (klass.nestingType() == ClassInfo.NestingType.LOCAL - || klass.nestingType() == ClassInfo.NestingType.ANONYMOUS) { - for (FieldInfo field : fields) { - if (Modifiers.isSynthetic(field.flags()) && field.name().startsWith("this$")) { - return true; - } - } - return false; - } - - return klass.nestingType() == ClassInfo.NestingType.INNER && !Modifier.isStatic(klass.flags()); - } - private void resolveTypeAnnotation(AnnotationTarget target, TypeAnnotationState typeAnnotationState) { // Signature is erroneously omitted from bridge methods with generic type annotations if (typeAnnotationState.genericsRequired && !signaturePresent.containsKey(target)) { @@ -2510,8 +2530,7 @@ public ClassSummary indexWithSummary(InputStream stream) throws IOException { methods = null; fields = null; recordComponents = null; - debugParameterNames = null; - methodParameterNames = null; + methodParams = null; modulePackages = null; moduleMainClass = null; } diff --git a/core/src/main/java/org/jboss/jandex/MethodParamList.java b/core/src/main/java/org/jboss/jandex/MethodParamList.java new file mode 100644 index 00000000..20044466 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/MethodParamList.java @@ -0,0 +1,168 @@ +package org.jboss.jandex; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +// only used during class indexing to remember: +// - parameter names +// - information about synthetic/mandated parameters +final class MethodParamList { + private final MethodInfo method; + + // from the `MethodParameters` attribute + private final List proper = new ArrayList<>(); + // from the local variable table + private final List debug = new ArrayList<>(); + + // these variables are initialized in `finish` + private ParamData[] possiblyNamed; + private boolean firstIsEnclosingInstance; + + MethodParamList(MethodInfo method) { + this.method = method; + } + + void appendProper(byte[] parameterName, boolean synthetic) { + proper.add(new ParamData(parameterName, synthetic)); + } + + void appendDebug(byte[] parameterName, boolean synthetic) { + debug.add(new ParamData(parameterName, synthetic)); + } + + void finish() { + List proper = this.proper; + List debug = this.debug; + + if (proper.isEmpty() && debug.isEmpty()) { + possiblyNamed = ParamData.EMPTY_ARRAY; + return; + } + + // parameters from the local variable table are always: + // - optionally, single synth param which is the enclosing instance + // - then, all params are possibly named + // (see also `Indexer.processLocalVariableTable`) + if (proper.isEmpty()) { + List list = debug; + if (debug.get(0).syntheticOrMandated) { + list = debug.subList(1, debug.size()); + firstIsEnclosingInstance = true; + } + possiblyNamed = list.toArray(ParamData.EMPTY_ARRAY); + return; + } + + // need to synchronize the shapes of `proper` and `debug` + // (fortunately, there are very few situations when they are out of sync) + if (method.isConstructor()) { + if (method.declaringClass().isRecord()) { + // keep record compact constructor parameters intact + } else if (method.declaringClass().isEnum()) { + // remove the first 2 synth params of an enum constructor + proper = proper.subList(2, method.parametersCount()); + } else { + // `method.declaringClass().nestingType()` is not necessarily correct at the moment, + // so we can't use it to detect if this constructor possibly belongs to an inner class + // + // in any case, we need to: + // 1. remember if there's an enclosing instance synth param at the beginning + // 2. remove all synth params + if (proper.get(0).syntheticOrMandated && !debug.isEmpty() && debug.get(0).syntheticOrMandated) { + debug.remove(0); + firstIsEnclosingInstance = true; + } + for (Iterator it = proper.iterator(); it.hasNext();) { + ParamData param = it.next(); + if (param.syntheticOrMandated) { + it.remove(); + } + } + } + } + + // if there's no local variable table, the adjusted `proper` list is the best we've got + if (debug.isEmpty()) { + possiblyNamed = proper.toArray(ParamData.EMPTY_ARRAY); + return; + } + + // `proper` should have the same shape as `debug` now, just being extra careful here + int size = Math.min(proper.size(), debug.size()); + ParamData[] result = new ParamData[size]; + for (int i = 0; i < size; i++) { + ParamData properParameter = proper.get(i); + ParamData debugParameter = debug.get(i); + ParamData resultParameter = new ParamData( + properParameter.name != null ? properParameter.name : debugParameter.name, + properParameter.syntheticOrMandated || debugParameter.syntheticOrMandated); + result[i] = resultParameter; + } + possiblyNamed = result; + } + + boolean firstIsEnclosingInstance() { + return firstIsEnclosingInstance; + } + + byte[][] getNames() { + ParamData[] params = possiblyNamed; + + // iterate backwards to ignore parameters at the end that don't have a name + int count = 0; + boolean seenNamed = false; + for (int i = params.length - 1; i >= 0; i--) { + if (!seenNamed && params[i].name == null) { + continue; + } + + seenNamed = true; + count++; + } + if (count == 0) { + return null; + } + + // copy parameter names into the result array + byte[][] result = new byte[count][]; + int pos = 0; + for (int i = 0; i < params.length; i++) { + result[pos] = params[i].name; + pos++; + if (pos >= result.length) { // shouldn't happen, just extra care + break; + } + } + + // many places in Jandex assume that list of parameter names: + // - may be shorter than the full list of parameters (that is, it may be a prefix) + // - doesn't contain any unknown (`null`) values + for (byte[] name : result) { + if (name == null) { + // if there actually is a `null` value, we rather pretend + // that we don't have parameter names at all + return null; + } + } + return result; + } + + static final class ParamData { + static final ParamData[] EMPTY_ARRAY = new ParamData[0]; + + final byte[] name; + final boolean syntheticOrMandated; + + ParamData(byte[] name, boolean syntheticOrMandated) { + this.name = name; + this.syntheticOrMandated = syntheticOrMandated; + } + + // only for debugging, not supposed to be used otherwise + @Override + public String toString() { + return (name == null ? "" : Utils.fromUTF8(name)) + (syntheticOrMandated ? "" : ""); + } + } +}