Code Review Meetup #4 で「歴史的経緯の説明 as code」という発表をしました

Sider社の開発する Querly を使うと「歴史的経緯の説明」をコード化できるよという話です。

Querlyの話は以前も書いたことがあります: プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利 - Islands in the byte stream

Querlyは便利で可愛いやつなんですが「価値がわかる」ところに到達するのが難しいツールなので(ぼくなんて2年かかってようやく理解できましたからね!)、もうちょい公式ドキュメントがわかりやすくなるといいな〜と思いますね。あと querly init がほしい。

特定のファイルに対して一定のテストカバレッジを保証する

ある特定のファイルのテストカバレッジが、ある特定のテストファイル(群)を実行したときに一定のカバレッジ率であることを保証したいと思うことはありますか?私はあります。

そこで、 次のような spec/coverage_helper.rb を用意して、 spec/rails_helper.rb などから require すると、 COVERAGE_ASSERTION=app/models/ability.rb rspec spec/models/ability_spec.rb:99 のようなコマンドをCIで実行したときに一定のテストカバレッジ以下のときにCIがコケるようにしました。

# frozen_string_literal: true

class CoverageAssertion
  class CoverageAssertionFailure < StandardError
  end

  attr_reader :file_map
  attr_reader :highline

  def initialize
    require "highline"

    coverage_assertion = ENV['COVERAGE_ASSERTION']
    @file_map = Hash[coverage_assertion.split(',').map { |pair| pair.split(':') }]
    @file_map.transform_keys! { |key| Pathname.new(key) }
    @file_map.transform_values! { |value| value.to_f }

    @highline = HighLine.new
  end

  def format(result)
    failure_message = +""
    result.groups.each do |_name, files|
      files.each do |f|
        filename = Pathname.new(f.filename).relative_path_from(Rails.root)
        threshold = file_map[filename]
        if threshold && f.covered_percent < threshold
          failure_message << highline.color("#{filename}: expected at least #{threshold}% but got #{f.covered_percent.round(2)}%\n", :red)
        end
      end
    end

    if failure_message.present?
      raise CoverageAssertionFailure, "Coverage assertion failed!\n#{failure_message}"
    end
  end
end

if ENV['CI'] || ENV['COVERAGE'] || ENV['COVERAGE_ASSERTION']
  require 'simplecov'
  require 'simplecov-lcov'

  # COVERAGE_ASSERTION="$file1:$least_coverage1,$file2:$least_coverage2,..."
  # See also .circleci/config.yml
  if ENV['COVERAGE_ASSERTION']
    SimpleCov.formatter = CoverageAssertion
  else
    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
      [
        SimpleCov::Formatter::HTMLFormatter,
        SimpleCov::Formatter::LcovFormatter,
      ],
    )
  end

  SimpleCov.start 'rails' do
    # add_filter [...]
  end
end

Rails + rspec + simplecov という条件で動くはず。

cmake-js があれば node-gyp に依存せず NodeJS native addon を作れそう

github.com

cmake-js という cmake のラッパーを使うと、 node-gyp を使わずに NodeJS native addon を作れるみたいです。 node-gyp は Python 2.x に依存しているのが嫌なので、 node-gyp 依存をなくせるというだけで個人的にはけっこう嬉しかったりします。

ついでに NodeJS native addon の新しい C++ API であるところの N-API もちょっと試してみました (src/hello.cc)。これは、 NodeJS のJSエンジンである V8 API を抽象化して、V8 API のバージョンごとの差異を吸収する API layer です。N-API 登場以前の NodeJS native addon は NodeJS / V8 のアップデートですぐビルドできなくなっていたものですが、 N-API を使うようにすることで複数の NodeJS バージョンに対応することが容易になりそうです。

cmake-js + N-API で NodeJS native addon がかなり開発しやすくなりましたね。NodeJS はスクリプト言語としては十分に高速なので速度のために native addon を開発する必要はほとんどないのですが、既存の資産 の活用はもっと気軽にできていいので、native addon を開発しやすくなるのはありがたいです。

プロジェクト固有のルールを指定できるLinterであるところのQuerlyがめちゃ便利

https://github.com/soutaro/querly

  • Rubyを構文解析したASTに対して独自DSLでパターンマッチ&メッセージを出すツール
  • プロジェクト固有の事情に配慮したLinterとして使える
  • false positive 上等で注意喚起として使う

たとえばKibelaの querly.yaml から一部抜粋するとこんな感じです。

rules:
  # ...
  - id: kibela.order_by_string
    pattern:
      - "order(:dstr:)"
      - "where(:dstr:)"
      - "find(:dstr:)"
      - "exists?(:dstr:)"
    message: "文字列によるSQL構築は本当に必要ですか? SQL Injection を引き起こさないように気をつけてください。"
  - id: kibela.block_call
    pattern:
      - "yield"
    message: "yieldではなくblock.callを使いましょう。そのほうが渡す引数が明確になります。"

kibela.order_by_string のルールの :dstr: は double-quoted string で、変数展開を伴う生SQLに対して注意を促しています。実際Querlyを使い始めてから1年以上たちますが、「気をつけてください」というレベルのメッセージでも十分役に立ちます。プロジェクト固有のガイドラインを(DSLがサポートしているかぎり)どんどん足していけるのは非常にいいですね。

ルールの設定は querly console でREPLを起動できるので、そこで行います。Railsプロジェクトだと querly console app lib ですかね。対象がRubyなのでそんなに正確ではないものの、構文解析をするぶん正規表現によるパターンマッチよりも遥かに正確ですし、実用上はそれで十分です。

惜しむらくはRubyしかターゲットにできないこと! TypeScript 用にもほしい…(切実)。

なお実行にそこそこ時間がかかってpush前にやるのは大変なので、 Kibelaの場合はSider で実行を自動化しています。

See Also

「GraphQL APIだとバージョニングが不要」という言明は誤解を招くので避けるべき

1行で

  • 「特定の場合でバージョニングしなくても対応できることはある」程度なので、「バージョニング不要」とは言わないほうがよい

どういうことか

  • RESTful API から GraphQL へ、GraphQL から別の Web API systemへ、ということを考えると大きな意味でのバージョニングは必要
    • e.g. 実際にGitHub は GraphQL API を "API v4" と呼んでいる
  • 細かなレベルのバージョニング、たとえば1画面の仕様が微妙に変わるたびに /foo, /foo_201807_1, foo_201807_2 みたいにどんどん特定画面専用APIを定義していく、みたいな意味でのバージョニングは不要
  • fieldの追加は無造作に行ってよい
    • RESTful APIでもfieldの追加は普通はできる、ただし負荷に注意
    • 重いcomputed fieldの場合でも、GraphQLのほうは無造作に追加できる
  • fieldのrename, deleteはGraphQLでも気軽には行えない
    • @deprecated directiveはあるが、何かを保証するものではないので削除の際は利用者との間でコンセンサスをとる

DX: Developer Experience (開発体験)は重要だ

  • DX: Developer Experience (開発体験)とは、あるシステムを「気持ちよく開発・保守できるかどうか」を示すもの
  • 開発者は開発・保守という行為を通じたそのシステムのユーザーであり、DXはUXの一種である
  • DXがよいと日々の開発を楽しめるようになり、気持ちに余裕ができる
    • 気持ちの余裕がでるとコードの品質があがり保守時のデグレも減らせる
    • また、DXがよい事自体がDXを高める動機になり、正のスパイラルを見込める
      • つまり、「定められたタスク」(=義務)以上のことを行うようになる
  • DXが悪いと開発を楽しめず、「定められたタスク」以外のことをしたくなくなる
  • DXは放置すると悪化するので、「DXがよくも悪くもない」プロダクトは時間が経つに連れ「DXが悪い」になる
    • なので積極的にDXを良くしていく活動を奨励していくのがよい

いくつか興味深いフィードバックがあったので記しておきます。

⇢ PX: Programming Experience という概念を提唱している研究会があるようですね(DXとはかなり異なる概念のようですが): SIGPX: Special Interest Group on Programming Experience

TypeScriptの `ReadonlyArray<T>` を使いやすくするためにtslintを活用する

最近、Kibelaのtslint configの Rule: array-type を "generics" にしました:

+     "array-type": [
+       true,
+       "generic"
+     ],

以前は特に指定しておらず、 T[]Array<T> が混在してていて、それでよしとしていました。今でも、混在することによるデメリットは特にないと思っていいます。ただ、 Array<T> には一つだけメリットがあったのでこちらに統一することにしました。 autofixできるので、エディタ上では T[] と書いて保存時にtslintに Array<T> に直させるということができるため、導入のデメリットがないというのも大きいです。

さて Array<T> のメリットは、 ReadonlyArray<T> に直しやすいということです。 ReadonlyArray<T>Array<T> から破壊的変更を伴うメソッド(mutation methods)を取り除いたインターフェイスで、特にReactではstateやpropsに破壊的変更を与えてはいけないということになっているので、積極的に使うメリットがあります。

ReadonlyArray<T> についてTypeScriptの公式マニュアルではほとんど触れられていませんが、インターフェイスの章にすこしだけ記述がありますね。

https://www.typescriptlang.org/docs/handbook/interfaces.html

TypeScript comes with a ReadonlyArray<T> type that is the same as Array<T> with all mutating methods removed, so you can make sure you don’t change your arrays after creation:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

なお同様に ReadonlyMap<K, V>ReadonlySet<T> もあるようです。

graphql-rubyの知見をscrapboxにまとめはじめた

scrapbox.io

React DOM elementsに focused プロパティがほしくなったとき

<input ref={(element) => this.input = element}/> しておいて this.input.focus() とか this.input.blur() とか呼び出すのはReact的ではないのでなんとかしたいものだなあ、と思って一旦こういう関数を作って運用してみることにしました。

// in TypeScript

// This function emulates "focus" attributes for React DOM elements
export function createFocusHandler<E extends HTMLElement>(focused: boolean) {
  return (element: E | null) => {
    if (element && element.matches(":focus") !== focused) {
      if (focused) {
        element.focus();
      } else {
        element.blur();
      }
    }
  };
}

これを次のように ref へのイベントハンドラとして渡せばOKです。

<input ref={createFocusHandler(focused)}/>

ラッパー component を作ってやっていくほうがいいような気もしますが、まあ動くのでとりあえずこれで。

yarn-deduplicate(1) で yarn.lock の重複エントリを最適化する

追記(2019/03/18): yarn-toolsからyarn-deduplicateが独立して使いやすくなり、 --strategy でdedupeの方法を選べるようになっています。タイトルも変更しました。

yarnpkg(1) を使って依存関係を管理しているとき、 yarnpkg upgrade-interactive は対話的にライブラリのアップデートができるので大変便利です。しかし、これを実行すると yarn.lock に不必要に重複エントリが作られることがよくあります。

nodejsで実行するケースでは重複があっても問題がないことが多いのですが、 クライアントサイドでは重複があると単純にファイルサイズが大きくなり。また、@types/* や react, jquery といったフレームワークはどの環境でも動作に問題が出たりするので、重複エントリは問題です*1。そこで今までは、 yarnpkg upgrade-interactive 後に 差分を眺めて重複エントリを1つにまとめるというタスクを手動でやっていました。

とはいえこの yarn.lock 最適化ロジックは機械的にできるはずなので、いいツールはないかと探していて、なければ作るのでもいいかと思っていいたところ、 yarn-deduplicate(1) というツールがその機能を提供しているようでした。

github.com

手元のかなり大きな yarn.lock でもちゃんと動いたので、 package.json の scripts セクションに次のようなdedupe scriptを登録して、 npm run dedupe で運用していこうと思います。

npx yarn-deduplicate --strategy=fewer && yarn install --force

こういうとき、 npx(1) を使うとプロジェクトの依存関係をいじらなくて済むのでいいですね。

*1:特にTypeScriptの型定義はグローバルスコープだったりするので、微妙に異なるバージョンの .d.ts が node_modules 以下に復数存在すると TS2300: Duplicate identifier が起きがちです。