-
-
Notifications
You must be signed in to change notification settings - Fork 352
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
feat: Add CtTry.addCatcherAt
#4954
Conversation
2cb5e87
to
8bc0965
Compare
@slarse please have a look at this when you get time. |
* @throws IndexOutOfBoundsException if the position is out of range (position < 0 || position > number of catchers) | ||
*/ | ||
@PropertySetter(role = CATCH) | ||
<T extends CtTry> T addCatcherAt(int position, CtCatch catcher); |
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 am using position
to refer to index
in the catchers
collection because it has been used for all analogous methods. See
<T extends CtAbstractSwitch<S>> T addCaseAt(int position, CtCase<? super S> c); |
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 think it's a fine enough word. "Index" is rather data structure specific, position feels more implementation agnostic to me.
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.
Some notes and some stuff I think we should change :)
* @throws IndexOutOfBoundsException if the position is out of range (position < 0 || position > number of catchers) | ||
*/ | ||
@PropertySetter(role = CATCH) | ||
<T extends CtTry> T addCatcherAt(int position, CtCatch catcher); |
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.
<T extends CtTry> T addCatcherAt(int position, CtCatch catcher); | |
CtTry addCatcherAt(int position, CtCatch catcher); |
This is a design flaw that we need to, well, not do anymore :).
I haven't been around for long enough to know why this is so prevalent in Spoon, but it's a terrible misuse of Java's generics. To illustrate why a return type like this is so problematic, consider the following code:
CtTry tryStatement = new CtTryImpl();
CtCatch catcher = new CtCatchImpl();
CtTryWithResource tryWithResource = tryStatement.addCatcherAt(0, catcher);
This will compile just fine without even a warning, but result in the following runtime error:
java.lang.ClassCastException: class spoon.support.reflect.code.CtTryImpl cannot be cast to class spoon.reflect.code.CtTryWithResource (spoon.support.reflect.code.CtTryImpl and spoon.reflect.code.CtTryWithResource are in unnamed module of loader 'app')
That's incredibly confusing as the calling code hasn't performed any casts, much less any unsafe casts.
One could argue that we should keep using this pattern for the interface to be consistent, but this pattern is so incredibly problematic that I think breaking consistency is a small price to pay for not making this problem any larger than it already is.
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.
That is indeed confusing, but why does it throw the class cast exception? CtTryWithResource
extends from CtTry
so it should be valid during runtime as well. Is it because type erasure replaces <T extends CtTry>
to CtTry
? I do not understand what exactly is happening at runtime.
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.
CtTryWithResource extends from CtTry so it should be valid during runtime as well.
Yes, but the actual dynamic type of the tryStatement
variable is CtTryImpl
, which does not implement CtTryWithResource
. So, there's an exception when the runtime tries to cast to the latter.
I wrote a really elaborate explanation for this, and then the page refreshed and I lost it :D. I'll give it a go again tomorrow, but the very short explanation is that the problems occur with the unchecked cast in the return statement (i.e. (T)
). Basically, it's not a cast at all, it's just telling the compiler "trust me mate, this is fine".
The actual cast here occurs when we assign to the variable with type CtTryWithResource
after the method has returned a value. That's where the crash occurs. Java is hailed as a statically typed language, but it's actually very much a dynamically typed language as well, and implicit type checks occur all the time wherever generics are involved.
I'll try to circle back around to the rather nasty ramifications this stuff can have: a confusing ClassCastException
is just the tip of the iceberg.
I spent way too much time on writing (and losing) the explanation here so I'll have to get back to the actual PR at a later time :)
} | ||
|
||
@Override | ||
public <T extends CtTry> T addCatcherAt(int position, CtCatch catcher) { |
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.
public <T extends CtTry> T addCatcherAt(int position, CtCatch catcher) { | |
public CtTry T addCatcherAt(int position, CtCatch catcher) { |
Same comment as on the interface.
CtCatch first = factory.createCatch(); | ||
first.setParameter(factory | ||
.createCatchVariable() | ||
.setType(factory.Type().createReference(IOException.class))); | ||
|
||
CtCatch second = factory.createCatch(); | ||
second.setParameter(factory | ||
.createCatchVariable() | ||
.setMultiTypes(List.of( | ||
factory.Type().createReference(ExceptionInInitializerError.class), | ||
factory.Type().createReference(NoSuchFieldException.class)))); | ||
|
||
CtCatch third = factory.createCatch(); | ||
third.setParameter(factory | ||
.createCatchVariable() | ||
.setType(factory.Type().createReference(NoSuchMethodException.class))); |
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.
This is kind of a mouthful to read, and there is as far as I can tell unnecessary complexity in having a catcher that catches multiple types (I can't see how this influences insertion?).
I'd suggest something like this:
CtCatch first = factory.createCatch(); | |
first.setParameter(factory | |
.createCatchVariable() | |
.setType(factory.Type().createReference(IOException.class))); | |
CtCatch second = factory.createCatch(); | |
second.setParameter(factory | |
.createCatchVariable() | |
.setMultiTypes(List.of( | |
factory.Type().createReference(ExceptionInInitializerError.class), | |
factory.Type().createReference(NoSuchFieldException.class)))); | |
CtCatch third = factory.createCatch(); | |
third.setParameter(factory | |
.createCatchVariable() | |
.setType(factory.Type().createReference(NoSuchMethodException.class))); | |
CtCatch first = createCatch(factory, IOException.class); | |
CtCatch second = createCatch(factory, ExceptionInInitializerError.class); | |
CtCatch third = createCatch(factory, NoSuchMethodException.class); |
With a helper like so:
private <T> CtCatch createCatch(Factory factory, Class<T> typeToCatch) {
CtTypeReference<T> typeReference = factory.Type().createReference(typeToCatch);
return factory.createCatch().setParameter(
factory.createCatchVariable().setType(typeReference)
);
}
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.
createCatch(Factory, Class )
Any reason why we pass the same instance of Factory
and not create a new one whenever we want to call createCatch
?
@Nested | ||
class AddCatcherAt { | ||
@Test | ||
void addsCatcherAtTheSpecifiedPosition() { |
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.
Note: This test really tests three separate cases in one go:
- Inserting into an empty list.
- Inserting at the beginning of a non-empty list.
- Inserting at a position that is neither first nor last.
Given that this functionality is neither complicated nor critical to Spoon, that's probably fine. But it always bears thinking about how much is tested in any single test. The fact that we never insert at the end of a non-empty list also isn't a big deal considering what the implementation looks like.
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.
But it always bears thinking about how much is tested in any single test.
Right. Ideally, we should test only one case per unit test. I will keep this in mind. We can skip refactoring for this feature because, like you said, it is not that critical and is rather a very simple feature.
The fact that we never insert at the end of a non-empty list also isn't a big deal considering what the implementation looks like.
Tests of addCatcher
would already test that so we do not need to think about them I guess.
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.
Tests of addCatcher would already test that so we do not need to think about them I guess.
Precisely, "considering what the implementation looks like" :). White box testing has merits in that we can omit redundant testing, but it has drawbacks to match (e.g. creating tests based on the implementation rather than the specification).
CtCatch catcherAtWrongPosition = factory.createCatch(); | ||
catcherAtWrongPosition.setParameter(factory | ||
.createCatchVariable() | ||
.setType(factory.Type().createReference(Exception.class))); |
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.
Also use the proposed helper here :)
Co-authored-by: Simon Larsén <slarse@slar.se>
} | ||
|
||
@Override | ||
public <T extends CtTry> T addCatcherAt(int position, CtCatch catcher) { | ||
if (catcher == null) { | ||
return (T) this; |
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.
💬 2 similar findings have been found in this PR
unchecked: unchecked cast
🔎 Expand here to view all instances of this finding
File Path | Line Number |
---|---|
src/main/java/spoon/support/reflect/code/CtTryImpl.java | 68 |
src/main/java/spoon/support/reflect/code/CtTryImpl.java | 82 |
Visit the Lift Web Console to find more details in your report.
ℹ️ Learn about @sonatype-lift commands
You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.
Command | Usage |
---|---|
@sonatype-lift ignore |
Leave out the above finding from this PR |
@sonatype-lift ignoreall |
Leave out all the existing findings from this PR |
@sonatype-lift exclude <file|issue|path|tool> |
Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file |
Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.
Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]
Co-authored-by: Simon Larsén <slarse@slar.se>
Co-authored-by: Simon Larsén <slarse@slar.se>
I pushed an empty commit to give @slarse co-authorship, but now I realise that I have accepted some of your suggestions so you will be a co-author anyway if this PR gets squashed and merged 😅 |
() -> tryStatement.addCatcherAt(1, catcherAtWrongPosition)); | ||
} | ||
|
||
private <T extends Throwable> CtCatch createCatch(Factory factory, Class<T> typeToCatch) { |
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 made it type stricter by replacing T
with <T extends Throwable>
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 think it will be a couple of days before I can return to this, I was away during the weekend which ate up most of what would normally be my OSS time :) |
Understandable. Take your time. Not very urgent. :) |
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.
Thanks @algomaster99 , sorry about the delay! Turned out I was away this weekend as well :s
@Nested | ||
class AddCatcherAt { | ||
@Test | ||
void addsCatcherAtTheSpecifiedPosition() { |
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.
Tests of addCatcher would already test that so we do not need to think about them I guess.
Precisely, "considering what the implementation looks like" :). White box testing has merits in that we can omit redundant testing, but it has drawbacks to match (e.g. creating tests based on the implementation rather than the specification).
() -> tryStatement.addCatcherAt(1, catcherAtWrongPosition)); | ||
} | ||
|
||
private <T extends Throwable> CtCatch createCatch(Factory factory, Class<T> typeToCatch) { |
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.
👍
Reference: #3884
The order of catchers is important because the thrown exception is matched with the parameters of the catchers in the order they appear, hence it is semantically important.