Constructor parameters added by the compiler (inner classes, enums) make `getParameters()` / `getParameterTypes()` inconsistent and misplace parameter annotations
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 48/100
Research direction
Start with JavaCodeUnit.getParameters() and getParameterTypes(), then trace how constructor descriptors, Signature, MethodParameters, and parameter annotations are imported; inspect the tests covering inner classes, enums, and annotation indexing, including the related issues #1088, #1759, and #1773. Implementing this requires coordinating parameter types, source/compiler-added parameter classification, and annotation placement. Done means constructor parameters match the class-file descriptor, retain available generic types, classify compiler-added parameters, and attach annotations to declared parameters.
Written by the indexing model from the issue text.
Description
Summary
For constructors of inner classes and enums, the compiler adds parameters that do not exist in the source code (the enclosing instance, the enum constant name and ordinal, captured local variables). Depending on whether the constructor also has a generic signature, JavaCodeUnit.getParameterTypes() and JavaCodeUnit.getParameters() sometimes include these parameters and sometimes not. In the case where they are included, parameter annotations end up on the wrong parameter (#1088).
The goal of this issue is to make getParameters() and getParameterTypes() consistent: one JavaParameter per parameter the constructor actually has in the class file, generic types where the class file provides them, and a way to tell which parameters were declared in the source code and which were added by the compiler.
Examples
@Retention(RUNTIME)
@interface Ann {}
class Outer {
class InnerWithoutGenerics {
InnerWithoutGenerics(@Ann String s) {}
}
class InnerWithGenerics {
InnerWithGenerics(@Ann List<String> list) {}
}
}
enum SomeEnum {
A("x");
SomeEnum(@Ann String s) {}
}
What ArchUnit (current main, imported with JDK 21) reports for the single constructor of each class:
| class file descriptor | getRawParameterTypes() |
getParameterTypes() |
getParameters() |
@Ann reported on |
|
|---|---|---|---|---|---|
InnerWithoutGenerics |
(Outer, String) |
[Outer, String] |
[Outer, String] |
2 parameters | parameter 0, i.e. the Outer instance (wrong) |
InnerWithGenerics |
(Outer, List) |
[Outer, List] |
[List<String>] |
1 parameter | parameter 0, i.e. List<String> |
SomeEnum |
(String, int, String) |
[String, int, String] |
[String] |
1 parameter | parameter 0, i.e. String s |
For comparison, java.lang.reflect.Constructor.getParameters() returns 2, 2 and 3 parameters respectively and reports @Ann on the last one in each case; with classes compiled by JDK 21, Parameter.isImplicit() is true for the Outer instance and Parameter.isSynthetic() for the enum name and ordinal.
So today:
getParameters().size()for two constructors of the same shape differs only because one of them has a generic parameter type.getParameterTypes()andgetParameters()are consistent withgetRawParameterTypes()only in the first row.- In the first row the annotation is attached to the compiler-added parameter instead of the declared one. This is #1088; #1759 fixed the annotation index in isolation, and the review of that PR led to the broader picture described here.
Why this happens
A class file carries the constructor parameters in two places:
- the method descriptor, which always lists every parameter, including the ones added by the compiler;
- the optional
Signatureattribute, which lists the parameters as declared in the source code with their generic types.javaconly writes it when the constructor needs it, and it omits the compiler-added parameters.
ArchUnit derives getRawParameterTypes() from the descriptor and getParameterTypes() / getParameters() from the Signature attribute if present, otherwise from the descriptor. Whether javac writes a Signature for a constructor without generics depends on the kind of compiler-added parameters: it writes one for enum constructors and for constructors of local classes that capture variables, but not for an inner class whose only extra parameter is the enclosing instance. That is why the three rows above differ.
A related quirk: if the Signature attribute is present but lists no parameters at all, as for an enum constructor without declared parameters (descriptor (String, int), Signature ()), ArchUnit cannot tell it from a missing attribute and falls back to the descriptor. So getParameters() has 2 entries for SomeEnum() {}, but 1 entry for SomeEnum(String s) {}.
Parameter annotations (RuntimeVisibleParameterAnnotations / RuntimeInvisibleParameterAnnotations) only count the parameters declared in the source code. When ArchUnit uses the descriptor to build getParameters(), annotation index i is applied to descriptor parameter i, which is shifted by the number of leading compiler-added parameters.
Proposal
getParameters()returns oneJavaParameterper descriptor parameter, so its size always equalsgetRawParameterTypes().size()andConstructor.getParameters().length.getParameterTypes()returns the type of each of these parameters: the generic type from theSignatureattribute where the class file provides one, the raw type otherwise. It has the same size asgetRawParameterTypes().- Each
JavaParametertells whether it was declared in the source code or added by the compiler, with predicates to filter on it. The JVM specification andjavax.lang.model.util.Elements.Origindistinguish three kinds: explicit (declared in source), mandated (not in source, but required by the language specification, e.g. the enclosing instance of an inner class), synthetic (an implementation detail of the compiler, e.g. enum name and ordinal, captured variables). Note that the Reflection API calls mandated parameters "implicit" (Parameter.isImplicit()), so the final naming needs some care. - Parameter annotations are attached to the declared parameter they belong to.
- Where the class file has a
MethodParametersattribute (written byjavac21+ for these constructors, or by anyjavacwith-parameters), its flags are used as the source of truth. Otherwise the kind of each parameter is derived from the kind of class (enum, inner class, local or anonymous class in a static or non-static context) and from the difference between descriptor andSignature.
Example after the change: all three constructors above have as many parameters as their descriptor, getParameterTypes() reports [Outer, List<String>] for InnerWithGenerics and [String, int, String] for SomeEnum, and @Ann is reported on the last parameter in all three cases.
Compatibility
For constructors of inner, local and anonymous classes and of enums that have a Signature attribute, getParameterTypes() and getParameters() grow by the compiler-added parameters. Up to now getParameterTypes() mirrored Constructor.getGenericParameterTypes(); afterwards both methods have the size of Constructor.getParameters() instead. Rules that count or index constructor parameters of such classes may see different results, and can use the new predicates to restrict themselves to declared parameters. getRawParameterTypes(), getFullName() and access resolution are not affected, since they are already based on the descriptor.
Out of scope
The compiler-added parameters get their raw type from the descriptor, e.g. Outer and not Outer<T> for an inner class of a generic class. Providing generic types for them is tracked separately in #1773.
- Dominant language
- Java
- Stars
- 3.9k
- Forks
- 350
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 16
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TNG/ArchUnit
-
Difficulty 3/5 1-2 days Newbie friendliness 52/100
Maintainers usually reply within 1 day
-
breaking change
Difficulty 5/5 Over a week Newbie friendliness 18/100
TNG/ArchUnit#1756 · 1 comment ·
Maintainers usually reply within 1 day
-
breaking change
Difficulty 4/5 3-5 days Newbie friendliness 30/100
Maintainers usually reply within 1 day
-
breaking change
Difficulty 4/5 3-5 days Newbie friendliness 22/100
Maintainers usually reply within 1 day
-
breaking change
Difficulty 3/5 Half a day Newbie friendliness 62/100
Maintainers usually reply within 1 day
Similar issues
-
area:core bug priority:p2
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
aomarai/minecraft-mcp#247 ·
Maintainers usually reply within 1 day
-
help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
antonyrag/ragleap-core#1062 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Vakore/ZappierGames#232 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
constellation-app/constellation#2588 ·