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

Support inheritance across modules and incremental annotation processing #1466

Conversation

stephanenicolas
Copy link
Contributor

@stephanenicolas stephanenicolas commented Feb 9, 2019

The PR is now ready for review: @JakeWharton @ZacSweers.
I did my best to comment it and help your review. I remain available to address your comments and answer your questions. I would appreciate our exchanges to be respectful and open.
Thx in advance.

This solves both:

Both issues are linked: to support incremental changes, we have to support the case where a subclass only is recompiled and the super class is not passed to the annotation processor. The same occurs for inheritance across modules.

In this case, we want to avoid doing some work that we have already done, we don't want to re-scan the annotations of the super class (it would have been possible to access it via annotation processing). To avoid this duplicate processing, we reuse the generated code of the super class in the subclass. I will comment more inside the PR on how this is currently achevied.

private Filer filer;
private @Nullable Trees trees;

private int sdk = 1;
private boolean debuggable = true;

private final RScanner rScanner = new RScanner();
private HashMap<String, List<Element>> mapGeneratedFileToOriginatingElements
= new LinkedHashMap<>();
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

This is used only for testing. I will talk more about tests below.

} catch (Throwable t) {
try {
trees = Trees.instance(processingEnv);
} catch (Throwable ignored) {
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

This whole block here is meant to access the ProcessingEnvironment instance passed by gradle. Gradle wraps the ProcessingEnvironment internally so that it can control the filer passed to annotation processors.

The wrapper class provided by gradle is IncrementalProcessingEnvironment.

This class is not exposed by Gradle. The idea is to access this class and the original wrapped ProcessingEnvironment. We need this vanilla instance code because Trees.getInstance(..) checks that it receives the original processing environment. If you look at the code of the Trees class, you will see that it first checks the class name of the instance. I tried to bypass this check and call the method getJavacTrees that the instance method is calling internally. But there are additional checks later on the road. I resorted to use reflection to access the original processing environment in the non exposed gradle wrapper class.

I have opened an issue in gradle's code base on this in: gradle/gradle#8393
The PR has been rejected has gradle doesn't want to expose this class. As you can see in this issue, we discuss the safety of accessing Trees in the BK processor during incremental annotation processing. This practice is a corner case when incremental processing is used and it is generally not advised to turn annotation processors to incremental if they use this API. Though, I explained why I believe there is no issue in doing so in our case.

Also, at the end of the discussion in the gradle PR, gradle says that the processing environment is always wrapped. It was not my experience when testing: I received the vanilla processing environment when I was developing. I didn't follow up with gradle on this. But for this PR, I don't think it should matter as the case was already handled by just swallowing the exception. In practice, the PR has been tested at an industrial scale in 2 companies that I know of and all works well. This code has fixed a bug in the first version of the fork: stephanenicolas#2.

@@ -201,6 +217,10 @@
return false;
}

public HashMap<String, List<Element>> getMapGeneratedFileToOriginatingElements() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method is also always visible for testing. Of course, we can change the visibility accordingly.

@@ -1273,7 +1326,7 @@ private static boolean isTypeEqual(TypeMirror typeMirror, String otherType) {
return null;
}
typeElement = (TypeElement) ((DeclaredType) type).asElement();
if (parents.contains(typeElement)) {
if (parents.contains(typeElement) || hasViewBinder(typeElement)) {
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

This comment is about all the changes above from line 381 to here. The idea is that we want to take into account the situation where a sub class is being processed and not its super class (it applies both when we are in an incremental build and when the super class is in a different module).

Even though the super class is not passed to the annotation processor, we can know if butterknife has generated some code for it by testing the presence of the generated class in the classpath. If such a __Binder exists for the super class, we create a StubBindingSet for it: we do re-scan its annotations here, only because we want to know which constructor of the super class to invoke later when we generate the __Binder of the sub class. (The constructor of a __Binder can have 2 forms depending on whether the class uses@BindView(s) annotations.)

Of course, I am open to amend the way of doing things if there is a better solution. We could have a better way to handle the case, by adding some information to the generated code of the super class for instance. I preferred to limit the scope of changes in the PR.

assertEquals("test/Test_ViewBinding.java", entry.getKey());
List<Element> elements = entry.getValue();
assertEquals(1, elements.size());
assertEquals("test.Test", elements.get(0).asType().toString());
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

In this test, I use the method and the map I have added to the BK processor.

There are different options here:

  1. unit testing: we can test that we pass the right originating element to the filer. This is a healthy practice, idependently of gradle's incremental solution.
  2. integration testing. I've seen other projects using integration tests with gradle, with fixtures, and I am not very familiar with this technology. Such tests can fully reproduce an incremental build.

I decided to go with the first testing strategy as it limits the impact of the changes here, I also think this test and the tested behavior is just "healthier" for the compiler. But we can also prefer to test more directly that incremental annotation processing is working as expected via an integration test, which would be better to ensure there is no regression at this level. It would probably bring more value for maintenance.
If we decide to go this way though, I will need some help to write such a test.

.and()
.generatesSources(bindingTestSource);
}
}
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 have separted the test to check if we support inheritance of a precompiled class. It covers both incremental compilation and support of inheritance across modules.

The idea is that we now have a new class (right below in the current PR) that is part of the test code itself. Hence it will be compiled, processed by butterknife too and passed on the classpath of the tests when they are run. It mimics the fact that we are relying on a compiled super class, and not to a super class passed to the processor.

@@ -0,0 +1,27 @@
buildscript {
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

I have decomposed the library of the sample into a base library and a library itself so that the sample also covers the case of inheritance across module. Feel free to revert if you want.

//TODO this change is just to demonstrate that the new compiler works well
//but should be reversed when the new version is released
//annotationProcessor deps.release.compiler
annotationProcessor project(':butterknife-compiler')
Copy link
Contributor Author

@stephanenicolas stephanenicolas Feb 9, 2019

Choose a reason for hiding this comment

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

I can revert that as soon as you want, I just use it for debugging purposes. I will probably open a ticket soon on the BK repo: in another project we now use composite builds to manage samples and it works great. It allows to have a clean sample and also to use the current modules being developed for testing purposes.

Copy link

@oldergod oldergod left a comment

Choose a reason for hiding this comment

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

cool

filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
//reflection won't be necessary after https://github.com/gradle/gradle/pull/8393

Choose a reason for hiding this comment

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

The issue is closed, is the comment still relevant?

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 think it still provides some context. Let me know if you want me to remove it.

Choose a reason for hiding this comment

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

If the issue isn't getting fixed, maybe we can change it. What would future dev would like to see here?

BindingSet.Builder parentBuilder = BindingSet.newBuilder(parentType);
if (hasViewBindings(parentType)) {
//add a fake field to the parent class so that it will indicate it has a view bindings.
//this is required for the subclass to generate a proper view binder

Choose a reason for hiding this comment

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

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 try to do that too. You mean it should start with upper case and end with a . ?

Choose a reason for hiding this comment

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

yes

import static butterknife.TestStubs.ANDROIDX_CONTEXT_COMPAT;
import java.util.List;
import java.util.Map;

Choose a reason for hiding this comment

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

not sure imports order is right, and no need to add empty lines between them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think of applying the spotless plugin in ButterKnife ? I can file a ticket for that.

Choose a reason for hiding this comment

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

Jake can decide, I don't know. My point is rather that whatever rule was applied before is the correct way to organize imports.


//TODO this change is just to demonstrate that the new compiler works well
//but should be reversed when the new version is released
//annotationProcessor deps.release.compiler

Choose a reason for hiding this comment

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

@stephanenicolas
Copy link
Contributor Author

stephanenicolas commented Feb 10, 2019 via email

@oldergod
Copy link

Tout est possible à celui qui croit and I believe in Jack Wharton.

@Alonexx
Copy link

Alonexx commented May 10, 2019

This change is not compatible with kapt, since Trees via reflection became invalid again. https://github.com/JetBrains/kotlin/blob/master/plugins/kapt3/kapt3-base/src/org/jetbrains/kotlin/kapt3/base/incremental/incrementalProcessors.kt

@dhruvj
Copy link

dhruvj commented May 18, 2019

When will this be merged? As Google is recommending everyone to move towards "modular" architecture, I think this problem is only going to get more frequently reported.

@stephanenicolas
Copy link
Contributor Author

stephanenicolas commented May 18, 2019 via email

@aritrobanerjee93
Copy link

When will this be merged?

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.

5 participants