RxJava2でonErrorをsubscribeしてないのを検知するCustom Lintを作る
深夜に眠れなかったので、男もすなるCustom Lint作りというものをやってみようかと。
RxJava2でonErrorをsubscribeしてないものを検知するというものを作りました。
きっと探せばあるんでしょうが、始める題材としては良さそうだと思ったのでここはあえて探さずに。
参考
下記を参考に作りました
関係ない話な上たまたまなんですが、上記qiitaのhotchemiさんもamyu君も今同じフロアで働いてたりします。
作り方
上記qiitaにも載ってますが、ここでも。下記手順で作ります。
- build.gradleにdependeicies追加
- Detector及びIssueを作成
- Registoryを作成
- Test作成
一つ一つ説明します。
build.gradleにdependeicies追加
build.gradleのdependeiciesに下記を追加しておきます。
compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'
compile 'com.android.tools.lint:lint:24.3.1'
testCompile 'junit:junit:4.11'
testCompile 'com.android.tools.lint:lint-tests:24.3.1'
testCompile 'com.android.tools:testutils:24.3.1'
Detector及びIssueを作成
Observable, Flowable, Maybe, Single, Completable が subscribe された時にsusbscribe() もしくは subscribe(onNext) の時はNGとします。
まずIssueというものを作っておきます。このlintの説明みたいなものです。
public static final Issue ISSUE = Issue.create(
"RxJava2SubscribeOnError",
"not subscribe onError",
"must subscribe onError",
Category.PERFORMANCE,
7,
Severity.ERROR,
new Implementation(
RxJava2SubscribeOnErrorDetector.class,
Scope.JAVA_FILE_SCOPE
)
);
そしてDetectorを作っていきます。
public class RxJava2SubscribeOnErrorDetector extends Detector implements Detector.JavaScanner {
:
private static final String CLS_FLOWABLE = "io.reactivex.Flowable";
private static final String CLS_OBSERAVLE = "io.reactivex.Observable";
private static final String CLS_SINGLE = "io.reactivex.Single";
private static final String CLS_COMPLETABLE = "io.reactivex.Completable";
private static final String CLS_OBJECT = "java.lang.Object";
private static final String CLS_CONSUMER = "io.reactivex.functions.Consumer";
private static final String CLS_ACTION = "io.reactivex.functions.Action";
private static final String CLS_CONSUMER_SIMPLE = "Consumer";
private static final String CLS_ACTION_SIMPLE = "Action";
private static final String ANONYMOUS_NEW_CONSUMER = "new io.reactivex.functions.Consumer(){}";
private static final String ANONYMOUS_NEW_ACTION = "new io.reactivex.functions.Action(){}";
private static final String METHOD_SUBSCRIBE = "subscribe";
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableConstructorTypes() {
return Arrays.asList(CLS_FLOWABLE, CLS_OBSERAVLE, CLS_SINGLE, CLS_COMPLETABLE);
}
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList(METHOD_SUBSCRIBE);
}
@Override
public void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation node
) {
boolean isNG = false;
StrictListAccessor<Expression, MethodInvocation> expressions = node.astArguments();
int size = expressions.size();
if (size == 0) {
// Any.subscribe() -> NG
isNG = true;
} else if (size == 1) {
// Any.subscribe(Consumer<T> onNext) -> NG
Expression expression = expressions.first();
JavaParser.ResolvedClass param1Class = context.getType(expression).getTypeClass();
while (!param1Class.getName().equals(CLS_OBJECT)) {
if (param1Class.getName().startsWith(CLS_CONSUMER)
|| param1Class.getName().startsWith(CLS_CONSUMER_SIMPLE)
|| param1Class.getName().startsWith(ANONYMOUS_NEW_CONSUMER)
|| param1Class.getName().startsWith(CLS_ACTION)
|| param1Class.getName().startsWith(CLS_ACTION_SIMPLE)
|| param1Class.getName().startsWith(ANONYMOUS_NEW_ACTION)) {
isNG = true;
break;
}
param1Class = param1Class.getSuperClass();
}
}
if (isNG) {
context.report(ISSUE, node, context.getLocation(node), "you must subscribe onError");
}
}
}
getApplicableConstructorTypesで上記の5クラスに絞るgetApplicableMethodNamesでsubscribeメソッドに絞るvisitMethodにて、先の条件に当てはまるものをNGとしてcontext.reportする
ということをやっています。
Registoryを作成
こういうものを作って
public class LintIssueRegistry extends IssueRegistry {
public LintIssueRegistry() {
}
@Override
public List<Issue> getIssues() {
return Collections.singletonList(RxJava2SubscribeOnErrorDetector.ISSUE);
}
}
build.gradleに下記を追記しておきます。
jar {
manifest {
attributes("Lint-Registry": "com.kgmshin.lint.LintIssueRegistry")
}
}
Test作成
LintDetectorTestを継承してテストを作成します。
public class RxJava2SubscribeOnErrorDetectorTest extends LintDetectorTest {
:
@Override
protected Detector getDetector() {
return new RxJava2SubscribeOnErrorDetector();
}
@Override
protected List<Issue> getIssues() {
return ImmutableList.of(RxJava2SubscribeOnErrorDetector.ISSUE);
}
@Test
public void testSingleSubscribeNoParam() throws Exception {
@Language("JAVA") String foo = "" +
PACKAGE +
IMPORT +
"public class Foo {\n" +
"public void test() {\n" +
"Single.just(\"test\").subscribe()" +
"}\n" +
"}";
String result = lintProject(java(SOURCE_PATH + "Foo.java", foo));
assertEquals(
"src/com/kgmyshin/lint/Foo.java:16: Error: you must subscribe onError [RxJava2SubscribeOnError]\n"
+ "Single.just(\"test\").subscribe()}\n"
+ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ "1 errors, 0 warnings\n",
result
);
}
:
}
getDetector()とgetIssues()で先ほどのDetectorとIssueを返すようにしてあとはゴリゴリテストを書いていくだけです。
gist
途中で何回も貼ってますが、gistにソースを載せています。
追記 2017/1/5 17:36
Testだと動いていたのですが、実際のプロジェクトで使ってみると一部動かなかったので修正しました。上記に紹介しているコードは最新です。
Single.just("test").subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
:
}
});
みたいなコードがある時に、
@Override
public void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation node
) {
:
Expression expression = expressions.first();
JavaParser.ResolvedClass param1Class = context.getType(expression).getTypeClass(); // ★
:
}
の★にたいして getSuperClass() した挙動がTest時と実際の時で違うみたいです。
うろ覚えですがTest時はgetName()とgetSuperClass()を繰り返した時
new io.reactivex.functions.Consumer(){}Consumerjava.lang.Object
みたいになってたんですが、実際は
new io.reactivex.functions.Consumer(){}java.lang.Object
ってなってたみたいです。
と、時間がなかったのでなんでそうなるかまでは今回は調べてないですが、以上です。
ちなみに
作ったlintのjarファイルは
~/.android/lint/
に格納されるのですが、module名を lint としてしまうと lint.jarが作られてしまいます。
何が言いたいかというと、結構どこのプロジェクトもmodule名をlintとしているっぽくて、同名のjarファイルができてしまうのでコマンドなどで上書きされないように名前を変えてコピペするようにするか、もしくは最初からモジュール名を変えておくと良いと思いました。