ChangeLog - noissefnoc

日々のChangeLog。趣味のコードとかの話

Google Apps Script でクラス型のコードを書いたさいのスクリプトエディタでの補完への対処方法 (Bad Hack)

TL;DR

Bad Hack の類の話です。

  • グローバルにインスタンス生成関数を定義し、関数にJSDocを記載して補完に使う
  • グローバルに補完用の関数を定義し、関数にJSDocを記載して補完に使う

ことで、副作用が発生しますが、それなりにスクリプトエディタで補完がきくようにできます。

背景

Google Apps Script のスクリプトエディタの補完の使用上、グローバルに定義されている関数定義に対するJSDocしか補完が効かない様子。JavaScriptのレイヤーでメソッドを定義すると、 Foge.prototype.bar = function() { ... } のようなコードになるので補完がきかない。

ライブラリ利用者はスクリプトエディタを使う人も多いことが想定されるので、スクリプトエディタ上で補完がきくのが望ましい。

対応策

対応策としては以下

  • グローバルにインスタンス生成関数を定義し、関数に対するJSDocを記載して補完に使う
  • グローバルに補完用の関数を定義し、関数に対するJSDocを記載して補完に使う

各パートでやることをもう少し具体化すると

  1. 実体隠蔽Hack:
    • インスタンス生成関数からのみインスタンスを生成したいので、new を呼べないようにクラス定義を隠す
    • Google Apps Script の機能で名前がアンダースコアで終わるものは外部参照できなくなるので、それを使う
  2. インスタンス補完Hack
    • 1で、直接 new できなくなったのでインスタンス生成関数をグローバルに定義。JSDocで型を指定
    • コード上は隠蔽した実体(アンダースコアで終わるもの)を返すが JSDoc 上の @return ではパッケージ名を返す
  3. メソッド補完Hack:
    • グローバルに補完用の関数を定義し、補完用の定義をJSDocで記載する

で、副作用が発生するのは「3. メソッド補完」で、他は正しくはないがコメントなので諦められなくは、ない...かな?

Google Apps Script のライブラリの外部参照

Google Apps Script でライブラリを使う場合、下図のような参照になります。

f:id:noissefnoc:20190408190526p:plain
Google Apps Script のライブラリ参照

この後クラス型のコードを書くことを想定しているので、 Pixela.Pixela のような名前になっています。

この仕様から察するにGoogle Apps Script の発想としては

  • メンバ変数やメソッドはグローバルに書く
  • プライベートなものは名前にアンダースコアをつけることで隠す

という方法で Pixela.create などとするのでしょう。

しかし、非JavaScript民としてはTypeScriptで書きたいという気分もあり、完全に Google Apps Script のローカルルールを過学習するのは辛い。よって何か方法がないか考えます。

そのまま書いた場合の状態

Hackを試みず、クラス型を素直に書くと下図の状態になります。

f:id:noissefnoc:20190408191225p:plain
Google Apps Script でクラス型コードを書いた場合の問題点

コンストラクタ含め、各メソッドが補完できません。

「ドキュメント見ろ」というのもその通りですが、ライブラリを作った自分ですら関数名やオプション覚えてないこともままあるのでこれはちょっと辛い。

Bad Hack 適応時

対してHackを試みると下図の状態になります。

f:id:noissefnoc:20190408191807p:plain
Google Apps Script でクラス型コードを補完できるようにHackする

「対応策」の箇所で書いたHackを適応すると概ね期待通りの動作となります。

副作用として実体のない補完用の関数(例: Pixela.createUser)が補完候補に登場する上に、実行すると期待と違う結果になるという事態が発生します。

順に細かく見ていきましょう。

1. 実体隠蔽Hack

これは単純に対象とするクラスの名前の最後にアンダースコア(今回の例だと Pixela_ )を付けるだけです。 ※これはHackと書きましたが、Google Apps Script で提供されている公式の機能です

2. インスタンス補完Hack

ここからHackです。コード上はアンダースコア付きの戻り値ですが、 JSDoc上はパッケージ名の型を返す のがポイントです。

以下がnoissefnoc/gas-library-pixelaでの実装です。

/**
 * create pixe.la API client<br />
 * ...
 * @param {string} username pixe.la username
 * @param {string} token  pixe.la API token
 * @return {Pixela} pixe.la API client instance
 */
function create(username: string, token: string): Pixela_ {
  return new Pixela_(username, token);
}

これで

の両方を誤魔化すことができます。Linterに「間違っている」と指摘されますが、そこはスルーしましょう。

3. メソッド補完Hack

これでライブラリ利用者側からは

な状態になりました。

ということで、 メソッドの関数と同じシグネチャの補完のためだけの関数をJSDoc付きでグローバルに定義します

利用者側から見たときには

  • Pixela.Pixela_.createUser :処理実体。
  • Pixela.createUser :補完の時に参照する関数。ライブラリのグローバルに定義し、実体的な処理はない

の二つの関数があることになります。

利用者が行儀よく

var pi = Pixela.create(u, t);
var resp = pi.createUser("yes", "yes");  // Pixela.Pixela_.createUser を呼んでいる

のような呼び方をしてくれる場合は問題ないのですが

var resp = Pixela.createUser("yes", "yes");   // Pixela.createUser を呼んでいる

とした場合は期待していない結果が返ってきます(補完候補に出てくるのに!)。この点は現状如何ともしがたそうで、解決は難しそうです。

まとめ

  • 上記のHackで Google Apps Script でクラス型のコードを書いたさいにもスクリプトエディタで補完をすることができるようになった
  • しかし、微妙な副作用が発生している