diff --git a/README.md b/README.md index ea6680126..e41995853 100755 --- a/README.md +++ b/README.md @@ -220,6 +220,13 @@ This pattern is *sort of* possible in RxJava 1, but only on `Subscriber` (via `o `CompletableObserver` (which matches RxJava 2's API). We are aggressively migrating our internal code to RxJava 2, and do not plan to try to backport this to RxJava 1. +Static analysis +------- + +There is an optional error-prone checker you can use to enforce use of AutoDispose. +Integration steps and more details +can be found on the [wiki](https://github.com/uber/AutoDispose/wiki/Error-Prone-Checker) + Download -------- diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 4af5a9d4a..d9c29e333 100755 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -24,6 +24,10 @@ def versions = [ support: '26.1.0' ] +def apt = [ + autoService : "com.google.auto.service:auto-service:1.0-rc4", +] + def build = [ buildToolsVersion: '26.0.2', compileSdkVersion: 26, @@ -33,6 +37,8 @@ def build = [ checkerFramework: 'org.checkerframework:dataflow:2.3.0', errorProne: "com.google.errorprone:error_prone_core:${versions.errorProne}", + errorProneCheckApi: "com.google.errorprone:error_prone_check_api:${versions.errorProne}", + errorProneTestHelpers: "com.google.errorprone:error_prone_test_helpers:${versions.errorProne}", nullAway: 'com.uber.nullaway:nullaway:0.2.0', repositories: [ @@ -84,6 +90,7 @@ def test = [ ] ext.deps = [ + "apt": apt, "build": build, "kotlin": kotlin, "misc": misc, diff --git a/settings.gradle b/settings.gradle index a74b395c8..f18299270 100755 --- a/settings.gradle +++ b/settings.gradle @@ -26,4 +26,5 @@ include ':autodispose-kotlin' include 'autodispose-rxlifecycle' include ':sample' include ':test-utils' +include ':static-analysis:autodispose-error-prone-checker' diff --git a/static-analysis/autodispose-error-prone-checker/build.gradle b/static-analysis/autodispose-error-prone-checker/build.gradle new file mode 100644 index 000000000..bbec49884 --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/build.gradle @@ -0,0 +1,31 @@ +plugins { + id "net.ltgt.errorprone" version "0.0.13" + id "java-library" +} + +// we use this config to get the path of the JDK 9 javac jar, to +// stick it in the bootclasspath when running tests +configurations.maybeCreate("epJavac") + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compileOnly deps.apt.autoService + compileOnly deps.build.errorProneCheckApi + + testImplementation(deps.build.errorProneTestHelpers) { + exclude group: "junit", module: "junit" + } + testImplementation deps.rx.java + testImplementation deps.test.junit + testImplementation project(':autodispose') + + epJavac deps.build.errorProneCheckApi +} + +test { + jvmArgs "-Xbootclasspath/p:${configurations.epJavac.asPath}" +} + +apply from: rootProject.file("gradle/gradle-mvn-push.gradle") diff --git a/static-analysis/autodispose-error-prone-checker/gradle.properties b/static-analysis/autodispose-error-prone-checker/gradle.properties new file mode 100644 index 000000000..e254e1781 --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/gradle.properties @@ -0,0 +1,19 @@ +# +# Copyright (C) 2017. Uber Technologies +# +# 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. +# + +POM_NAME=AutoDispose Error-Prone Checker +POM_ARTIFACT_ID=autodispose-error-prone-checker +POM_PACKAGING=jar diff --git a/static-analysis/autodispose-error-prone-checker/src/main/java/com/uber/autodispose/error/prone/checker/UseAutoDispose.java b/static-analysis/autodispose-error-prone-checker/src/main/java/com/uber/autodispose/error/prone/checker/UseAutoDispose.java new file mode 100644 index 000000000..1ced255e3 --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/main/java/com/uber/autodispose/error/prone/checker/UseAutoDispose.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.ErrorProneFlags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.google.errorprone.matchers.method.MethodMatchers; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.tools.javac.code.Type; +import java.util.List; +import java.util.Optional; + +import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; +import static com.google.errorprone.BugPattern.StandardTags.CONCURRENCY; +import static com.google.errorprone.matchers.Matchers.instanceMethod; + +/** + * Checker for subscriptions not binding to lifecycle in components with lifecycle. + * Use -XepOpt:AutoDisposeLeakCheck flag to add support for custom components with lifecycle. + * The sample configuration for Conductor: + *

+ *   -XepOpt:AutoDisposeLeakCheck=com.bluelinelabs.conductor.Controller,android.app.Activity
+ * 
+ */ +@AutoService(BugChecker.class) +@BugPattern( + name = "UseAutoDispose", + summary = "Always apply an AutoDispose scope before " + + "subscribing within defined scoped elements.", + tags = CONCURRENCY, + severity = ERROR +) +public final class UseAutoDispose extends BugChecker + implements MethodInvocationTreeMatcher { + + private static final String AS = "as"; + private static final ImmutableList AS_CALL_MATCHERS; + private static final ImmutableList SUBSCRIBE_MATCHERS; + private static final String SUBSCRIBE = "subscribe"; + + private final Matcher matcher; + + public UseAutoDispose() { + this(ErrorProneFlags.empty()); + } + + public UseAutoDispose(ErrorProneFlags flags) { + Optional> inputClasses = flags.getList("AutoDisposeLeakCheck"); + ImmutableList defaultClassesWithLifecycle = new ImmutableList.Builder() + .add("android.app.Activity") + .add("android.app.Fragment") + .add("com.uber.autodispose.LifecycleScopeProvider") + .add("android.support.v4.app.Fragment") + .add("android.arch.lifecycle.LifecycleOwner") + .add("com.uber.autodispose.ScopeProvider") + .build(); + ImmutableList classesWithLifecycle = inputClasses.orElse(defaultClassesWithLifecycle); + matcher = matcher(classesWithLifecycle); + } + + static { + AS_CALL_MATCHERS = new ImmutableList.Builder() + .add(instanceMethod().onDescendantOf("io.reactivex.Single").named(AS)) + .add(instanceMethod().onDescendantOf("io.reactivex.Observable").named(AS)) + .add(instanceMethod().onDescendantOf("io.reactivex.Completable").named(AS)) + .add(instanceMethod().onDescendantOf("io.reactivex.Flowable").named(AS)) + .add(instanceMethod().onDescendantOf("io.reactivex.Maybe").named(AS)) + .add(instanceMethod().onDescendantOf("io.reactivex.parallel.ParallelFlowable").named(AS)) + .build(); + + SUBSCRIBE_MATCHERS = new ImmutableList.Builder() + .add(instanceMethod().onDescendantOf("io.reactivex.Single").named(SUBSCRIBE)) + .add(instanceMethod().onDescendantOf("io.reactivex.Observable").named(SUBSCRIBE)) + .add(instanceMethod().onDescendantOf("io.reactivex.Completable").named(SUBSCRIBE)) + .add(instanceMethod().onDescendantOf("io.reactivex.Flowable").named(SUBSCRIBE)) + .add(instanceMethod().onDescendantOf("io.reactivex.Maybe").named(SUBSCRIBE)) + .add(instanceMethod() + .onDescendantOf("io.reactivex.parallel.ParallelFlowable") + .named(SUBSCRIBE)) + .build(); + } + + /** + * Matcher to find the as operator in the observable chain. + */ + private static final Matcher METHOD_NAME_MATCHERS = + new Matcher() { + @Override + public boolean matches(ExpressionTree tree, VisitorState state) { + if (!(tree instanceof MethodInvocationTree)) { + return false; + } + MethodInvocationTree invTree = (MethodInvocationTree) tree; + + ExpressionTree methodSelectTree = invTree.getMethodSelect(); + + // MemberSelectTree is used only for member access expression. + // This is not true for scenarios such as calling super constructor. + if (!(methodSelectTree instanceof MemberSelectTree)) { + return false; + } + + final MemberSelectTree memberTree = (MemberSelectTree) methodSelectTree; + if (!memberTree.getIdentifier().contentEquals(AS)) { + return false; + } + + return AS_CALL_MATCHERS + .stream() + .filter(methodNameMatcher -> methodNameMatcher.matches(invTree, state)) + .map(methodNameMatcher -> { + ExpressionTree arg = invTree.getArguments().get(0); + final Type scoper = state + .getTypeFromString("com.uber.autodispose.AutoDisposeConverter"); + return ASTHelpers.isSubtype(ASTHelpers.getType(arg), scoper, state); + }) + // Filtering the method invocation with name as + // and has an argument of type AutoDisposeConverter. + .filter(Boolean::booleanValue) + .findFirst() + .orElse(false); + } + }; + + private static Matcher matcher(List classesWithLifecycle) { + return (Matcher) (tree, state) -> { + + boolean matchFound = false; + ExpressionTree methodSelectTree = tree.getMethodSelect(); + + // MemberSelectTree is used only for member access expression. + // This is not true for scenarios such as calling super constructor. + if (!(methodSelectTree instanceof MemberSelectTree)) { + return false; + } + + final MemberSelectTree memberTree = (MemberSelectTree) tree.getMethodSelect(); + if (!memberTree.getIdentifier().contentEquals(SUBSCRIBE)) { + return false; + } + + matchFound = SUBSCRIBE_MATCHERS + .stream() + .map(methodNameMatcher -> methodNameMatcher.matches(tree, state)) + .filter(Boolean::booleanValue) // Filtering the method invocation with name subscribe + .findFirst() + .orElse(false); + + if (!matchFound) { + return false; + } + + ClassTree enclosingClass = + ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class); + Type.ClassType enclosingClassType = ASTHelpers.getType(enclosingClass); + + return classesWithLifecycle + .stream() + .map(classWithLifecycle -> { + Type lifecycleType = state.getTypeFromString(classWithLifecycle); + return ASTHelpers.isSubtype(enclosingClassType, lifecycleType, state) + && !METHOD_NAME_MATCHERS.matches(memberTree.getExpression(), state); + }) + // Filtering the method invocation which is a + // subtype of one of the classes with lifecycle and name as + .filter(Boolean::booleanValue) + .findFirst() + .orElse(false); + }; + } + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (matcher.matches(tree, state)) { + return buildDescription(tree).build(); + } else { + return Description.NO_MATCH; + } + } + + @Override + public String linkUrl() { + return "https://github.com/uber/AutoDispose/wiki/Error-Prone-Checker"; + } +} diff --git a/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/ComponentWithLifeCycle.java b/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/ComponentWithLifeCycle.java new file mode 100644 index 000000000..880fdd9b5 --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/ComponentWithLifeCycle.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +public class ComponentWithLifeCycle { } diff --git a/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/UseAutoDisposeTest.java b/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/UseAutoDisposeTest.java new file mode 100644 index 000000000..add55cbbe --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/test/java/com/uber/autodispose/error/prone/checker/UseAutoDisposeTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +import com.google.errorprone.CompilationTestHelper; +import java.util.Collections; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class UseAutoDisposeTest { + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = + CompilationTestHelper + .newInstance(UseAutoDispose.class, getClass()); + } + + @Test + public void test_autodisposePositiveCasesWithDefaultClass() { + compilationHelper + .addSourceFile("UseAutoDisposeDefaultClassPositiveCases.java") + .doTest(); + } + + @Test + public void test_autodisposePositiveCaseswithCustomClass() { + compilationHelper.setArgs( + Collections.singletonList("-XepOpt:AutoDisposeLeakCheck" + + "=com.uber.autodispose.error.prone.checker.ComponentWithLifeCycle")); + compilationHelper + .addSourceFile("UseAutoDisposeCustomClassPositiveCases.java") + .doTest(); + } + + @Test + public void test_autodisposeNegativeCases() { + compilationHelper + .addSourceFile("UseAutoDisposeNegativeCases.java") + .doTest(); + } +} diff --git a/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeCustomClassPositiveCases.java b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeCustomClassPositiveCases.java new file mode 100644 index 000000000..b3b12e82a --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeCustomClassPositiveCases.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +import com.uber.autodispose.AutoDispose; +import com.uber.autodispose.ObservableScoper; +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import org.reactivestreams.Subscriber; + +public class UseAutoDisposeCustomClassPositiveCases extends ComponentWithLifeCycle { + public void observable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Observable.empty().subscribe(); + } + + public void single_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Single.just(true).subscribe(); + } + + public void completable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Completable.complete().subscribe(); + } + + public void maybe_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Maybe.empty().subscribe(); + } + + public void flowable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Flowable.empty().subscribe(); + } + + public void parallelFlowable_subscribeWithoutAutoDispose() { + Subscriber[] subscribers = new Subscriber[] {}; + Flowable.just(1, 2) + .parallel(2) + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + .subscribe(subscribers); + } +} diff --git a/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeDefaultClassPositiveCases.java b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeDefaultClassPositiveCases.java new file mode 100644 index 000000000..794759b2d --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeDefaultClassPositiveCases.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.LifecycleScopeProvider; +import com.uber.autodispose.TestLifecycleScopeProvider; + +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.annotations.CheckReturnValue; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; +import javax.annotation.Nullable; +import org.reactivestreams.Subscriber; + +/** + * Cases that don't use autodispose and should fail the {@link UseAutoDispose} check. + */ +public class UseAutoDisposeDefaultClassPositiveCases + implements LifecycleScopeProvider { + + private final BehaviorSubject lifecycleSubject = + BehaviorSubject.create(); + + /** + * @return a sequence of lifecycle events. + */ + @CheckReturnValue public Observable lifecycle() { + return lifecycleSubject.hide(); + } + + /** + * @return a sequence of lifecycle events. It's recommended to back this with a static instance to + * avoid unnecessary object allocation. + */ + @CheckReturnValue + public Function correspondingEvents() { + return new Function() { + @Override public TestLifecycleScopeProvider.TestLifecycle apply( + TestLifecycleScopeProvider.TestLifecycle testLifecycle) { + switch (testLifecycle) { + case STARTED: + return TestLifecycleScopeProvider.TestLifecycle.STOPPED; + case STOPPED: + throw new LifecycleEndedException(); + default: + throw new IllegalStateException("Unknown lifecycle event."); + } + } + }; + } + + /** + * @return the last seen lifecycle event, or {@code null} if none. + */ + @Nullable public TestLifecycleScopeProvider.TestLifecycle peekLifecycle() { + return lifecycleSubject.getValue(); + } + + public void observable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Observable.empty().subscribe(); + } + + public void single_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Single.just(true).subscribe(); + } + + public void completable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Completable.complete().subscribe(); + } + + public void maybe_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Maybe.empty().subscribe(); + } + + public void flowable_subscribeWithoutAutoDispose() { + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + Flowable.empty().subscribe(); + } + + public void parallelFlowable_subscribeWithoutAutoDispose() { + Subscriber[] subscribers = new Subscriber[] {}; + Flowable.just(1, 2) + .parallel(2) + // BUG: Diagnostic contains: Always apply an AutoDispose scope before subscribing within defined scoped elements. + .subscribe(subscribers); + } +} diff --git a/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeNegativeCases.java b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeNegativeCases.java new file mode 100644 index 000000000..1f5a598c7 --- /dev/null +++ b/static-analysis/autodispose-error-prone-checker/src/test/resources/com/uber/autodispose/error/prone/checker/UseAutoDisposeNegativeCases.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017. Uber Technologies + * + * 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.uber.autodispose.error.prone.checker; + +import com.uber.autodispose.AutoDispose; +import com.uber.autodispose.LifecycleEndedException; +import com.uber.autodispose.LifecycleScopeProvider; +import com.uber.autodispose.TestLifecycleScopeProvider; + +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.annotations.CheckReturnValue; +import io.reactivex.functions.Function; +import io.reactivex.subjects.BehaviorSubject; +import javax.annotation.Nullable; +import org.reactivestreams.Subscriber; + +/** + * Cases that use {@link AutoDispose} and should not fail the {@link UseAutoDispose} check. + */ +public class UseAutoDisposeNegativeCases + implements LifecycleScopeProvider { + + private final BehaviorSubject lifecycleSubject = + BehaviorSubject.create(); + + /** + * @return a sequence of lifecycle events. + */ + @CheckReturnValue public Observable lifecycle() { + return lifecycleSubject.hide(); + } + + /** + * @return a sequence of lifecycle events. It's recommended to back this with a static instance to + * avoid unnecessary object allocation. + */ + @CheckReturnValue + public Function correspondingEvents() { + return new Function() { + @Override public TestLifecycleScopeProvider.TestLifecycle apply( + TestLifecycleScopeProvider.TestLifecycle testLifecycle) { + switch (testLifecycle) { + case STARTED: + return TestLifecycleScopeProvider.TestLifecycle.STOPPED; + case STOPPED: + throw new LifecycleEndedException(); + default: + throw new IllegalStateException("Unknown lifecycle event."); + } + } + }; + } + + /** + * @return the last seen lifecycle event, or {@code null} if none. + */ + @Nullable public TestLifecycleScopeProvider.TestLifecycle peekLifecycle() { + return lifecycleSubject.getValue(); + } + + public void observable_subscribeWithAutoDispose() { + Observable.just(1).as(AutoDispose.autoDisposable(this)).subscribe(); + } + + public void single_subscribeWithAutoDispose() { + Single.just(true).as(AutoDispose.autoDisposable(this)).subscribe(); + } + + public void completable_subscribeWithAutoDispose() { + Completable.complete().as(AutoDispose.autoDisposable(this)).subscribe(); + } + + public void maybe_subscribeWithAutoDispose() { + Maybe.just(1).as(AutoDispose.autoDisposable(this)).subscribe(); + } + + public void flowable_subscribeWithAutoDispose() { + Flowable.just(1).as(AutoDispose.autoDisposable(this)).subscribe(); + } + + public void parallelFlowable_subscribeWithAutoDispose() { + Subscriber[] subscribers = new Subscriber[] {}; + Flowable.just(1, 2) + .parallel(2) + .as(AutoDispose.autoDisposable(this)) + .subscribe(subscribers); + } +}