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

add support for accesses from lambdas #847

Merged
merged 5 commits into from
Jun 3, 2022
Merged

add support for accesses from lambdas #847

merged 5 commits into from
Jun 3, 2022

Conversation

oberprah
Copy link
Collaborator

@oberprah oberprah commented Apr 8, 2022

For lambdas in Java code (e.g. () -> "some value") the compiler adds a synthetic static method to the Java class that follows the naming pattern lambda$declaringMethod$42. It also adds an invokeDynamic instruction that creates the link from the method that declares the lambda to this static method.

So far we ignore those invokeDynamic instructions for lambda cases. On the other hand we treat the synthetic lambda method like an ordinary method. This can lead to confusing behavior, e.g.

Supplier<SomeObject> call() {
  return () -> new SomeObject();
}

will now create a JavaConstructorCall that originates from some method lambda$call$0(), while there will be no trace about any constructor call from within the method call().

We decided that for a typical user the simplest and likely most appropriate behavior will be to treat this constructor call from within the lambda as a JavaConstructorCall from call() to SomeObject.<init>() (even though technically it is something different, since the call to new SomeObject() will in this case not happen on invocation time of the method, but on invocation time of the return value). From an architecture test point of view the relevant information will likely be, that there is a dependency from call() to new SomeObject(), which is exactly what this way of treating the call from the lambda will provide.

Thus, to improve this we will track all invokeDynamic instructions and then "merge" these with the accesses from synthetic lambda methods into more appropriate accesses from the code unit declaring the lambda to the target that the synthetic lambda method targets.

@codecholeric
Copy link
Collaborator

Thanks a lot!! 😄 I just had time to take a quick look, but just so we don't forget it: We also need to assert that the methods of imported classes now don't contain any lambda$... methods anymore. These are synthetic methods targeted by invokedynamic when a method declares lambdas, but since we now create a direct relationship from the declaring method to the accesses within the lambda it doesn't make sense to keep these anymore.
Compare also #839 (comment)

@codecholeric codecholeric marked this pull request as ready for review May 6, 2022 19:13
@codecholeric codecholeric changed the title Add support for Lambdas add support for accesses from lambdas May 6, 2022
@codecholeric
Copy link
Collaborator

Rebased it on main and refactored it a little more! We were actually missing that there are also all those other types of accesses besides method calls that can happen from a lambda. Fixed that and filtered out the synthetic lambda methods from the JavaClass.methods. That should make everything a lot easier to understand 🙂
Can you counter-review my changes? (i.e. the final state of the PR)

@codecholeric codecholeric requested a review from hankem May 22, 2022 16:01
Two accesses should not be equal if origin, target and line number match. Because it is legal to write something like

```
void call() {
  callSomeMethod.chainSameMethod().chainSameMethod().chainSameMethod();
}
```

In such a case origin, target and line number are identical. Thus, when we return a `Set<JavaCall<?>>` we will only contain one element with the old implementation, which is not correct. Since only ArchUnit's importer creates domain objects we can simply use the identity `equals` and `hashCode`. Because in the end each access is unique by itself and users never create these objects themselves to compare them against imported objects.

Signed-off-by: Peter Gafert <[email protected]>
There are some clean-ups with regard to the previous Java 8 migration (e.g. functional patterns for `Optional`, redundant type information, etc.). Also there were some unused methods that were overlooked when the dependency resolution process was improved.

Signed-off-by: Peter Gafert <[email protected]>
Most tests were meanwhile residing in the source set `jdk9test` because it was the next higher source set that supported Java 8 language level. Since we now support Java 8 language level everywhere we can merge these tests back into the normal `test` source set.

Signed-off-by: Peter Gafert <[email protected]>
oberprah and others added 2 commits May 29, 2022 19:19
For lambdas in Java code (e.g. `() -> "some value"`) the compiler adds a synthetic static method to the Java class that follows the naming pattern `lambda$declaringMethod$42`. It also adds an `invokeDynamic` instruction that creates the link from the method that declares the lambda to this static method.

So far we ignore those `invokeDynamic` instructions for lambda cases. On the other hand we treat the synthetic lambda method like an ordinary method. This can lead to confusing behavior, e.g.

```
Supplier<SomeObject> call() {
  return () -> new SomeObject();
}
```

will now create a `JavaConstructorCall` that originates from some method `lambda$call$0()`, while there will be no trace about any constructor call from within the method `call()`.

We decided that for a typical user the simplest and likely most appropriate behavior will be to treat this constructor call from within the lambda as a `JavaConstructorCall` from `call()` to `SomeObject.<init>()` (even though technically it is something different, since the call to `new SomeObject()` will in this case not happen on invocation time of the method, but on invocation time of the return value). From an architecture test point of view the relevant information will likely be, that there is a dependency from `call()` to `new SomeObject()`, which is exactly what this way of treating the call from the lambda will provide.

Thus, to improve this we will track all `invokeDynamic` instructions and then "merge" these with the accesses from synthetic lambda methods into more appropriate accesses from the code unit declaring the lambda to the target that the synthetic lambda method targets. For cases where users actually want to distinguish a "direct" access and one that is declared in the context of a lambda we add the boolean flag `declaredInLambda` to `JavaAccess`. This way the information can still be retrieved if it should be relevant in a certain context.

Signed-off-by: Hannes Oberprantacher <[email protected]>
Signed-off-by: Peter Gafert <[email protected]>
Unfortunately, the `gradle-build-action` cache seems to be corrupt and there is no way to manually invalidate the cache. All we can do is to reconfigure the cache prefix or disable the cache for 7 days, which is the default period until caches are invalidated automatically. Since disabling the cache is the simpler solution, and it is only for 7 days, I decided to go with this way.

Signed-off-by: Peter Gafert <[email protected]>
@codecholeric codecholeric merged commit 347dc45 into TNG:main Jun 3, 2022
snuyanzin added a commit to snuyanzin/flink that referenced this pull request Nov 7, 2022
snuyanzin added a commit to snuyanzin/flink that referenced this pull request Dec 11, 2022
snuyanzin added a commit to snuyanzin/flink that referenced this pull request Dec 12, 2022
codecholeric added a commit that referenced this pull request Apr 15, 2023
With #847 we have changed the way lambdas are handled. Before ArchUnit
didn't have any special handling for lambdas which lead to synthetic
methods (like `lambda$foo$123`) being imported like regular methods and
connections like `call() -> doSomething()` in the following example
would be lost:

```
void call() {
  inLambda(() -> doSomething());
}
```

Afterwards we started to eliminate these synthetic methods and attached
the call directly to the origin method. However, try-catch-blocks are
also associated with methods as their owners and here we forgot to
attach them to the correct declaring method. Thus, by throwing out
synthetic lambda methods from the import we would completely lose
try-catch-blocks declared within lambdas.

We now fix this by resolving the correct origin/declaring method for any
try-catch-block, even if it's declared within a lambda.

Resolves: #1069
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