アナログ金木犀

つれづれなるまままにつれづれする

RxJava2でonErrorをsubscribeしてないのを検知するCustom Lintを作る

深夜に眠れなかったので、男もすなるCustom Lint作りというものをやってみようかと。

RxJava2でonErrorをsubscribeしてないものを検知するというものを作りました。

きっと探せばあるんでしょうが、始める題材としては良さそうだと思ったのでここはあえて探さずに。

参考

下記を参考に作りました

関係ない話な上たまたまなんですが、上記qiitaのhotchemiさんもamyu君も今同じフロアで働いてたりします。

作り方

上記qiitaにも載ってますが、ここでも。下記手順で作ります。

  1. build.gradleにdependeicies追加
  2. Detector及びIssueを作成
  3. Registoryを作成
  4. 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, Completablesubscribe された時に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クラスに絞る
  • getApplicableMethodNamessubscribeメソッドに絞る
  • 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()を繰り返した時

  1. new io.reactivex.functions.Consumer(){}
  2. Consumer
  3. java.lang.Object

みたいになってたんですが、実際は

  1. new io.reactivex.functions.Consumer(){}
  2. java.lang.Object

ってなってたみたいです。

と、時間がなかったのでなんでそうなるかまでは今回は調べてないですが、以上です。

ちなみに

作ったlintのjarファイルは

~/.android/lint/

に格納されるのですが、module名を lint としてしまうと lint.jarが作られてしまいます。

何が言いたいかというと、結構どこのプロジェクトもmodule名をlintとしているっぽくて、同名のjarファイルができてしまうのでコマンドなどで上書きされないように名前を変えてコピペするようにするか、もしくは最初からモジュール名を変えておくと良いと思いました。

Androidでファイル群テンプレートを作る話

File TemplateとかProject Templateは作れるのは知っていたのですが、

Files Templateって作れないのかなーと思ってました。

設計によってはある機能や画面を作る度に似たようなクラス群を作ったりしますしね。

ちょっと調べてみたら Project Templateと同じ要領で作れたので紹介します。

(Macでの開発を前提として書いていきます)

デフォルトのテンプレートの置き場所

Project Template同様、下記に置いてあります。

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/

作り方

下記を用意します。

  • template.xml
  • globals.xml.ftl
  • recipe.xml.ftl
  • 作りたいテンプレート
template.xml

このFiles Templateの概要をtemplate.xmlに載せます。例はこんな感じ。

<?xml version="1.0"?>
<template
    name="Template Name"
    description="make screen template"
    revision="1"
    format="5">

    <category value="MyCategory"/>

    <parameter
        name="Class Name"
        constraints="unique|nonempty"
        default="ClassName"
        id="className"
        type="string"/>

    <parameter
        name="Resource Name"
        constraints="unique|nonempty"
        default="resName"
        id="resName"
        type="string"/>


    <globals file="globals.xml.ftl"/>
    <execute file="recipe.xml.ftl"/>

</template>

template タグにこのFiles templateの説明を。

category にカテゴリ名を。

f:id:kgmyshin:20170103170208p:plain

上のOtherService がカテゴリ名です。自分の場合は、汎用的でないがあるプロジェクトでやたら使うテンプレートとか作るときにプロジェクト名をカテゴリ名としてたりします。

parameterには自分が入力したいパラメーターを。上のxmlだと下記のようになります。

f:id:kgmyshin:20170103171247p:plain

globals.xml.ftl

ここには様々なファイルパスを定義しておきます。必要なものだけで良いです。

<?xml version="1.0"?>
<globals>
    <global id="resOut" value="${resDir}" />
    <global id="manifestOut" value="${manifestDir}" />
    <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
    <global id="unitTestOut" value="${escapeXmlAttribute(projectOut)}/src/test/java/${slashedPackageName(packageName)}" />
    <global id="relativePackage" value="<#if relativePackage?has_content>${relativePackage}<#else>${packageName}</#if>" />
</globals>
recipe.xml.ftl

root ディレクトリを作ってそこにテンプレートを作っておきます。 (下記を参考)

recipe.xml.ftl ではディレクトリを作ったり、テンプレートからファイルを作成したり、コードをマージしたりできます。

<?xml version="1.0"?>
<recipe>

    <instantiate from="root/src/app_package/Activity.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${className}Activity.java" />
    <instantiate from="root/src/app_package/Contract.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${className}Contract.java" />
    <instantiate from="root/src/app_package/Fragment.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${className}Fragment.java" />
    <instantiate from="root/src/app_package/Presenter.java.ftl"
                   to="${escapeXmlAttribute(srcOut)}/${className}Presenter.java" />


    <instantiate from="root/res/layout/activity.xml.ftl"
                   to="${escapeXmlAttribute(resOut)}/layout/activity_${resName}.xml" />
    <instantiate from="root/res/layout/fragment.xml.ftl"
                   to="${escapeXmlAttribute(resOut)}/layout/fragment_${resName}.xml" />

    <merge from="root/AndroidManifest.xml.ftl"
             to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
</recipe>

上記では

  • HogeActivity, HogeContract, HogeFragment, HogePresenterクラスを作成
  • activity_hoge.xmlfragment_hoge.xmlを作成
  • AndroidManifest.xmlにテンプレートの内容をマージ

ということをやってます。

使ってみる

Cmd + N を押して「New」を開いて下の方に行くと

f:id:kgmyshin:20170103175739p:plain

こういうのが出てくるのであとはパラメータを入力して実行です。

共有方法とか

汎用的なテンプレートならgithubに全公開とかでいいと思いますが、プロジェクトに閉じたものならプロジェクトのgit管理下に置いて運用するのがいいと思います。

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/

に、コピーしてくださいというのも面倒なので、スクリプトを用意してそれもセットにして管理しておくのがオススメ。

注意

/Applications/Android Studio.app/Contents/plugins/android/lib/templates/

配下にテンプレートを置いておくとアップデート時にvalidation errorになります。

アップデートの時は削除しましょう。

終わりに

次回のkyobashi.dexでこのことも話そうと思います。

rmp-quipper.connpass.com

多分これに関してはこっちの方が詳しくなってるかもですが、それ以外のことも似たようなことを話そうかと。

何か聞きたいこととかあれば是非来てください。m m

振り返りと今年の抱負

2016年やったこと

仕事

リクルートマーケティングパートナーズに2015年の5月に入社したので、2016年は入社して1~2年目ってことになる。

2016年が始まってからの4月くらいまでは、新卒の子とiOSのエンジニアとスタディサプリ EnglishAndroid版を作っていた。ここで学んだチーム開発のことについては、shibuya.apkで話したし、 dexfmでも話した。ただ、話した内容は全然氷山の一角なのと、アップデートもあるので次回のDroidKaigiでも話そうと思う(なんども同じこと話してごめんなさい)。

リリース後は、Google IOに行かせてもらった。一応ブログも頑張った。

tech.recruit-mp.co.jp

tech.recruit-mp.co.jp

Google IOから帰ってきては、 kidsly というサービスのAndroidで kotlinにリプレースするプロジェクトに入った。

その後はそれが終わって、新しいプロジェクトに関わっている。

登壇とか

羅列。

1/22 kyobashi.dex #2 (開催)

Coordinator Layout Behavior

2/5 shinobu.apk #1

shinobu.apk #1 のパネルディスカッション録音データとShow Notesを公開しました! #shinobuapk - I'm knowledge worker

4/20 potatotips #28

CQRSをモバイルに適用してみる // Speaker Deck

5/9 shibuya.apk #7

unskilled team development for android // Speaker Deck

5/27 kyobashi.dex #3 (開催)

advanced data binding // Speaker Deck

7/6 retty tech cafe #6

awareness api // Speaker Deck

8/8 Androidオールスターズ2

memorandum/my_architecture.pdf at master · kgmyshin/memorandum · GitHub

9/29 potatotips #34

layered arch with swinject // Speaker Deck

その他

2016年振り返り

登壇回数の減少

2016年の後半は登壇回数が減っている件。

これについては、狙ったというか自分で意図的に減らした。発表のためにインプットをしてたんだが、発表に慣れてしまってアウトプットの質が低くなってきたように感じたのが理由。

大きな舞台や頻度を減らしてしっかりアウトプットする方向に切り替えたので減ったこと自体は悪くない。数ヶ月に一回できれば大きな舞台で登壇できれば良しとする。

エンジニアとしての成長

やりたいことはいっぱいあったはず。基礎、機械学習、インフラ、etc...

仕事の中でScalaやcycle.jsに触れたことは幸いだったけど、それ以外が良くなかった。

サービス作りやOSS

2016年はOSSに関わる時間をサービス作りに充てようと考えていた。結果、サービス作ろうとして結局一つも仕上がらなかった。良くない。

痩せれた

これはグッドポイント。痩せれたのを機に中学高校に部活でやっていたバスケをまた始めた。

葛藤

キャリアパスについて悩みができてきた。これはまだ答えが出てない。

2017年の抱負

やりたいことベースに2016年の振り返りを意識しつつ抱負を。

なによりもサービス作りを

仕事でもしっかり。だが、ちゃんと個人でもリリースまで。 場合によってはどこかを手伝うという選択肢もありかもしれない。 ただ必ず個人で何か作ってリリースする。

エンジニアとしての成長

2016年で痩せれたのは何よりも習慣づけというのが上手くなったからだと思う。 学習の習慣づけをして、仕事が忙しいときにでもしっかりと。週一で優先順位つけてタスク分解及び検証。

登壇

数ヶ月に一回できれば大きな舞台で、を継続。

キャリアパス

「本当にやりたいことは?」をしっかり意識して、ちょっと考え直す。今月中に。

もうちょい痩せる。

痩せる。

バスケ

週一はやる。みんなで大会出て優勝。

締め

今年もよろしくお願い致します。

ちなみに

今年のおみくじは吉で要約すると「筋を通せ。筋を通しさえしていれば大丈夫。」みたいな感じだったので、しっかり筋を通していきたいと思います。

SHIROBAKOで「好きなシーンは?」と言われて、ばばっと思いつくやつをざざっとまとめてみる

こんにちは。

この記事はSHIROBAKO Advent Calendar 30日目(????)の記事です。

前回はしの(@shanon)の斬新な切り口の記事でしたね。

shanonim.hatenablog.com

興津さんかわいい。

今回はただただ僕が「好きなシーンは?」と聞かれた時に今ばばっと思いつくやつを、ざざっとまとめてみたいと思います。

佐倉さんのかっこいいやつ

まずは佐倉さんのかっこいいセリフから。1話でみやもりに急ぎの仕事を依頼された時の返答。

佐倉 「ダビングまであと1時間?何言ってんの?20分あれば余裕だよ」

仕事できすぎかよ。

本田さんの庇うやつ

12話で、原画が見つからないうえ重いシーンしか残っていないところ。なべPが監督や制作進行を集めて、作画を軽くすることを提案するシーンです。

なべP 「監督、絵コンテ変えましょう」

監督も渋々ですが、頷こうとしたその時にあの優しい本田さんが珍しく声を荒げます。

本田 「何言ってんですか!ここはクライマックスじゃないですか!最終話のクライマックスで逃げるなら今まで何やってきたんだって話ですよ!」

監督ではなく、デスクの本田さんが譲れない! ってのが本当に良い!

デスクと監督は一心同体みたいなセリフが劇中ででてきますが、まさに!って感じでしたね。

本田さんのこういう姿勢はクリエーターを守るというような意思から来てるのでしょうか。そう考えるとACツチノコの社長が会社を立ち上げる時に手伝ったっていうのもうなづけるかも。

杉江さんのありがとう

大量の馬のシーンを描くための作戦会議のシーン。 杉江さんが、このままだと間に合わない旨を述べた後に続きます。

杉江 「けど、ラフ原で良ければ時間短縮できる」

:

杉江 「ただ、僕のラフは原画に慣れてない若い人には線が拾えないかもしれない」

そして、第二原画をムサニのツートップが

小笠原「問題ありません、第二原画、私がやります」

井口 「私もやります」

と、小笠原さん井口さんに続き遠藤さんや他の原画も「自分もやらせてほしいと」「私も」「やりたいです」と続いていきます。

続いていくのですが、エマだけが言い出せないというところでで杉江さんエマの方を向いて言います。

杉江 「ありがとう、助かるよ」

優しすぎか!

興津さんのガッツポーズ

何も語ることはありません。

えまのマスクシーン

エマのマスクのシーンについては去年書いてました。

motida-japan.hatenablog.com

所感

とりあえずこういうこと話ながら酒が飲みたいっすね!

SHIROBAKOで語られる夫婦の話

こんにちは。

この記事は SHIROBAKO Advent Calendar 28日目(!?) の記事です。

このアドベントカレンダーの一つ目の投稿で SHIROBAKOで語られる親と子供の話 を書きました。

これと同じベクトルで夫婦の話も書いてみたいなぁと思ってて、ちょうどチャンスが来た(作った)ので書いてみたいと思います。

SHIROBAKOに出てくる夫婦ってすごくほっこりするシーンが多いんですよね。

さっそく夫婦のほっこり会話シーンを紹介していきますね。

遠藤夫妻

イデポン宮森 発動編の一幕。

遠藤さんが断っていた仕事をやっぱり引き受けて、徹夜をしようとするところです。描いてる途中でハッと思い出したように奥さんに電話をかけます。

(電話つながる)

遠藤 「ごめん、今日夕飯いらない。」

遠藤妻「わかった。がんばってね。」

遠藤 「あぁ。」

(電話切る)

たった、3つのセリフでいろんなことが伝わってきます。

いつものことなんでしょうけど、「わかった。」に続くのが「がんばってね。」って時点で、 夕飯をいらない という事実だけでなく、言外にある 仕事で徹夜するから というのもしっかり伝わってて関係の深さを感じます。ほっこりしますねぇ。

ちなみに遠藤さんの奥さんはすごく美人です。

(この記事、27日の昼ごろにはできてたんですが、奇しくも27日目とかぶりましたね。これはかわいいからしょうがない。)

杉江夫妻

第11話「原画売りの少女」で、みやもりが原画を杉江さんに依頼するシーン。

杉江  「みやもりさん、まだみんな会社に残ってるかな?」

みやもり「え?」

杉江  「話をしないといけないから」

杉江妻 「あなたの悪い癖。いつも言葉が少ないか多いのよ」

杉江妻 「杉江は受けるつもりでいるみたい。でも少し作戦が必要だって」

杉江  「うん。明日から帰り遅くなるから」

杉江妻 「お弁当二つ作らないとね」

前半の癖の会話。

少し笑いながら夫のことはなんでも知ってるような言い回しにほっこりしてしまいますねぇ。

たった一言の「会社にみんないるかな?」だけで、全てを読み取るような察しのよさ、というよりは夫への理解さすがです。

そして最後のお弁当の会話もツーカー具合も目指すべき男女関係の一つだなぁと。

若かりし頃の杉江夫妻がこちら。

ここから何年も経ってこの関係になったんですねぇ。(杉江さんの奥さんの若い頃もかわいい)

所感?

ちょっと短くなってしまいましたが、ほっこりできましたでしょうか?

最後に yome.fm のリンクをそっと添えて、締めとしたいと思います。にやけること必至なので聞いてみてくださいね。

yomefm.github.io

ちょうど新しい話も出てきたみたいですよ?

yomefm.github.io