-
-
Notifications
You must be signed in to change notification settings - Fork 351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: sealed classes (JEP 409) #4337
Conversation
7607c00
to
45f209a
Compare
c3cc9b3
to
d4d0b2a
Compare
Co-authored-by: I-Al-Istannen <i-al-istannen@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work. Added style and set order releated comments.
The printing looks a bit strange to me but this seems to be the JDK style.
|
||
@Override | ||
@UnsettableProperty | ||
CtEnum<T> setPermittedTypes(Collection<CtTypeReference<?>> permittedTypes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we use List here because we need the order for printing?
Or state in the comment the order of the collection is relevant
|
||
@Override | ||
@UnsettableProperty | ||
CtRecord setPermittedTypes(Collection<CtTypeReference<?>> permittedTypes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see other comment
* @return this. | ||
*/ | ||
@PropertySetter(role = CtRole.PERMITTED_TYPE) | ||
CtSealable setPermittedTypes(Collection<CtTypeReference<?>> permittedTypes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see other comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even for printing, the order does not affect the semantics. We can use a LinkedHashSet internally and add a note to the documentation that the iteration order of the collection defines the order when printing, but I would personally prefer to sort it in the printer. Coupling the model structure that strong with the printer feels odd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the DJPP we can sort them, but for the sniper we more or less need the old order. Comment and LinkedHashSet is fine for me.
src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java
Outdated
Show resolved
Hide resolved
src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderQuery.java
Outdated
Show resolved
Hide resolved
@Override | ||
public CtClass<T> setPermittedTypes(Collection<CtTypeReference<?>> permittedTypes) { | ||
Collection<CtTypeReference<?>> types = permittedTypes != null ? permittedTypes : CtElementImpl.emptySet(); | ||
getFactory().getEnvironment().getModelChangeListener().onSetDeleteAll(this, CtRole.PERMITTED_TYPE, this.permittedTypes, new HashSet<>(this.permittedTypes)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In worst case you reorder the collection with new HashSet<>(col)
maybe use LinkedHashSet or something here, even if everywhere else we still do it wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, I don't know the effects on the model change listener when having a different order here ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default changelistener implementation ignores all but role and changed element. Other implementations I have never seen.
return this; | ||
} | ||
if (this.permittedTypes == CtElementImpl.<CtTypeReference<?>>emptySet()) { | ||
this.permittedTypes = new HashSet<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See other comments about ordered collections
@@ -1613,6 +1627,7 @@ public void visitCtCatch(final spoon.reflect.code.CtCatch catchBlock) { | |||
replaceInSetIfExist(ctClass.getSuperInterfaces(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeInformationSuperInterfacesReplaceListener(ctClass)); | |||
replaceInListIfExist(ctClass.getFormalCtTypeParameters(), new spoon.support.visitor.replace.ReplacementVisitor.CtFormalTypeDeclarerFormalCtTypeParametersReplaceListener(ctClass)); | |||
replaceInListIfExist(ctClass.getTypeMembers(), new spoon.support.visitor.replace.ReplacementVisitor.CtTypeTypeMembersReplaceListener(ctClass)); | |||
replaceInSetIfExist(ctClass.getPermittedTypes(), new spoon.support.visitor.replace.ReplacementVisitor.CtSealablePermittedTypesReplaceListener(ctClass)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the set and order problem
printer.writeln().incTab().writeKeyword("permits").writeSpace(); | ||
printList(sealable.getPermittedTypes(), null, false, null, false, false, ",", true, false, null, prettyPrinter::scan); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
printer.writeln().incTab().writeKeyword("permits").writeSpace(); | |
printList(sealable.getPermittedTypes(), null, false, null, false, false, ",", true, false, null, prettyPrinter::scan); | |
printList(sealable.getPermittedTypes(), "permits", true, null, false, false, ",", true, false, null, prettyPrinter::scan); |
I am unsure why writeln
and incTab
was used. Is that necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like how to print these permitted types "correctly" is not really clear. I found three styles
Interface Style
https://github.com/openjdk/jdk/tree/739769c8fc4b496f08a92225a12d07414537b6c0/test/hotspot/jtreg/runtime/sealedClasses
https://github.com/openjdk/jdk/blob/739769c8fc4b496f08a92225a12d07414537b6c0/test/hotspot/jtreg/runtime/sealedClasses/planets/OuterPlanets.jcod#L26
If you look in the openjdk tests the print them like supertypes. e.g.
sealed public class OuterPlanets permits Neptune, asteroids.Pluto, asteroids.Charon
NewLine Style
If you look at the JEP(https://openjdk.java.net/jeps/409) you find them in a new line formatted.
public abstract sealed class Shape
permits Circle, Rectangle, Square { ... }
Eclipse Style(?)
On example, strangely formats each permitted type in a new line. Looks as ugly as a new line for each parameter some old eclipse projects use.
public abstract sealed class Shape
permits com.example.polar.Circle,
com.example.quad.Rectangle,
com.example.quad.simple.Square
So the best we can do is guessing what is correct. The JEP formatting could be a result of the maximum line length.
You could look at the formatters like https://github.com/google/google-java-format if you want to see how google does it. Anyone has a preferred style, any style I missed or a good explanation which style is the best?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion on this. It just happens to be a very long line if it's all in only one (especially with generics, superinterfaces and superclasses).
See ConstantDesc for an example where the permitted types are listed vertically in the OpenJDK, but this one might look odd depending on tab size too.
In best case, the printer could determine how to format without exceeding a configurable line length, but I guess that's a whole new level of problems.
I'm open for more opinions on this.
Funfact: In current projects, we have permits formatted by IntelliJ, it's on a new line if the line would exceed 120 characters and if that would exceed the limit again, it wraps again. It then looks like
sealed class X
permits A, B, C, D,
E, F, G, H {
}
@MartinWitt Were your concerns here fixed? Think you can bring this one in? |
I didn't change anything about the ordering of the permitted types. I originally chose a (Hash)Set because uniqueness is more relevant semantically. Using a List does not make sense imho. I could use a LinkedHashSet internally, but doing that just for printing is odd too, especially if it becomes part of the method contracts. Not guaranteeing any order and sorting in the printer sounds best for me, but I don't know how well the sniper printer can handle that. I also got the questions in the PR description, I think they are still open (and also the question about the formatting style when printing permitted types) |
It's not strictly necessary, as Spoon in general has little validation. But I think it's a nice effort to put validation where the effort and performance impact of it are low.
They could be overridden, but don't need to be. It's fine to leave it to just return
I'd have to look at this, could you point me to a test case to run where something you expect to be implicit ends up not being implicit? |
In that case, it would probably make sense to make it more consistent? Although it's currently not checked with
I'm just not sure if it's worth to have them
Oh, sorry, this is actually a bit out of date. It belongs to my comment here, this is my current "fix". If you remove that part, the test here will fail for those two arguments |
# Conflicts: # src/main/java/spoon/support/compiler/jdt/JDTTreeBuilderHelper.java
#4215 is finally done so I will try to start here over the weekend. |
I got held up that weekend and then I was sick for two weeks. Back on my feet now but I'm so behind on everything I don't know when I'll get around to this. @MartinWitt since you've already reviewed once, would you be able to wrap this one up? It's been sitting for 6 months now, we really gotta get better at bringing in these new feature PRs :s |
Sure, I will take a look at this next week. |
Yes, but this can be done in a future PR.
I'm kinda out of touch of the state of sealed types currently. Does java have implicit sealed/permitted types? Do you have an example for this? |
According to JLS 8.1.6
For enums, the permitted subclasses are always implicit. The tests contain sealed classes and interfaces with direct subtypes in the same compilation unit without explicit permits clause to cover that. |
There's one thing I'd like to point out with the changes here. with the following setup (borrowed from sonarqube): public sealed class A permits B, C, D, E {} // Noncompliant
final class B extends A {}
final class C extends A {}
final class D extends A {}
final class E extends A {} the permits clause seems to be interpreted as a set of fields, of the type A.permits. Also, the subclasses of A are treated as nested types of that class, despite being declared outside it. oops, I just realised I had set the wrong Java version in my spoon env, never mind. |
Sorry, @SirYwell, the weather was nice and I forgot about your PR. I will complete the evaluation process in the next days. After the 'CtRecord' breaking update a short time after release, I got the idea to identify new features and code pieces as Beta or Experimental. I hope we don't need to update anything in the API, but real-world usage frequently reveals something we overlooked. |
After a long time, this PR is ready2merge. Sorry, @SirYwell that this review took so long. |
As this is a new AST Element, any last review from @monperrus or @slarse. From my side, this PR is perfect and ready2merge. |
@MartinWitt I haven\t got the time right now for diving deep into this, but I made a quick scan over the additions, in particular the new interface, and I don't have anything to add. If there's anything specific you'd like my input on then please point me in that direction. Otherwise I think you should go ahead and merge. |
Thanks, @SirYwell, for this great addition. Now we have support for all java features again. |
Indeed, thank you for the hard work @SirYwell ! |
This targets #4189.
With Java 17, sealed classes were finalized. This Pull Request adds support for it to spoon. For the changes to the JLS for this feature, see here.
Model
The
CtSealable
interface is adds methods to retrieve, remove and add permitted types. It does not handle thesealed
modifier (e.g. adding it).Shadow Model
As
sealed
isn't a modifier in Bytecode, it is set whenever the class has permitted types.Questions
final
,asbtract
,sealed
andnon-sealed
at the same time. Would it make sense to have a similar validation here like it is done for visibility modifiers?CtSealable
. Other than the existing methods returning a generic type, this is quite limiting. In theory, those methods could be overridden in the subclasses, but that means 3 overridden methods in each of CtRecord, CtEnum, etc,. Is it worth that noisy overhead or is it probably better to return void instead?Note regards 2.:
It seems like the ReplacementVisitorGenerator does not like overriding methods in other interfaces. It produces invalid code in such cases.
TODO:
CtSealable
seems limiting. Edit: See question 2.