Skip to content
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

Merged
merged 8 commits into from
Oct 23, 2022
Merged

Conversation

algomaster99
Copy link
Contributor

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.

@algomaster99 algomaster99 changed the title feat: Add CtTry.addCatchAt feat: Add CtTry.addCatcherAt Oct 12, 2022
@algomaster99
Copy link
Contributor Author

@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);
Copy link
Contributor Author

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);
as an example.

Copy link
Collaborator

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.

@algomaster99 algomaster99 requested a review from SirYwell October 12, 2022 12:24
Copy link
Collaborator

@slarse slarse left a 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public <T extends CtTry> T addCatcherAt(int position, CtCatch catcher) {
public CtTry T addCatcherAt(int position, CtCatch catcher) {

Same comment as on the interface.

src/test/java/spoon/test/trycatch/TryCatchTest.java Outdated Show resolved Hide resolved
Comment on lines 458 to 473
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)));
Copy link
Collaborator

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:

Suggested change
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)
			);
		}

Copy link
Contributor Author

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() {
Copy link
Collaborator

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:

  1. Inserting into an empty list.
  2. Inserting at the beginning of a non-empty list.
  3. 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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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).

Comment on lines 493 to 496
CtCatch catcherAtWrongPosition = factory.createCatch();
catcherAtWrongPosition.setParameter(factory
.createCatchVariable()
.setType(factory.Type().createReference(Exception.class)));
Copy link
Collaborator

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 :)

src/test/java/spoon/test/trycatch/TryCatchTest.java Outdated Show resolved Hide resolved
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;
Copy link
Contributor

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 ]

algomaster99 and others added 3 commits October 13, 2022 13:21
Co-authored-by: Simon Larsén <slarse@slar.se>
Co-authored-by: Simon Larsén <slarse@slar.se>
@algomaster99
Copy link
Contributor Author

algomaster99 commented Oct 13, 2022

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) {
Copy link
Contributor Author

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>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@algomaster99 algomaster99 requested review from slarse and removed request for SirYwell October 15, 2022 16:13
@slarse
Copy link
Collaborator

slarse commented Oct 16, 2022

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 :)

@algomaster99
Copy link
Contributor Author

Understandable. Take your time. Not very urgent. :)

Copy link
Collaborator

@slarse slarse left a 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() {
Copy link
Collaborator

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@slarse slarse merged commit ca87730 into INRIA:master Oct 23, 2022
@algomaster99 algomaster99 deleted the add-catcher-at branch October 30, 2022 07:35
@monperrus monperrus mentioned this pull request Mar 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants