diff options
author | Eugene Susla <eugenesusla@google.com> | 2019-10-22 17:32:08 -0700 |
---|---|---|
committer | Eugene Susla <eugenesusla@google.com> | 2019-11-01 17:53:56 +0000 |
commit | 322e8b17721a6956e00407e8d431ceecbd245b5c (patch) | |
tree | 9fe5a43411281cf93c377a2d6fcd326a299ec97e | |
parent | 4a0b1750ef697bb4f329a3beffe88bf46e1e6385 (diff) |
[codegen] Support nested classes
Adds support for arbitrarily-nested @DataClasses
Only static ones are supported for now
See FileInfo for the main implementation piece
Fixes: 139833958
Test: . frameworks/base/tests/Codegen/runTest.sh
Change-Id: I31cd16969788c47003a7a15a3573a4bf623ab960
19 files changed, 1162 insertions, 363 deletions
diff --git a/tests/Codegen/runTest.sh b/tests/Codegen/runTest.sh index 929f122e261e..31ab6d2ba46a 100755 --- a/tests/Codegen/runTest.sh +++ b/tests/Codegen/runTest.sh @@ -17,6 +17,7 @@ else header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java && \ header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java && \ header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java && \ + header_and_eval codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java && \ ( cd $ANDROID_BUILD_TOP && header_and_eval mmma -j16 frameworks/base/tests/Codegen && \ diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java index 325c1c09dd8c..56ad217e6d19 100644 --- a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java @@ -32,7 +32,7 @@ public class HierrarchicalDataClassBase implements Parcelable { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -98,11 +98,15 @@ public class HierrarchicalDataClassBase implements Parcelable { }; @DataClass.Generated( - time = 1571258914826L, - codegenVersion = "1.0.9", + time = 1572630437620L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassBase.java", inputSignatures = "private int mBaseData\nclass HierrarchicalDataClassBase extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genSetters=true)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java index 6c92009f8533..59e07c471d63 100644 --- a/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java +++ b/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java @@ -46,7 +46,7 @@ public class HierrarchicalDataClassChild extends HierrarchicalDataClassBase { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -120,11 +120,15 @@ public class HierrarchicalDataClassChild extends HierrarchicalDataClassBase { }; @DataClass.Generated( - time = 1571258915848L, - codegenVersion = "1.0.9", + time = 1572630438646L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/HierrarchicalDataClassChild.java", inputSignatures = "private @android.annotation.NonNull java.lang.String mChildData\nclass HierrarchicalDataClassChild extends com.android.codegentest.HierrarchicalDataClassBase implements []\n@com.android.internal.util.DataClass(genParcelable=true, genConstructor=false, genSetters=true)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java b/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java index 36def8a8dfb1..3c00a3a63eca 100644 --- a/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java +++ b/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java @@ -52,7 +52,7 @@ public class ParcelAllTheThingsDataClass implements Parcelable { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -361,7 +361,7 @@ public class ParcelAllTheThingsDataClass implements Parcelable { } /** Builds the instance. This builder should not be touched after calling this! */ - public ParcelAllTheThingsDataClass build() { + public @NonNull ParcelAllTheThingsDataClass build() { checkNotUsed(); mBuilderFieldsSet |= 0x100; // Mark builder used @@ -410,11 +410,15 @@ public class ParcelAllTheThingsDataClass implements Parcelable { } @DataClass.Generated( - time = 1571258913802L, - codegenVersion = "1.0.9", + time = 1572630436563L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/ParcelAllTheThingsDataClass.java", inputSignatures = " @android.annotation.NonNull java.lang.String[] mStringArray\n @android.annotation.NonNull int[] mIntArray\n @android.annotation.NonNull java.util.List<java.lang.String> mStringList\n @android.annotation.NonNull java.util.Map<java.lang.String,com.android.codegentest.SampleWithCustomBuilder> mMap\n @android.annotation.NonNull java.util.Map<java.lang.String,java.lang.String> mStringMap\n @android.annotation.NonNull android.util.SparseArray<com.android.codegentest.SampleWithCustomBuilder> mSparseArray\n @android.annotation.NonNull android.util.SparseIntArray mSparseIntArray\n @java.lang.SuppressWarnings({\"WeakerAccess\"}) @android.annotation.Nullable java.lang.Boolean mNullableBoolean\nclass ParcelAllTheThingsDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genAidl=false, genToString=true)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClass.java b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java index c444d61a0fba..9765bdc261db 100644 --- a/tests/Codegen/src/com/android/codegentest/SampleDataClass.java +++ b/tests/Codegen/src/com/android/codegentest/SampleDataClass.java @@ -342,7 +342,7 @@ public final class SampleDataClass implements Parcelable { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -1430,7 +1430,7 @@ public final class SampleDataClass implements Parcelable { */ @SuppressWarnings("WeakerAccess") @DataClass.Generated.Member - public static class Builder { + public static final class Builder { private int mNum; private int mNum2; @@ -1793,7 +1793,7 @@ public final class SampleDataClass implements Parcelable { } /** Builds the instance. This builder should not be touched after calling this! */ - public SampleDataClass build() { + public @NonNull SampleDataClass build() { checkNotUsed(); mBuilderFieldsSet |= 0x100000; // Mark builder used @@ -1872,11 +1872,15 @@ public final class SampleDataClass implements Parcelable { } @DataClass.Generated( - time = 1571258911688L, - codegenVersion = "1.0.9", + time = 1572630434434L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleDataClass.java", inputSignatures = "public static final java.lang.String STATE_NAME_UNDEFINED\npublic static final java.lang.String STATE_NAME_ON\npublic static final java.lang.String STATE_NAME_OFF\npublic static final int STATE_UNDEFINED\npublic static final int STATE_ON\npublic static final int STATE_OFF\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @com.android.codegentest.SampleDataClass.RequestFlags int FLAG_AUGMENTED_REQUEST\nprivate int mNum\nprivate int mNum2\nprivate int mNum4\nprivate @android.annotation.Nullable java.lang.String mName\nprivate @android.annotation.NonNull java.lang.String mName2\nprivate @android.annotation.NonNull java.lang.String mName4\nprivate @android.annotation.Nullable android.view.accessibility.AccessibilityNodeInfo mOtherParcelable\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.codegentest.MyDateParcelling.class) @android.annotation.NonNull java.util.Date mDate\nprivate @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForPattern.class) @android.annotation.NonNull java.util.regex.Pattern mPattern\nprivate @android.annotation.NonNull java.util.List<android.net.LinkAddress> mLinkAddresses2\nprivate @com.android.internal.util.DataClass.PluralOf(\"linkAddress\") @android.annotation.NonNull java.util.ArrayList<android.net.LinkAddress> mLinkAddresses\nprivate @android.annotation.Nullable android.net.LinkAddress[] mLinkAddresses4\nprivate @com.android.codegentest.SampleDataClass.StateName @android.annotation.NonNull java.lang.String mStateName\nprivate @com.android.codegentest.SampleDataClass.RequestFlags int mFlags\nprivate @com.android.codegentest.SampleDataClass.State int mState\npublic @android.annotation.NonNull java.lang.CharSequence charSeq\nprivate final @android.annotation.Nullable android.net.LinkAddress[] mLinkAddresses5\nprivate transient android.net.LinkAddress[] mLinkAddresses6\ntransient int[] mTmpStorage\nprivate @android.annotation.StringRes int mStringRes\nprivate @android.annotation.IntRange(from=0L, to=6L) int mDayOfWeek\nprivate @android.annotation.Size(2L) @android.annotation.NonNull @com.android.internal.util.DataClass.Each @android.annotation.FloatRange(from=0.0) float[] mCoords\nprivate static java.lang.String defaultName4()\nprivate int[] lazyInitTmpStorage()\npublic android.net.LinkAddress[] getLinkAddresses4()\nprivate boolean patternEquals(java.util.regex.Pattern)\nprivate int patternHashCode()\nprivate void onConstructed()\npublic void dump(java.io.PrintWriter)\nclass SampleDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=true, genEqualsHashCode=true, genToString=true, genForEachField=true, genSetters=true)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java index c7a773530963..d13257743e21 100644 --- a/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java +++ b/tests/Codegen/src/com/android/codegentest/SampleDataClassTest.java @@ -243,6 +243,26 @@ public class SampleDataClassTest { assertEquals(instance.toString(), unparceledInstance.toString()); } + @Test + public void testNestedDataClasses_notMangledWhenParceled() { + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass("1"), + SampleWithNestedDataClasses.NestedDataClass.CREATOR); + + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass2("2"), + SampleWithNestedDataClasses.NestedDataClass2.CREATOR); + + assertEqualsAfterParcelling( + new SampleWithNestedDataClasses.NestedDataClass2.NestedDataClass3(3), + SampleWithNestedDataClasses.NestedDataClass2.NestedDataClass3.CREATOR); + } + + private static <T extends Parcelable> void assertEqualsAfterParcelling( + T p, Parcelable.Creator<T> creator) { + assertEquals(p, parcelAndUnparcel(p, creator)); + } + private static <T extends Parcelable> T parcelAndUnparcel( T original, Parcelable.Creator<T> creator) { Parcel p = Parcel.obtain(); diff --git a/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java b/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java index 55feae7200ea..c62f2b8f6acb 100644 --- a/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java +++ b/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java @@ -85,7 +85,7 @@ public class SampleWithCustomBuilder implements Parcelable { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -224,7 +224,7 @@ public class SampleWithCustomBuilder implements Parcelable { } /** Builds the instance. This builder should not be touched after calling this! */ - public SampleWithCustomBuilder build() { + public @NonNull SampleWithCustomBuilder build() { checkNotUsed(); mBuilderFieldsSet |= 0x8; // Mark builder used @@ -253,11 +253,15 @@ public class SampleWithCustomBuilder implements Parcelable { } @DataClass.Generated( - time = 1571258912752L, - codegenVersion = "1.0.9", + time = 1572630435484L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithCustomBuilder.java", inputSignatures = " long delayAmount\n @android.annotation.NonNull java.util.concurrent.TimeUnit delayUnit\n long creationTimestamp\nprivate static java.util.concurrent.TimeUnit unparcelDelayUnit(android.os.Parcel)\nprivate void parcelDelayUnit(android.os.Parcel,int)\nclass SampleWithCustomBuilder extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genAidl=false, genToString=true)\nabstract com.android.codegentest.SampleWithCustomBuilder.Builder setDelayAmount(long)\npublic abstract com.android.codegentest.SampleWithCustomBuilder.Builder setDelayUnit(java.util.concurrent.TimeUnit)\npublic com.android.codegentest.SampleWithCustomBuilder.Builder setDelay(long,java.util.concurrent.TimeUnit)\nclass BaseBuilder extends java.lang.Object implements []") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java b/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java new file mode 100644 index 000000000000..5b93b38161e3 --- /dev/null +++ b/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.codegentest; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; + +/** + * An example of deeply nested data classes + */ +public class SampleWithNestedDataClasses { + + int mFoo = 0; + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass implements Parcelable { + + @NonNull String mBar; + + + + // Code below generated by codegen v1.0.11. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass( + @NonNull String bar) { + this.mBar = bar; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBar); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull String getBar() { + return mBar; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass that = (NestedDataClass) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mBar, that.mBar); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Objects.hashCode(mBar); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeString(mBar); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + String bar = in.readString(); + + this.mBar = bar; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBar); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass> CREATOR + = new Parcelable.Creator<NestedDataClass>() { + @Override + public NestedDataClass[] newArray(int size) { + return new NestedDataClass[size]; + } + + @Override + public NestedDataClass createFromParcel(@NonNull Parcel in) { + return new NestedDataClass(in); + } + }; + + @DataClass.Generated( + time = 1572630440713L, + codegenVersion = "1.0.11", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull java.lang.String mBar\nclass NestedDataClass extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass2 implements Parcelable { + + @NonNull String mBaz; + + @DataClass(genEqualsHashCode = true) + public static class NestedDataClass3 implements Parcelable { + + @NonNull long mBaz2; + + + + // Code below generated by codegen v1.0.11. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass3( + @NonNull long baz2) { + this.mBaz2 = baz2; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz2); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull long getBaz2() { + return mBaz2; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass3 other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass3 that = (NestedDataClass3) o; + //noinspection PointlessBooleanExpression + return true + && mBaz2 == that.mBaz2; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + Long.hashCode(mBaz2); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeLong(mBaz2); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass3(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + long baz2 = in.readLong(); + + this.mBaz2 = baz2; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz2); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass3> CREATOR + = new Parcelable.Creator<NestedDataClass3>() { + @Override + public NestedDataClass3[] newArray(int size) { + return new NestedDataClass3[size]; + } + + @Override + public NestedDataClass3 createFromParcel(@NonNull Parcel in) { + return new NestedDataClass3(in); + } + }; + + @DataClass.Generated( + time = 1572630440724L, + codegenVersion = "1.0.11", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull long mBaz2\nclass NestedDataClass3 extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + + + // Code below generated by codegen v1.0.11. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @DataClass.Generated.Member + public NestedDataClass2( + @NonNull String baz) { + this.mBaz = baz; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull String getBaz() { + return mBaz; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@android.annotation.Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(NestedDataClass2 other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + NestedDataClass2 that = (NestedDataClass2) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mBaz, that.mBaz); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Objects.hashCode(mBaz); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeString(mBaz); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + protected NestedDataClass2(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + String baz = in.readString(); + + this.mBaz = baz; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mBaz); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<NestedDataClass2> CREATOR + = new Parcelable.Creator<NestedDataClass2>() { + @Override + public NestedDataClass2[] newArray(int size) { + return new NestedDataClass2[size]; + } + + @Override + public NestedDataClass2 createFromParcel(@NonNull Parcel in) { + return new NestedDataClass2(in); + } + }; + + @DataClass.Generated( + time = 1572630440729L, + codegenVersion = "1.0.11", + sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/SampleWithNestedDataClasses.java", + inputSignatures = " @android.annotation.NonNull java.lang.String mBaz\nclass NestedDataClass2 extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + + } + + void someCode() {} +} diff --git a/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java b/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java index b967f19f9f7e..2964451225e4 100644 --- a/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java +++ b/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java @@ -51,7 +51,7 @@ public class StaleDataclassDetectorFalsePositivesTest { - // Code below generated by codegen v1.0.9. + // Code below generated by codegen v1.0.11. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -65,11 +65,15 @@ public class StaleDataclassDetectorFalsePositivesTest { @DataClass.Generated( - time = 1571258916868L, - codegenVersion = "1.0.9", + time = 1572630439617L, + codegenVersion = "1.0.11", sourceFile = "frameworks/base/tests/Codegen/src/com/android/codegentest/StaleDataclassDetectorFalsePositivesTest.java", inputSignatures = "public @android.annotation.NonNull java.lang.String someMethod(int)\nclass StaleDataclassDetectorFalsePositivesTest extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false)") @Deprecated private void __metadata() {} + + //@formatter:on + // End of generated code + } diff --git a/tools/codegen/src/com/android/codegen/ClassInfo.kt b/tools/codegen/src/com/android/codegen/ClassInfo.kt index 92da9dab863b..bf95a2eb2193 100644 --- a/tools/codegen/src/com/android/codegen/ClassInfo.kt +++ b/tools/codegen/src/com/android/codegen/ClassInfo.kt @@ -1,47 +1,15 @@ package com.android.codegen -import com.github.javaparser.ParseProblemException -import com.github.javaparser.ParseResult -import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration -open class ClassInfo(val sourceLines: List<String>) { +open class ClassInfo(val classAst: ClassOrInterfaceDeclaration, val fileInfo: FileInfo) { - private val userSourceCode = (sourceLines + "}").joinToString("\n") - val fileAst: CompilationUnit = try { - JAVA_PARSER.parse(userSourceCode).throwIfFailed() - } catch (e: ParseProblemException) { - throw parseFailed(cause = e) - } - - fun <T> ParseResult<T>.throwIfFailed(): T { - if (problems.isNotEmpty()) { - throw parseFailed( - desc = this@throwIfFailed.problems.joinToString("\n"), - cause = this@throwIfFailed.problems.mapNotNull { it.cause.orElse(null) }.firstOrNull()) - } - return result.get() - } + val fileAst = fileInfo.fileAst - private fun parseFailed(cause: Throwable? = null, desc: String = ""): RuntimeException { - return RuntimeException("Failed to parse code:\n" + - userSourceCode - .lines() - .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" } - .joinToString("\n") + "\n$desc", - cause) - } - - val classAst = fileAst.types[0] as ClassOrInterfaceDeclaration val nestedClasses = classAst.members.filterIsInstance<ClassOrInterfaceDeclaration>() - val superInterfaces = (fileAst.types[0] as ClassOrInterfaceDeclaration) - .implementedTypes.map { it.asString() } - - val superClass = run { - val superClasses = (fileAst.types[0] as ClassOrInterfaceDeclaration).extendedTypes - if (superClasses.isNonEmpty) superClasses[0] else null - } + val superInterfaces = classAst.implementedTypes.map { it.asString() } + val superClass = classAst.extendedTypes.getOrNull(0) val ClassName = classAst.nameAsString private val genericArgsAst = classAst.typeParameters diff --git a/tools/codegen/src/com/android/codegen/ClassPrinter.kt b/tools/codegen/src/com/android/codegen/ClassPrinter.kt index bd72d9e7ec21..a4fd374d0c6e 100644 --- a/tools/codegen/src/com/android/codegen/ClassPrinter.kt +++ b/tools/codegen/src/com/android/codegen/ClassPrinter.kt @@ -11,36 +11,12 @@ import com.github.javaparser.ast.type.ClassOrInterfaceType * [ClassInfo] + utilities for printing out new class code with proper indentation and imports */ class ClassPrinter( - source: List<String>, - private val stringBuilder: StringBuilder, - var cliArgs: Array<String> -) : ClassInfo(source) { + classAst: ClassOrInterfaceDeclaration, + fileInfo: FileInfo +) : ClassInfo(classAst, fileInfo), Printer<ClassPrinter>, ImportsProvider { val GENERATED_MEMBER_HEADER by lazy { "@$GeneratedMember" } - // Imports - val NonNull by lazy { classRef("android.annotation.NonNull") } - val NonEmpty by lazy { classRef("android.annotation.NonEmpty") } - val Nullable by lazy { classRef("android.annotation.Nullable") } - val TextUtils by lazy { classRef("android.text.TextUtils") } - val LinkedHashMap by lazy { classRef("java.util.LinkedHashMap") } - val Collections by lazy { classRef("java.util.Collections") } - val Preconditions by lazy { classRef("com.android.internal.util.Preconditions") } - val ArrayList by lazy { classRef("java.util.ArrayList") } - val DataClass by lazy { classRef("com.android.internal.util.DataClass") } - val DataClassEnum by lazy { classRef("com.android.internal.util.DataClass.Enum") } - val ParcelWith by lazy { classRef("com.android.internal.util.DataClass.ParcelWith") } - val PluralOf by lazy { classRef("com.android.internal.util.DataClass.PluralOf") } - val Each by lazy { classRef("com.android.internal.util.DataClass.Each") } - val DataClassGenerated by lazy { classRef("com.android.internal.util.DataClass.Generated") } - val DataClassSuppressConstDefs by lazy { classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") } - val DataClassSuppress by lazy { classRef("com.android.internal.util.DataClass.Suppress") } - val GeneratedMember by lazy { classRef("com.android.internal.util.DataClass.Generated.Member") } - val Parcelling by lazy { classRef("com.android.internal.util.Parcelling") } - val Parcelable by lazy { classRef("android.os.Parcelable") } - val Parcel by lazy { classRef("android.os.Parcel") } - val UnsupportedAppUsage by lazy { classRef("android.annotation.UnsupportedAppUsage") } - init { val fieldsWithMissingNullablity = fields.filter { field -> !field.isPrimitive @@ -60,50 +36,61 @@ class ClassPrinter( } } - /** - * Optionally shortens a class reference if there's a corresponding import present - */ - fun classRef(fullName: String): String { - if (cliArgs.contains(FLAG_NO_FULL_QUALIFIERS)) { - return fullName.split(".").dropWhile { it[0].isLowerCase() }.joinToString(".") - } + val cliArgs get() = fileInfo.cliArgs - val pkg = fullName.substringBeforeLast(".") - val simpleName = fullName.substringAfterLast(".") - if (fileAst.imports.any { imprt -> - imprt.nameAsString == fullName - || (imprt.isAsterisk && imprt.nameAsString == pkg) - }) { - return simpleName - } else { - val outerClass = pkg.substringAfterLast(".", "") - if (outerClass.firstOrNull()?.isUpperCase() == true) { - return classRef(pkg) + "." + simpleName - } - } - return fullName - } + fun print() { + currentIndent = fileInfo.sourceLines + .find { "class $ClassName" in it }!! + .takeWhile { it.isWhitespace() } + .plus(INDENT_SINGLE) - /** @see classRef */ - inline fun <reified T : Any> classRef(): String { - return classRef(T::class.java.name) - } + +fileInfo.generatedWarning - /** @see classRef */ - fun memberRef(fullName: String): String { - val className = fullName.substringBeforeLast(".") - val methodName = fullName.substringAfterLast(".") - return if (fileAst.imports.any { - it.isStatic - && (it.nameAsString == fullName - || (it.isAsterisk && it.nameAsString == className)) - }) { - className.substringAfterLast(".") + "." + methodName - } else { - classRef(className) + "." + methodName + if (FeatureFlag.CONST_DEFS()) generateConstDefs() + + + if (FeatureFlag.CONSTRUCTOR()) { + generateConstructor("public") + } else if (FeatureFlag.BUILDER() + || FeatureFlag.COPY_CONSTRUCTOR() + || FeatureFlag.WITHERS()) { + generateConstructor("/* package-private */") } + if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() + + if (FeatureFlag.GETTERS()) generateGetters() + if (FeatureFlag.SETTERS()) generateSetters() + if (FeatureFlag.TO_STRING()) generateToString() + if (FeatureFlag.EQUALS_HASH_CODE()) generateEqualsHashcode() + + if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField() + + if (FeatureFlag.WITHERS()) generateWithers() + + if (FeatureFlag.PARCELABLE()) generateParcelable() + + if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon() + if (FeatureFlag.BUILDER()) generateBuilder() + + if (FeatureFlag.AIDL()) fileInfo.generateAidl() //TODO guard against nested classes requesting aidl + + generateMetadata(fileInfo.file) + + +""" + //@formatter:on + $GENERATED_END + + """ + + rmEmptyLine() } + override var currentIndent: String + get() = fileInfo.currentIndent + set(value) { fileInfo.currentIndent = value } + override val stringBuilder get() = fileInfo.stringBuilder + + val dataClassAnnotationFeatures = classAst.annotations .find { it.nameAsString == DataClass } ?.let { it as? NormalAnnotationExpr } @@ -143,7 +130,7 @@ class ClassPrinter( || onByDefault FeatureFlag.CONSTRUCTOR -> !FeatureFlag.BUILDER() FeatureFlag.PARCELABLE -> "Parcelable" in superInterfaces - FeatureFlag.AIDL -> FeatureFlag.PARCELABLE() + FeatureFlag.AIDL -> fileInfo.mainClass.nameAsString == ClassName && FeatureFlag.PARCELABLE() FeatureFlag.IMPLICIT_NONNULL -> fields.any { it.isNullable } && fields.none { "@$NonNull" in it.annotations } else -> onByDefault @@ -163,162 +150,7 @@ class ClassPrinter( } } - var currentIndent = INDENT_SINGLE - private set - - fun pushIndent() { - currentIndent += INDENT_SINGLE - } - - fun popIndent() { - currentIndent = currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length) - } - - fun backspace() = stringBuilder.setLength(stringBuilder.length - 1) - val lastChar get() = stringBuilder[stringBuilder.length - 1] - - private fun appendRaw(s: String) { - stringBuilder.append(s) - } - - fun append(s: String) { - if (s.isBlank() && s != "\n") { - appendRaw(s) - } else { - appendRaw(s.lines().map { line -> - if (line.startsWith(" *")) line else line.trimStart() - }.joinToString("\n$currentIndent")) - } - } - - fun appendSameLine(s: String) { - while (lastChar.isWhitespace() || lastChar.isNewline()) { - backspace() - } - appendRaw(s) - } - - fun rmEmptyLine() { - while (lastChar.isWhitespaceNonNewline()) backspace() - if (lastChar.isNewline()) backspace() - } - - /** - * Syntactic sugar for: - * ``` - * +"code()"; - * ``` - * to append the given string plus a newline - */ - operator fun String.unaryPlus() = append("$this\n") - - /** - * Syntactic sugar for: - * ``` - * !"code()"; - * ``` - * to append the given string without a newline - */ - operator fun String.not() = append(this) - - /** - * Syntactic sugar for: - * ``` - * ... { - * ... - * }+";" - * ``` - * to append a ';' on same line after a block, and a newline afterwards - */ - operator fun Unit.plus(s: String) { - appendSameLine(s) - +"" - } - - /** - * A multi-purpose syntactic sugar for appending the given string plus anything generated in - * the given [block], the latter with the appropriate deeper indent, - * and resetting the indent back to original at the end - * - * Usage examples: - * - * ``` - * "if (...)" { - * ... - * } - * ``` - * to append a corresponding if block appropriate indentation - * - * ``` - * "void foo(...)" { - * ... - * } - * ``` - * similar to the previous one, plus an extra empty line after the function body - * - * ``` - * "void foo(" { - * <args code> - * } - * ``` - * to use proper indentation for args code and close the bracket on same line at end - * - * ``` - * "..." { - * ... - * } - * to use the correct indentation for inner code, resetting it at the end - */ - inline operator fun String.invoke(block: ClassPrinter.() -> Unit) { - if (this == " {") { - appendSameLine(this) - } else { - append(this) - } - when { - endsWith("(") -> { - indentedBy(2, block) - appendSameLine(")") - } - endsWith("{") || endsWith(")") -> { - if (!endsWith("{")) appendSameLine(" {") - indentedBy(1, block) - +"}" - if ((endsWith(") {") || endsWith(")") || this == " {") - && !startsWith("synchronized") - && !startsWith("switch") - && !startsWith("if ") - && !contains(" else ") - && !contains("new ") - && !contains("return ")) { - +"" // extra line after function definitions - } - } - else -> indentedBy(2, block) - } - } - - inline fun indentedBy(level: Int, block: ClassPrinter.() -> Unit) { - append("\n") - level times { - append(INDENT_SINGLE) - pushIndent() - } - block() - level times { popIndent() } - rmEmptyLine() - +"" - } - inline fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) { - forEachApply { - b() - if (isLast) { - while (lastChar == ' ' || lastChar == '\n') backspace() - if (lastChar == ',') backspace() - } - } - } inline operator fun <R> invoke(f: ClassPrinter.() -> R): R = run(f) @@ -381,10 +213,10 @@ class ClassPrinter( BuilderClass = (builderFactoryOverride.type as ClassOrInterfaceType).nameAsString BuilderType = builderFactoryOverride.type.asString() } else { - val builderExtension = (fileAst.types - + classAst.childNodes.filterIsInstance(TypeDeclaration::class.java)).find { - it.nameAsString == CANONICAL_BUILDER_CLASS - } + val builderExtension = classAst + .childNodes + .filterIsInstance(TypeDeclaration::class.java) + .find { it.nameAsString == CANONICAL_BUILDER_CLASS } if (builderExtension != null) { BuilderClass = BASE_BUILDER_CLASS val tp = (builderExtension as ClassOrInterfaceDeclaration).typeParameters diff --git a/tools/codegen/src/com/android/codegen/FieldInfo.kt b/tools/codegen/src/com/android/codegen/FieldInfo.kt index 1a7fd6e241aa..ed35a1dfc599 100644 --- a/tools/codegen/src/com/android/codegen/FieldInfo.kt +++ b/tools/codegen/src/com/android/codegen/FieldInfo.kt @@ -1,5 +1,6 @@ package com.android.codegen +import com.github.javaparser.JavaParser import com.github.javaparser.ast.body.FieldDeclaration import com.github.javaparser.ast.expr.ClassExpr import com.github.javaparser.ast.expr.Name @@ -111,11 +112,12 @@ data class FieldInfo( val annotations by lazy { if (FieldClass in BUILTIN_SPECIAL_PARCELLINGS) { classPrinter { - fieldAst.addAnnotation(SingleMemberAnnotationExpr( - Name(ParcelWith), - ClassExpr(JAVA_PARSER - .parseClassOrInterfaceType("$Parcelling.BuiltIn.For$FieldClass") - .throwIfFailed()))) + fileInfo.apply { + fieldAst.addAnnotation(SingleMemberAnnotationExpr( + Name(ParcelWith), + ClassExpr(parseJava(JavaParser::parseClassOrInterfaceType, + "$Parcelling.BuiltIn.For$FieldClass")))) + } } } fieldAst.annotations.map { it.removeComment().toString() } diff --git a/tools/codegen/src/com/android/codegen/FileInfo.kt b/tools/codegen/src/com/android/codegen/FileInfo.kt new file mode 100644 index 000000000000..9c15fbf84223 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/FileInfo.kt @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.TypeDeclaration +import java.io.File + +/** + * File-level parsing & printing logic + * + * @see [main] entrypoint + */ +class FileInfo( + val sourceLines: List<String>, + val cliArgs: Array<String>, + val file: File) + : Printer<FileInfo>, ImportsProvider { + + override val fileAst: CompilationUnit + = parseJava(JavaParser::parse, sourceLines.joinToString("\n")) + + override val stringBuilder = StringBuilder() + override var currentIndent = INDENT_SINGLE + + + val generatedWarning = run { + val fileEscaped = file.absolutePath.replace( + System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP") + + """ + + + // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ $THIS_SCRIPT_LOCATION$CODEGEN_NAME ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + """ + } + private val generatedWarningNumPrecedingEmptyLines + = generatedWarning.lines().takeWhile { it.isBlank() }.size + + val classes = fileAst.types + .filterIsInstance<ClassOrInterfaceDeclaration>() + .flatMap { it.plusNested() } + .filterNot { it.isInterface } + + val mainClass = classes.find { it.nameAsString == file.nameWithoutExtension }!! + + // Parse stage 1 + val classBounds: List<ClassBounds> = classes.map { ast -> + ClassBounds(ast, fileInfo = this) + }.apply { + forEachApply { + if (ast.isNestedType) { + val parent = find { + it.name == (ast.parentNode.get()!! as TypeDeclaration<*>).nameAsString + }!! + parent.nested.add(this) + nestedIn = parent + } + } + } + + // Parse Stage 2 + var codeChunks = buildList<CodeChunk> { + val mainClassBounds = classBounds.find { it.nestedIn == null }!! + add(CodeChunk.FileHeader( + mainClassBounds.fileInfo.sourceLines.subList(0, mainClassBounds.range.start))) + add(CodeChunk.DataClass.parse(mainClassBounds)) + } + + // Output stage + fun main() { + codeChunks.forEach { print(it) } + } + + fun print(chunk: CodeChunk) { + when(chunk) { + is CodeChunk.GeneratedCode -> { + // Re-parse class code, discarding generated code and nested dataclasses + val ast = chunk.owner.chunks + .filter { + it.javaClass == CodeChunk.Code::class.java + || it.javaClass == CodeChunk.ClosingBrace::class.java + } + .flatMap { (it as CodeChunk.Code).lines } + .joinToString("\n") + .let { + parseJava(JavaParser::parseTypeDeclaration, it) + as ClassOrInterfaceDeclaration + } + + // Write new generated code + ClassPrinter(ast, fileInfo = this).print() + } + is CodeChunk.ClosingBrace -> { + // Special case - print closing brace with -1 indent + rmEmptyLine() + popIndent() + +"\n}" + } + // Print general code as-is + is CodeChunk.Code -> chunk.lines.forEach { stringBuilder.appendln(it) } + // Recursively render data classes + is CodeChunk.DataClass -> chunk.chunks.forEach { print(it) } + } + } + + /** + * Output of stage 1 of parsing a file: + * Recursively nested ranges of code line numbers containing nested classes + */ + data class ClassBounds( + val ast: ClassOrInterfaceDeclaration, + val fileInfo: FileInfo, + val name: String = ast.nameAsString, + val range: ClosedRange<Int> = ast.range.get()!!.let { rng -> rng.begin.line-1..rng.end.line-1 }, + val nested: MutableList<ClassBounds> = mutableListOf(), + var nestedIn: ClassBounds? = null) { + + val nestedDataClasses: List<ClassBounds> by lazy { + nested.filter { it.isDataclass }.sortedBy { it.range.start } + } + val isDataclass = ast.annotations.any { it.nameAsString.endsWith("DataClass") } + + val baseIndentLength = fileInfo.sourceLines.find { "class $name" in it }!!.takeWhile { it == ' ' }.length + val baseIndent = buildString { repeat(baseIndentLength) { append(' ') } } + + val sourceNoPrefix = fileInfo.sourceLines.drop(range.start) + val generatedCodeRange = sourceNoPrefix + .indexOfFirst { it.startsWith("$baseIndent$INDENT_SINGLE// $GENERATED_WARNING_PREFIX") } + .let { start -> + if (start < 0) { + null + } else { + var endInclusive = sourceNoPrefix.indexOfFirst { + it.startsWith("$baseIndent$INDENT_SINGLE$GENERATED_END") + } + if (endInclusive == -1) { + // Legacy generated code doesn't have end markers + endInclusive = fileInfo.sourceLines.size - 2 + } + IntRange( + range.start + start - fileInfo.generatedWarningNumPrecedingEmptyLines, + range.start + endInclusive) + } + } + + /** Debug info */ + override fun toString(): String { + return buildString { + appendln("class $name $range") + nested.forEach { + appendln(it) + } + appendln("end $name") + } + } + } + + /** + * Output of stage 2 of parsing a file + */ + sealed class CodeChunk { + /** General code */ + open class Code(val lines: List<String>): CodeChunk() {} + + /** Copyright + package + imports + main javadoc */ + class FileHeader(lines: List<String>): Code(lines) + + /** Code to be discarded and refreshed */ + open class GeneratedCode(lines: List<String>): Code(lines) { + lateinit var owner: DataClass + + class Placeholder: GeneratedCode(emptyList()) + } + + object ClosingBrace: Code(listOf("}")) + + data class DataClass( + val ast: ClassOrInterfaceDeclaration, + val chunks: List<CodeChunk>, + val generatedCode: GeneratedCode?): CodeChunk() { + + companion object { + fun parse(classBounds: ClassBounds): DataClass { + val initial = Code(lines = classBounds.fileInfo.sourceLines.subList( + fromIndex = classBounds.range.start, + toIndex = findLowerBound( + thisClass = classBounds, + nextNestedClass = classBounds.nestedDataClasses.getOrNull(0)))) + + val chunks = mutableListOf<CodeChunk>(initial) + + classBounds.nestedDataClasses.forEachSequentialPair { + nestedDataClass, nextNestedDataClass -> + chunks += DataClass.parse(nestedDataClass) + chunks += Code(lines = classBounds.fileInfo.sourceLines.subList( + fromIndex = nestedDataClass.range.endInclusive + 1, + toIndex = findLowerBound( + thisClass = classBounds, + nextNestedClass = nextNestedDataClass))) + } + + var generatedCode = classBounds.generatedCodeRange?.let { rng -> + GeneratedCode(classBounds.fileInfo.sourceLines.subList( + rng.start, rng.endInclusive+1)) + } + if (generatedCode != null) { + chunks += generatedCode + chunks += ClosingBrace + } else if (classBounds.isDataclass) { + + // Insert placeholder for generated code to be inserted for the 1st time + chunks.last = (chunks.last as Code) + .lines + .dropLastWhile { it.isBlank() } + .run { + if (last().dropWhile { it.isWhitespace() }.startsWith("}")) { + dropLast(1) + } else { + this + } + }.let { Code(it) } + generatedCode = GeneratedCode.Placeholder() + chunks += generatedCode + chunks += ClosingBrace + } else { + // Outer class may be not a @DataClass but contain ones + // so just skip generated code for them + } + + return DataClass(classBounds.ast, chunks, generatedCode).also { + generatedCode?.owner = it + } + } + + private fun findLowerBound(thisClass: ClassBounds, nextNestedClass: ClassBounds?): Int { + return nextNestedClass?.range?.start + ?: thisClass.generatedCodeRange?.start + ?: thisClass.range.endInclusive + 1 + } + } + } + + /** Debug info */ + fun summary(): String = when(this) { + is Code -> "${javaClass.simpleName}(${lines.size} lines): ${lines.getOrNull(0)?.take(70) ?: ""}..." + is DataClass -> "DataClass ${ast.nameAsString}:\n" + + chunks.joinToString("\n") { it.summary() } + + "\n//end ${ast.nameAsString}" + } + } + + private fun ClassOrInterfaceDeclaration.plusNested(): List<ClassOrInterfaceDeclaration> { + return mutableListOf<ClassOrInterfaceDeclaration>().apply { + add(this@plusNested) + childNodes.filterIsInstance<ClassOrInterfaceDeclaration>() + .flatMap { it.plusNested() } + .let { addAll(it) } + } + } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/Generators.kt b/tools/codegen/src/com/android/codegen/Generators.kt index bd32f9c6d9cd..c25d0c74f251 100644 --- a/tools/codegen/src/com/android/codegen/Generators.kt +++ b/tools/codegen/src/com/android/codegen/Generators.kt @@ -119,14 +119,14 @@ fun ClassPrinter.generateConstDef(consts: List<Pair<VariableDeclarator, FieldDec } } -fun ClassPrinter.generateAidl(javaFile: File) { - val aidl = File(javaFile.path.substringBeforeLast(".java") + ".aidl") +fun FileInfo.generateAidl() { + val aidl = File(file.path.substringBeforeLast(".java") + ".aidl") if (aidl.exists()) return aidl.writeText(buildString { sourceLines.dropLastWhile { !it.startsWith("package ") }.forEach { appendln(it) } - append("\nparcelable $ClassName;\n") + append("\nparcelable ${mainClass.nameAsString};\n") }) } diff --git a/tools/codegen/src/com/android/codegen/ImportsProvider.kt b/tools/codegen/src/com/android/codegen/ImportsProvider.kt new file mode 100644 index 000000000000..ba0a0318c843 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/ImportsProvider.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +import com.github.javaparser.ast.CompilationUnit + +/** + * Mixin for optionally shortening references based on existing imports + */ +interface ImportsProvider { + + abstract val fileAst: CompilationUnit + + val NonNull: String get() { return classRef("android.annotation.NonNull") } + val NonEmpty: String get() { return classRef("android.annotation.NonEmpty") } + val Nullable: String get() { return classRef("android.annotation.Nullable") } + val TextUtils: String get() { return classRef("android.text.TextUtils") } + val LinkedHashMap: String get() { return classRef("java.util.LinkedHashMap") } + val Collections: String get() { return classRef("java.util.Collections") } + val Preconditions: String get() { return classRef("com.android.internal.util.Preconditions") } + val ArrayList: String get() { return classRef("java.util.ArrayList") } + val DataClass: String get() { return classRef("com.android.internal.util.DataClass") } + val DataClassEnum: String get() { return classRef("com.android.internal.util.DataClass.Enum") } + val ParcelWith: String get() { return classRef("com.android.internal.util.DataClass.ParcelWith") } + val PluralOf: String get() { return classRef("com.android.internal.util.DataClass.PluralOf") } + val Each: String get() { return classRef("com.android.internal.util.DataClass.Each") } + val DataClassGenerated: String get() { return classRef("com.android.internal.util.DataClass.Generated") } + val DataClassSuppressConstDefs: String get() { return classRef("com.android.internal.util.DataClass.SuppressConstDefsGeneration") } + val DataClassSuppress: String get() { return classRef("com.android.internal.util.DataClass.Suppress") } + val GeneratedMember: String get() { return classRef("com.android.internal.util.DataClass.Generated.Member") } + val Parcelling: String get() { return classRef("com.android.internal.util.Parcelling") } + val Parcelable: String get() { return classRef("android.os.Parcelable") } + val Parcel: String get() { return classRef("android.os.Parcel") } + val UnsupportedAppUsage: String get() { return classRef("android.annotation.UnsupportedAppUsage") } + + /** + * Optionally shortens a class reference if there's a corresponding import present + */ + fun classRef(fullName: String): String { + + val pkg = fullName.substringBeforeLast(".") + val simpleName = fullName.substringAfterLast(".") + if (fileAst.imports.any { imprt -> + imprt.nameAsString == fullName + || (imprt.isAsterisk && imprt.nameAsString == pkg) + }) { + return simpleName + } else { + val outerClass = pkg.substringAfterLast(".", "") + if (outerClass.firstOrNull()?.isUpperCase() == true) { + return classRef(pkg) + "." + simpleName + } + } + return fullName + } + + /** @see classRef */ + fun memberRef(fullName: String): String { + val className = fullName.substringBeforeLast(".") + val methodName = fullName.substringAfterLast(".") + return if (fileAst.imports.any { + it.isStatic + && (it.nameAsString == fullName + || (it.isAsterisk && it.nameAsString == className)) + }) { + className.substringAfterLast(".") + "." + methodName + } else { + classRef(className) + "." + methodName + } + } +} + +/** @see classRef */ +inline fun <reified T : Any> ImportsProvider.classRef(): String { + return classRef(T::class.java.name) +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/Main.kt b/tools/codegen/src/com/android/codegen/Main.kt index ce83d3dc8e51..4b508d022991 100755 --- a/tools/codegen/src/com/android/codegen/Main.kt +++ b/tools/codegen/src/com/android/codegen/Main.kt @@ -6,6 +6,7 @@ import java.io.File const val THIS_SCRIPT_LOCATION = "" const val GENERATED_WARNING_PREFIX = "Code below generated by $CODEGEN_NAME" +const val GENERATED_END = "// End of generated code" const val INDENT_SINGLE = " " val PRIMITIVE_TYPES = listOf("byte", "short", "int", "long", "char", "float", "double", "boolean") @@ -115,81 +116,15 @@ fun main(args: Array<String>) { System.exit(0) } val file = File(args.last()).absoluteFile - val sourceLinesNoClosingBrace = file.readLines().dropLastWhile { + val sourceLisnesOriginal = file.readLines() + val sourceLinesNoClosingBrace = sourceLisnesOriginal.dropLastWhile { it.startsWith("}") || it.all(Char::isWhitespace) } val cliArgs = handleUpdateFlag(args, sourceLinesNoClosingBrace) - val sourceLinesAsIs = discardGeneratedCode(sourceLinesNoClosingBrace) - val sourceLines = sourceLinesAsIs - .filterNot { it.trim().startsWith("//") } - .map { it.trimEnd().dropWhile { it == '\n' } } - val stringBuilder = StringBuilder(sourceLinesAsIs.joinToString("\n")) - ClassPrinter(sourceLines, stringBuilder, cliArgs).run { - - val cliExecutable = "$THIS_SCRIPT_LOCATION$CODEGEN_NAME" - val fileEscaped = file.absolutePath.replace( - System.getenv("ANDROID_BUILD_TOP"), "\$ANDROID_BUILD_TOP") - - - +""" - - - - // $GENERATED_WARNING_PREFIX v$CODEGEN_VERSION. - // - // DO NOT MODIFY! - // CHECKSTYLE:OFF Generated code - // - // To regenerate run: - // $ $cliExecutable ${cliArgs.dropLast(1).joinToString("") { "$it " }}$fileEscaped - // - // To exclude the generated code from IntelliJ auto-formatting enable (one-time): - // Settings > Editor > Code Style > Formatter Control - //@formatter:off - - """ - - if (FeatureFlag.CONST_DEFS()) generateConstDefs() - - - if (FeatureFlag.CONSTRUCTOR()) { - generateConstructor("public") - } else if (FeatureFlag.BUILDER() - || FeatureFlag.COPY_CONSTRUCTOR() - || FeatureFlag.WITHERS()) { - generateConstructor("/* package-private */") - } - if (FeatureFlag.COPY_CONSTRUCTOR()) generateCopyConstructor() - - if (FeatureFlag.GETTERS()) generateGetters() - if (FeatureFlag.SETTERS()) generateSetters() - if (FeatureFlag.TO_STRING()) generateToString() - if (FeatureFlag.EQUALS_HASH_CODE()) generateEqualsHashcode() - - if (FeatureFlag.FOR_EACH_FIELD()) generateForEachField() - - if (FeatureFlag.WITHERS()) generateWithers() - - if (FeatureFlag.PARCELABLE()) generateParcelable() - - if (FeatureFlag.BUILDER() && FeatureFlag.BUILD_UPON()) generateBuildUpon() - if (FeatureFlag.BUILDER()) generateBuilder() - - if (FeatureFlag.AIDL()) generateAidl(file) - - generateMetadata(file) - - rmEmptyLine() - } - stringBuilder.append("\n}\n") - file.writeText(stringBuilder.toString().mapLines { trimEnd() }) -} - -internal fun discardGeneratedCode(sourceLinesNoClosingBrace: List<String>): List<String> { - return sourceLinesNoClosingBrace - .takeWhile { GENERATED_WARNING_PREFIX !in it } - .dropLastWhile(String::isBlank) + val fileInfo = FileInfo(sourceLisnesOriginal, cliArgs, file) + fileInfo.main() + file.writeText(fileInfo.stringBuilder.toString().mapLines { trimEnd() }) } private fun handleUpdateFlag(cliArgs: Array<String>, sourceLines: List<String>): Array<String> { diff --git a/tools/codegen/src/com/android/codegen/Printer.kt b/tools/codegen/src/com/android/codegen/Printer.kt new file mode 100644 index 000000000000..b30e3f68b307 --- /dev/null +++ b/tools/codegen/src/com/android/codegen/Printer.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.codegen + +/** + * Mixin for syntactic sugar around indent-aware printing into [stringBuilder] + */ +interface Printer<SELF: Printer<SELF>> { + + val stringBuilder: StringBuilder + + var currentIndent: String + + fun pushIndent() { + currentIndent += INDENT_SINGLE + } + + fun popIndent() { + currentIndent = if (currentIndent.length >= INDENT_SINGLE.length) { + currentIndent.substring(0, currentIndent.length - INDENT_SINGLE.length) + } else { + "" + } + } + + fun backspace() = stringBuilder.setLength(stringBuilder.length - 1) + val lastChar get() = stringBuilder[stringBuilder.length - 1] + + private fun appendRaw(s: String) { + stringBuilder.append(s) + } + + fun append(s: String) { + if (s.isBlank() && s != "\n") { + appendRaw(s) + } else { + appendRaw(s.lines().map { line -> + if (line.startsWith(" *")) line else line.trimStart() + }.joinToString("\n$currentIndent")) + } + } + + fun appendSameLine(s: String) { + while (lastChar.isWhitespace() || lastChar.isNewline()) { + backspace() + } + appendRaw(s) + } + + fun rmEmptyLine() { + while (lastChar.isWhitespaceNonNewline()) backspace() + if (lastChar.isNewline()) backspace() + } + + /** + * Syntactic sugar for: + * ``` + * +"code()"; + * ``` + * to append the given string plus a newline + */ + operator fun String.unaryPlus() = append("$this\n") + + /** + * Syntactic sugar for: + * ``` + * !"code()"; + * ``` + * to append the given string without a newline + */ + operator fun String.not() = append(this) + + /** + * Syntactic sugar for: + * ``` + * ... { + * ... + * }+";" + * ``` + * to append a ';' on same line after a block, and a newline afterwards + */ + operator fun Unit.plus(s: String) { + appendSameLine(s) + +"" + } + + /** + * A multi-purpose syntactic sugar for appending the given string plus anything generated in + * the given [block], the latter with the appropriate deeper indent, + * and resetting the indent back to original at the end + * + * Usage examples: + * + * ``` + * "if (...)" { + * ... + * } + * ``` + * to append a corresponding if block appropriate indentation + * + * ``` + * "void foo(...)" { + * ... + * } + * ``` + * similar to the previous one, plus an extra empty line after the function body + * + * ``` + * "void foo(" { + * <args code> + * } + * ``` + * to use proper indentation for args code and close the bracket on same line at end + * + * ``` + * "..." { + * ... + * } + * to use the correct indentation for inner code, resetting it at the end + */ + operator fun String.invoke(block: SELF.() -> Unit) { + if (this == " {") { + appendSameLine(this) + } else { + append(this) + } + when { + endsWith("(") -> { + indentedBy(2, block) + appendSameLine(")") + } + endsWith("{") || endsWith(")") -> { + if (!endsWith("{")) appendSameLine(" {") + indentedBy(1, block) + +"}" + if ((endsWith(") {") || endsWith(")") || this == " {") + && !startsWith("synchronized") + && !startsWith("switch") + && !startsWith("if ") + && !contains(" else ") + && !contains("new ") + && !contains("return ")) { + +"" // extra line after function definitions + } + } + else -> indentedBy(2, block) + } + } + + fun indentedBy(level: Int, block: SELF.() -> Unit) { + append("\n") + level times { + append(INDENT_SINGLE) + pushIndent() + } + (this as SELF).block() + level times { popIndent() } + rmEmptyLine() + +"" + } + + fun Iterable<FieldInfo>.forEachTrimmingTrailingComma(b: FieldInfo.() -> Unit) { + forEachApply { + b() + if (isLast) { + while (lastChar == ' ' || lastChar == '\n') backspace() + if (lastChar == ',') backspace() + } + } + } +}
\ No newline at end of file diff --git a/tools/codegen/src/com/android/codegen/Utils.kt b/tools/codegen/src/com/android/codegen/Utils.kt index e703397214eb..c19ae3b0b11f 100644 --- a/tools/codegen/src/com/android/codegen/Utils.kt +++ b/tools/codegen/src/com/android/codegen/Utils.kt @@ -1,5 +1,11 @@ package com.android.codegen +import com.github.javaparser.JavaParser +import com.github.javaparser.ParseProblemException +import com.github.javaparser.ParseResult +import com.github.javaparser.ast.Node +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.TypeDeclaration import com.github.javaparser.ast.expr.* import com.github.javaparser.ast.nodeTypes.NodeWithModifiers import java.time.Instant @@ -92,3 +98,49 @@ val AnnotationExpr.args: Map<String, Expression> get() = when (this) { is NormalAnnotationExpr -> pairs.map { it.name.asString() to it.value }.toMap() else -> throw IllegalArgumentException("Unknown annotation expression: $this") } + +val TypeDeclaration<*>.nestedTypes get() = childNodes.filterIsInstance<TypeDeclaration<*>>() +val TypeDeclaration<*>.nestedDataClasses get() + = nestedTypes.filterIsInstance<ClassOrInterfaceDeclaration>() + .filter { it.annotations.any { it.nameAsString.endsWith("DataClass") } } +val TypeDeclaration<*>.startLine get() = range.get()!!.begin.line + +inline fun <T> List<T>.forEachSequentialPair(action: (T, T?) -> Unit) { + forEachIndexed { index, t -> + action(t, getOrNull(index + 1)) + } +} + +fun <T: Node> parseJava(fn: JavaParser.(String) -> ParseResult<T>, source: String): T = try { + val parse = JAVA_PARSER.fn(source) + if (parse.problems.isNotEmpty()) { + throw parseFailed( + source, + desc = parse.problems.joinToString("\n"), + cause = parse.problems.mapNotNull { it.cause.orElse(null) }.firstOrNull()) + } + parse.result.get() +} catch (e: ParseProblemException) { + throw parseFailed(source, cause = e) +} + +private fun parseFailed(source: String, cause: Throwable? = null, desc: String = ""): RuntimeException { + return RuntimeException("Failed to parse code:\n" + + source + .lines() + .mapIndexed { lnNum, ln -> "/*$lnNum*/$ln" } + .joinToString("\n") + "\n$desc", + cause) +} + +var <T> MutableList<T>.last + get() = last() + set(value) { + if (isEmpty()) { + add(value) + } else { + this[size - 1] = value + } + } + +inline fun <T> buildList(init: MutableList<T>.() -> Unit) = mutableListOf<T>().apply(init)
\ No newline at end of file diff --git a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt index 7fe21c7aab3e..51faa49a86cc 100644 --- a/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt +++ b/tools/processors/staledataclass/src/android/processor/staledataclass/StaleDataclassProcessor.kt @@ -208,7 +208,16 @@ class StaleDataclassProcessor: AbstractProcessor() { val refreshCmd = if (file != null) { "$CODEGEN_NAME $file" } else { - "find \$ANDROID_BUILD_TOP -path */${clazz.replace('.', '/')}.java -exec $CODEGEN_NAME {} \\;" + var gotTopLevelCalssName = false + val filePath = clazz.split(".") + .takeWhile { word -> + if (!gotTopLevelCalssName && word[0].isUpperCase()) { + gotTopLevelCalssName = true + return@takeWhile true + } + !gotTopLevelCalssName + }.joinToString("/") + "find \$ANDROID_BUILD_TOP -path */$filePath.java -exec $CODEGEN_NAME {} \\;" } } |