diff --git a/butterknife-compiler/src/main/java/butterknife/compiler/BindingSet.java b/butterknife-compiler/src/main/java/butterknife/compiler/BindingSet.java index e5a891d06..d17e98f39 100644 --- a/butterknife-compiler/src/main/java/butterknife/compiler/BindingSet.java +++ b/butterknife-compiler/src/main/java/butterknife/compiler/BindingSet.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; import javax.annotation.Nullable; +import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; @@ -84,14 +85,14 @@ private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean this.parentBinding = parentBinding; } - JavaFile brewJava(int sdk, boolean debuggable) { - TypeSpec bindingConfiguration = createType(sdk, debuggable); + JavaFile brewJava(int sdk, boolean debuggable, Element originatingElement) { + TypeSpec bindingConfiguration = createType(sdk, debuggable, originatingElement); return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration) .addFileComment("Generated code from Butter Knife. Do not modify!") .build(); } - private TypeSpec createType(int sdk, boolean debuggable) { + private TypeSpec createType(int sdk, boolean debuggable, Element originatingElement) { TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName()) .addModifiers(PUBLIC); if (isFinal) { @@ -125,6 +126,8 @@ private TypeSpec createType(int sdk, boolean debuggable) { result.addMethod(createBindingUnbindMethod(result)); } + result.addOriginatingElement(originatingElement); + return result.build(); } diff --git a/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java b/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java index 3f30e35d9..2f5d9c62b 100644 --- a/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java +++ b/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java @@ -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,6 +120,7 @@ public final class ButterKnifeProcessor extends AbstractProcessor { ); private Types typeUtils; + private Elements elementUtils; private Filer filer; private @Nullable Trees trees; @@ -124,6 +128,8 @@ public final class ButterKnifeProcessor extends AbstractProcessor { private boolean debuggable = true; private final RScanner rScanner = new RScanner(); + private HashMap> 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 + 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) { + } } } @@ -190,9 +204,11 @@ private Set> 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> getSupportedAnnotations() { return false; } + public HashMap> getMapGeneratedFileToOriginatingElements() { + return mapGeneratedFileToOriginatingElements; + } + private Map findAndParseTargets(RoundEnvironment env) { Map builderMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); @@ -358,6 +378,12 @@ private Map 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 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 + 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 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)) { return typeElement; } } diff --git a/butterknife-compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors b/butterknife-compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000..d262f75e5 --- /dev/null +++ b/butterknife-compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +butterknife.compiler.ButterKnifeProcessor,isolating diff --git a/butterknife-runtime/src/test/java/butterknife/BindViewTest.java b/butterknife-runtime/src/test/java/butterknife/BindViewTest.java index 76bae63c3..1eab6aac5 100644 --- a/butterknife-runtime/src/test/java/butterknife/BindViewTest.java +++ b/butterknife-runtime/src/test/java/butterknife/BindViewTest.java @@ -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; + 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> map = butterKnifeProcessor.getMapGeneratedFileToOriginatingElements(); + assertEquals(1, map.size()); + Map.Entry> entry = map.entrySet().iterator().next(); + assertEquals("test/Test_ViewBinding.java", entry.getKey()); + List elements = entry.getValue(); + assertEquals(1, elements.size()); + assertEquals("test.Test", elements.get(0).asType().toString()); } @Test public void bindingViewSubclassNonDebuggable() { diff --git a/butterknife-runtime/src/test/java/butterknife/InheritanceTest.java b/butterknife-runtime/src/test/java/butterknife/InheritanceTest.java new file mode 100644 index 000000000..1416784d0 --- /dev/null +++ b/butterknife-runtime/src/test/java/butterknife/InheritanceTest.java @@ -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); + } +} diff --git a/butterknife-runtime/src/test/java/butterknife/precompiled/Base.java b/butterknife-runtime/src/test/java/butterknife/precompiled/Base.java new file mode 100644 index 000000000..7d809d607 --- /dev/null +++ b/butterknife-runtime/src/test/java/butterknife/precompiled/Base.java @@ -0,0 +1,8 @@ +package butterknife.precompiled; + +import android.view.View; +import butterknife.BindView; + +public class Base { + @BindView(1) View thing; +} diff --git a/sample/base-library/build.gradle b/sample/base-library/build.gradle new file mode 100644 index 000000000..df6bacd6f --- /dev/null +++ b/sample/base-library/build.gradle @@ -0,0 +1,27 @@ +buildscript { + 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 +} diff --git a/sample/base-library/src/main/AndroidManifest.xml b/sample/base-library/src/main/AndroidManifest.xml new file mode 100644 index 000000000..72d0525b8 --- /dev/null +++ b/sample/base-library/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/sample/base-library/src/main/java/com/example/butterknife/baselibrary/BaseActivity.java b/sample/base-library/src/main/java/com/example/butterknife/baselibrary/BaseActivity.java new file mode 100644 index 000000000..e55e4ec2d --- /dev/null +++ b/sample/base-library/src/main/java/com/example/butterknife/baselibrary/BaseActivity.java @@ -0,0 +1,7 @@ +package com.example.butterknife.baselibrary; + +import android.app.Activity; +import butterknife.BindString; + +public class BaseActivity extends Activity { +} diff --git a/sample/library/src/main/res/values/strings.xml b/sample/base-library/src/main/res/values/strings.xml similarity index 88% rename from sample/library/src/main/res/values/strings.xml rename to sample/base-library/src/main/res/values/strings.xml index b73bb3432..9bc9c4630 100644 --- a/sample/library/src/main/res/values/strings.xml +++ b/sample/base-library/src/main/res/values/strings.xml @@ -2,4 +2,4 @@ Butter Knife - \ No newline at end of file + diff --git a/sample/library/build.gradle b/sample/library/build.gradle index 180017472..c4c7233fd 100644 --- a/sample/library/build.gradle +++ b/sample/library/build.gradle @@ -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 + annotationProcessor project(':butterknife-compiler') testImplementation deps.junit testImplementation deps.truth diff --git a/sample/library/src/main/java/com/example/butterknife/library/SimpleActivity.java b/sample/library/src/main/java/com/example/butterknife/library/SimpleActivity.java index 1d96194a6..63562cd8a 100644 --- a/sample/library/src/main/java/com/example/butterknife/library/SimpleActivity.java +++ b/sample/library/src/main/java/com/example/butterknife/library/SimpleActivity.java @@ -1,7 +1,6 @@ package com.example.butterknife.library; import android.annotation.SuppressLint; -import android.app.Activity; import android.os.Bundle; import androidx.annotation.NonNull; import android.view.View; @@ -16,11 +15,12 @@ import butterknife.OnClick; import butterknife.OnItemClick; import butterknife.OnLongClick; +import com.example.butterknife.baselibrary.BaseActivity; import java.util.List; import static android.widget.Toast.LENGTH_SHORT; -public class SimpleActivity extends Activity { +public class SimpleActivity extends BaseActivity { private static final ButterKnife.Action ALPHA_FADE = new ButterKnife.Action() { @Override public void apply(@NonNull View view, int index) { AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1); diff --git a/settings.gradle b/settings.gradle index 9ecaae160..50c52a6e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,6 +8,7 @@ include ':butterknife-reflect' include ':butterknife-runtime' //include ':sample:app' +//include ':sample:base-library' //include ':sample:library' rootProject.name = 'butterknife-parent'