-
Notifications
You must be signed in to change notification settings - Fork 4.6k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,7 @@ | |
import java.util.Arrays; | ||
import java.util.BitSet; | ||
import java.util.Deque; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
import java.util.LinkedHashSet; | ||
import java.util.List; | ||
|
@@ -72,6 +73,8 @@ | |
import javax.lang.model.type.TypeKind; | ||
import javax.lang.model.type.TypeMirror; | ||
import javax.lang.model.type.TypeVariable; | ||
import javax.lang.model.util.ElementFilter; | ||
import javax.lang.model.util.Elements; | ||
import javax.lang.model.util.Types; | ||
import javax.tools.Diagnostic.Kind; | ||
|
||
|
@@ -117,13 +120,16 @@ public final class ButterKnifeProcessor extends AbstractProcessor { | |
); | ||
|
||
private Types typeUtils; | ||
private Elements elementUtils; | ||
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<>(); | ||
|
||
@Override public synchronized void init(ProcessingEnvironment env) { | ||
super.init(env); | ||
|
@@ -143,10 +149,18 @@ public final class ButterKnifeProcessor extends AbstractProcessor { | |
debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE)); | ||
|
||
typeUtils = env.getTypeUtils(); | ||
elementUtils = env.getElementUtils(); | ||
filer = env.getFiler(); | ||
try { | ||
trees = Trees.instance(processingEnv); | ||
} catch (IllegalArgumentException ignored) { | ||
//reflection won't be necessary after https://github.com/gradle/gradle/pull/8393 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The issue is closed, is the comment still relevant? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
java.lang.reflect.Field delegateField = processingEnv.getClass().getDeclaredField("delegate"); | ||
delegateField.setAccessible(true); | ||
trees = Trees.instance((ProcessingEnvironment) delegateField.get(processingEnv)); | ||
} catch (Throwable t) { | ||
try { | ||
trees = Trees.instance(processingEnv); | ||
} catch (Throwable ignored) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole block here is meant to access the The wrapper class provided by gradle is This class is not exposed by Gradle. The idea is to access this class and the original wrapped I have opened an issue in gradle's code base on this in: gradle/gradle#8393 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. |
||
} | ||
} | ||
} | ||
|
||
|
@@ -190,9 +204,11 @@ private Set<Class<? extends Annotation>> getSupportedAnnotations() { | |
TypeElement typeElement = entry.getKey(); | ||
BindingSet binding = entry.getValue(); | ||
|
||
JavaFile javaFile = binding.brewJava(sdk, debuggable); | ||
JavaFile javaFile = binding.brewJava(sdk, debuggable, typeElement); | ||
try { | ||
javaFile.writeTo(filer); | ||
mapGeneratedFileToOriginatingElements.put(javaFile.toJavaFileObject().getName(), | ||
javaFile.typeSpec.originatingElements); | ||
} catch (IOException e) { | ||
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); | ||
} | ||
|
@@ -201,6 +217,10 @@ private Set<Class<? extends Annotation>> getSupportedAnnotations() { | |
return false; | ||
} | ||
|
||
public HashMap<String, List<Element>> getMapGeneratedFileToOriginatingElements() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
return mapGeneratedFileToOriginatingElements; | ||
} | ||
|
||
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { | ||
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); | ||
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); | ||
|
@@ -358,6 +378,12 @@ private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { | |
bindingMap.put(type, builder.build()); | ||
} else { | ||
BindingSet parentBinding = bindingMap.get(parentType); | ||
|
||
// parent binding is null, let's try to find a previouly generated binding | ||
if (parentBinding == null && hasViewBinder(parentType)) { | ||
parentBinding = createStubBindingSet(parentType); | ||
} | ||
|
||
if (parentBinding != null) { | ||
builder.setParent(parentBinding); | ||
bindingMap.put(type, builder.build()); | ||
|
@@ -371,6 +397,33 @@ private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { | |
return bindingMap; | ||
} | ||
|
||
private BindingSet createStubBindingSet(TypeElement parentType) { | ||
BindingSet parentBinding; | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
parentBuilder.addField(new Id(-1), new FieldViewBinding("", null, false)); | ||
} | ||
parentBinding = parentBuilder.build(); | ||
return parentBinding; | ||
} | ||
|
||
private boolean hasViewBindings(TypeElement parentType) { | ||
for (VariableElement fieldElement : ElementFilter.fieldsIn(parentType.getEnclosedElements())) { | ||
if (fieldElement.getAnnotation(BindView.class) != null | ||
|| fieldElement.getAnnotation(BindViews.class) != null) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private boolean hasViewBinder(TypeElement typeElement) { | ||
final String viewBindingClassName = typeElement.getQualifiedName().toString() + "_ViewBinding"; | ||
return elementUtils.getTypeElement(viewBindingClassName) != null; | ||
} | ||
|
||
private void logParsingError(Element element, Class<? extends Annotation> annotation, | ||
Exception e) { | ||
StringWriter stackTrace = new StringWriter(); | ||
|
@@ -1273,7 +1326,7 @@ private BindingSet.Builder getOrCreateBindingBuilder( | |
return null; | ||
} | ||
typeElement = (TypeElement) ((DeclaredType) type).asElement(); | ||
if (parents.contains(typeElement)) { | ||
if (parents.contains(typeElement) || hasViewBinder(typeElement)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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. |
||
return typeElement; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
butterknife.compiler.ButterKnifeProcessor,isolating |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,22 @@ | |
import butterknife.compiler.ButterKnifeProcessor; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.testing.compile.JavaFileObjects; | ||
|
||
import javax.lang.model.element.Element; | ||
import javax.tools.JavaFileObject; | ||
import javax.tools.StandardLocation; | ||
|
||
import org.junit.Test; | ||
|
||
import static butterknife.TestStubs.ANDROIDX_CONTEXT_COMPAT; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
import static com.google.common.truth.Truth.assertAbout; | ||
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; | ||
import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; | ||
import static java.util.Arrays.asList; | ||
import static junit.framework.Assert.assertEquals; | ||
|
||
public class BindViewTest { | ||
@Test public void bindingViewNonDebuggable() { | ||
|
@@ -49,12 +57,21 @@ public class BindViewTest { | |
+ "}" | ||
); | ||
|
||
ButterKnifeProcessor butterKnifeProcessor = new ButterKnifeProcessor(); | ||
assertAbout(javaSource()).that(source) | ||
.withCompilerOptions("-Xlint:-processing", "-Abutterknife.debuggable=false") | ||
.processedWith(new ButterKnifeProcessor()) | ||
.processedWith(butterKnifeProcessor) | ||
.compilesWithoutWarnings() | ||
.and() | ||
.generatesSources(bindingSource); | ||
|
||
Map<String, List<Element>> map = butterKnifeProcessor.getMapGeneratedFileToOriginatingElements(); | ||
assertEquals(1, map.size()); | ||
Map.Entry<String, List<Element>> entry = map.entrySet().iterator().next(); | ||
assertEquals("test/Test_ViewBinding.java", entry.getKey()); | ||
List<Element> elements = entry.getValue(); | ||
assertEquals(1, elements.size()); | ||
assertEquals("test.Test", elements.get(0).asType().toString()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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. |
||
} | ||
|
||
@Test public void bindingViewSubclassNonDebuggable() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package butterknife; | ||
|
||
import butterknife.compiler.ButterKnifeProcessor; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.testing.compile.JavaFileObjects; | ||
import javax.tools.JavaFileObject; | ||
import javax.tools.StandardLocation; | ||
import org.junit.Test; | ||
|
||
import static com.google.common.truth.Truth.assertAbout; | ||
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; | ||
import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources; | ||
import static java.util.Arrays.asList; | ||
|
||
public class InheritanceTest { | ||
|
||
@Test public void bindingViewFinalClassWithBaseClassAlreadyCompiledInDifferentModule() { | ||
JavaFileObject testSource = JavaFileObjects.forSourceString("test.Test", "" | ||
+ "package test;\n" | ||
+ "import android.view.View;\n" | ||
+ "import butterknife.BindView;\n" | ||
+ "import butterknife.precompiled.Base;\n" | ||
+ "public final class Test extends Base {\n" | ||
+ " @BindView(1) View thing;\n" | ||
+ "}" | ||
); | ||
|
||
JavaFileObject bindingTestSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" | ||
+ "package test;\n" | ||
+ "import android.view.View;\n" | ||
+ "import androidx.annotation.UiThread;\n" | ||
+ "import butterknife.internal.Utils;\n" | ||
+ "import butterknife.precompiled.Base_ViewBinding;\n" | ||
+ "import java.lang.IllegalStateException;\n" | ||
+ "import java.lang.Override;\n" | ||
+ "public final class Test_ViewBinding extends Base_ViewBinding {\n" | ||
+ " private Test target;\n" | ||
+ " @UiThread\n" | ||
+ " public Test_ViewBinding(Test target, View source) {\n" | ||
+ " super(target, source);\n" | ||
+ " this.target = target;\n" | ||
+ " target.thing = Utils.findRequiredView(source, 1, \"field 'thing'\");\n" | ||
+ " }\n" | ||
+ " @Override\n" | ||
+ " public void unbind() {\n" | ||
+ " Test target = this.target;\n" | ||
+ " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" | ||
+ " this.target = null\n" | ||
+ " target.thing = null;\n" | ||
+ " super.unbind();\n" | ||
+ " }\n" | ||
+ "}" | ||
); | ||
|
||
assertAbout(javaSources()).that(asList(testSource)) | ||
.withCompilerOptions("-Xlint:-processing") | ||
.processedWith(new ButterKnifeProcessor()) | ||
.compilesWithoutWarnings() | ||
.and() | ||
.generatesSources(bindingTestSource); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package butterknife.precompiled; | ||
|
||
import android.view.View; | ||
import butterknife.BindView; | ||
|
||
public class Base { | ||
@BindView(1) View thing; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
buildscript { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
repositories { | ||
mavenCentral() | ||
jcenter() | ||
} | ||
|
||
dependencies { | ||
classpath "com.jakewharton:butterknife-gradle-plugin:${versions.release}" | ||
} | ||
} | ||
|
||
apply plugin: 'com.android.library' | ||
apply plugin: 'com.jakewharton.butterknife' | ||
|
||
android { | ||
compileSdkVersion versions.compileSdk | ||
buildToolsVersion versions.buildTools | ||
|
||
defaultConfig { | ||
minSdkVersion versions.minSdk | ||
} | ||
} | ||
|
||
dependencies { | ||
compile deps.release.runtime | ||
annotationProcessor deps.release.compiler | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<manifest package="com.example.butterknife.baselibrary"/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.example.butterknife.baselibrary; | ||
|
||
import android.app.Activity; | ||
import butterknife.BindString; | ||
|
||
public class BaseActivity extends Activity { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,4 @@ | |
|
||
<resources> | ||
<string name="app_name">Butter Knife</string> | ||
</resources> | ||
</resources> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,12 @@ android { | |
|
||
dependencies { | ||
implementation deps.release.runtime | ||
annotationProcessor deps.release.compiler | ||
implementation project(':sample:base-library') | ||
|
||
//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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
annotationProcessor project(':butterknife-compiler') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
testImplementation deps.junit | ||
testImplementation deps.truth | ||
|
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 used only for testing. I will talk more about tests below.