Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
85ecc86
Error prone check added to check unbinded subscriptions
bangarharshit Jan 2, 2018
456ed87
Removed shadow plugin and other unused configs from build.gradle
bangarharshit Jan 5, 2018
db11fb2
Renamed package and module name to remove rx
bangarharshit Jan 5, 2018
872e71e
Removed to based api and inlined the builders
bangarharshit Jan 5, 2018
f876a67
Fixes in the checker - caching matcher
bangarharshit Jan 5, 2018
93df2cd
Updated link url to point to a shortcut to wiki/readmen on autodispose
bangarharshit Jan 6, 2018
47c35ce
Renamed test to AutoDisposeLeakCheck
bangarharshit Jan 7, 2018
cf07e46
Added logic to unset default classes
bangarharshit Jan 7, 2018
2547c2f
Updated link url
bangarharshit Jan 7, 2018
702b929
Used streams and lambdas to simplify code
bangarharshit Jan 7, 2018
e1703ff
Fixed javadoc and build errors
bangarharshit Jan 7, 2018
dd16a14
Fixed a bug when the invocation is not of the correct type
bangarharshit Jan 20, 2018
2e8bf18
Rebased master and fixed naming conventions
bangarharshit Jan 20, 2018
a9bf402
Changed plugin type to java library and tested it on sample project
bangarharshit Jan 20, 2018
a90e3d6
Fixed checkstyle and removed redundant params from tesT
bangarharshit Jan 20, 2018
d4b3f22
Updated readme to point to the wiki for error prone check. This secti…
bangarharshit Jan 20, 2018
9fd6dde
Reverted to compileOnly and clubbed all testImplementation together
bangarharshit Jan 21, 2018
5de25a5
Renamed tooling to static-analysis
bangarharshit Jan 21, 2018
c51f4db
Fixed readme
bangarharshit Jan 21, 2018
aca6ccf
Converted for loop for subscriber matcher to lambda and use method re…
bangarharshit Jan 21, 2018
2a9ba37
Newline in readme
bangarharshit Jan 21, 2018
8308c39
Added comments to explain filtering
bangarharshit Jan 21, 2018
6fd81f3
Fixed comments
bangarharshit Jan 21, 2018
7965a81
Fixed tags and fleshed out the comment
bangarharshit Jan 21, 2018
282291b
Renamed class to UseAutoDispose
bangarharshit Jan 21, 2018
64e5621
Fixed description
bangarharshit Jan 21, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down
7 changes: 7 additions & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: [
Expand Down Expand Up @@ -84,6 +90,7 @@ def test = [
]

ext.deps = [
"apt": apt,
"build": build,
"kotlin": kotlin,
"misc": misc,
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ include ':autodispose-kotlin'
include 'autodispose-rxlifecycle'
include ':sample'
include ':test-utils'
include ':static-analysis:autodispose-error-prone-checker'

31 changes: 31 additions & 0 deletions static-analysis/autodispose-error-prone-checker/build.gradle
Original file line number Diff line number Diff line change
@@ -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")
19 changes: 19 additions & 0 deletions static-analysis/autodispose-error-prone-checker/gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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:
* <pre><code>
* -XepOpt:AutoDisposeLeakCheck=com.bluelinelabs.conductor.Controller,android.app.Activity
* </code></pre>
*/
@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<MethodMatchers.MethodNameMatcher> AS_CALL_MATCHERS;
private static final ImmutableList<MethodMatchers.MethodNameMatcher> SUBSCRIBE_MATCHERS;
private static final String SUBSCRIBE = "subscribe";

private final Matcher<MethodInvocationTree> matcher;

public UseAutoDispose() {
this(ErrorProneFlags.empty());
}

public UseAutoDispose(ErrorProneFlags flags) {
Optional<ImmutableList<String>> inputClasses = flags.getList("AutoDisposeLeakCheck");
ImmutableList<String> defaultClassesWithLifecycle = new ImmutableList.Builder<String>()
.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<String> classesWithLifecycle = inputClasses.orElse(defaultClassesWithLifecycle);
matcher = matcher(classesWithLifecycle);
}

static {
AS_CALL_MATCHERS = new ImmutableList.Builder<MethodMatchers.MethodNameMatcher>()
.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<MethodMatchers.MethodNameMatcher>()
.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<ExpressionTree> METHOD_NAME_MATCHERS =
new Matcher<ExpressionTree>() {
@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<MethodInvocationTree> matcher(List<String> classesWithLifecycle) {
return (Matcher<MethodInvocationTree>) (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";
}
}
Original file line number Diff line number Diff line change
@@ -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 { }
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading