#tsc_api_study #1
正規表現と型推論を突合し、命名規則にルールを導入するツールを紹介
- RegExp & Type -Naming Rule Linter#tsc_api_study @Takepepe
View Slide
About Me■ Takefumi Yoshii / @Takepepe■ DeNA / DeSC Healthcare■ Frontend Developer2
Agenda■ 1. 推論内容が見える「ts.TypeChecker」■ 2. TypeScript AST Viewer が教えてくれること■ 3. 正規表現で命名を規制する■ 4. 処理の流れ■ 5. ts.TypeChecker の展望3
1. 推論内容が見える「ts.TypeChecker」
1. 推論内容が見える「ts.TypeChecker」昨年末の Advent Calendar 投稿ネタとして、ts.TypeChecker を使った「anycop」というツールを作りました。anycop: https://www.npmjs.com/package/anycopQiita記事:https://qiita.com/Takepepe/items/3353159894ed57b6f0a8
1. 推論内容が見える「ts.TypeChecker」これは CLI で、any推論となっている宣言箇所を洗い出す代物です。プロジェクト全体の型安全カバレッジを算出し、CIを利用したワークフローに導入できます。anycop: https://www.npmjs.com/package/anycopQiita記事:https://qiita.com/Takepepe/items/3353159894ed57b6f0a8
1. 推論内容が見える「ts.TypeChecker」ツール内部で TypeScript Compiler API を使っていて、ts.TypeChecker がこの機能の実現に貢献しました。ts.TypeChecker は「VSCodeにおけるDXの勘所をNode.js で享受できるAPI」と言っても過言ではないでしょう。
1. 推論内容が見える「ts.TypeChecker」「マウスオーバーしたら、推論内容が見えるアレ」を、Node.js アプリケーションに落とし込むことが出来ると想像すると分かりやすいです。どの様に扱うのか、まずは可視化されたものを見ていきます。
2. TypeScript AST Viewer が教えてくれること
2. TypeScript AST Viewer が教えてくれることTypeScript AST Viewer は TSCompilerAPI を扱う上で必携ツールですね。
2. TypeScript AST Viewer が教えてくれること興味対象の ts.Node がどの様に表現されているのか分かります。
2. TypeScript AST Viewer が教えてくれること例えば「const flag = false」という変数宣言があった場合。この変数に適用されている型推論はTreeViewer(画面中央) のVariableDeclaration を選択ことすることで調べられます。
2. TypeScript AST Viewer が教えてくれることPropertiesViewer(画面右)に表示されている「Type」の内訳を確認すると「flags:512 (BooleanLiteral)」が適用されていることが確認できます。
2. TypeScript AST Viewer が教えてくれることこの 512 という値は、ts.TypeFlags のenum に格納されている列挙値です。そこには、object 以外に判別できる数種類の型が列挙されています。参照:https://github.com/microsoft/TypeScript/blob/master/lib/typescript.d.ts#L2334-L2382
2. TypeScript AST Viewer が教えてくれることts.TypeChecker のすごさは、興味対象 ts.Node の型推論を拾えることです。PropertiesViewer の「Type」に表示されている内容は、ts.TypeChecker で拾える内容そのものです。
ifStatememt で絞り込まれた値「n」も、「推論内容を絞り込んだ状態で」取得できます。
2. TypeScript AST Viewer が教えてくれること興味対象の ts.Node がどの様なものであるのか把握し、Node.js でどの様に取り扱うのか?アイディア次第でこれまでの linter では不可能だった規制を実現することが出来ます。ワクワクしてきましたね。
3. 正規表現で命名を規制する
3. 正規表現で命名を規制するこの API を使い「wordcop」というツールを作りました。次の三点を突合し、望まない命名を機械的に弾きます。■ 変数名名称■ 推論適用されている型■ 正規表現https://www.npmjs.com/package/wordcop
3. 正規表現で命名を規制する「boolean / number / string / array」のいずれかが推論適用されている変数を見つけた場合、正規表現によるチェックが走ります。module.exports = {targetDir: "../example-app",regExpChecker: {boolean: /^(is|has|should)/i,number: /.*(count|size|length)$/i,string: /.*(label|str)$/i,array: /.*(s|es|ies|list|items)$/i}}
3. 正規表現で命名を規制する「チェック対象としたい型が適用された変数」に対し、正規表現を必要なだけコンフィグファイルに記述します。(正規表現サンプルがイケてないのは容赦ください)module.exports = {targetDir: "../example-app",regExpChecker: {boolean: /^(is|has|should)/i,number: /.*(count|size|length)$/i,string: /.*(label|str)$/i,array: /.*(s|es|ies|list|items)$/i}}
3. 正規表現で命名を規制するnpm に上がってるので試してみて貰えると嬉しいです。$ yarn add -D wordcop
4. 処理の流れ
4. 処理の流れ■ 1. ts.TypeChecker を取得する■ 2. ts.TypeFlags に対応する正規表現規制をマッピングする■ 3. ts.SourceFile 毎にトラバース■ 4. ts.VariableDeclaration 毎にチェック
4-1. ts.TypeChecker を取得するはじめに ts.TypeChecker を取得します。ts.TypeChecker は ts.Program から取得することができるts.Program と対のインスタンスです。const checker: ts.TypeChecker = program.getTypeChecker()
4-1. ts.TypeChecker を取得するエントリーポイントで生成したts.TypeChecker インスタンス(checker)をアプリケーション内で引き回します。const checker: ts.TypeChecker = program.getTypeChecker()
4-2. ts.TypeFlags に対応する正規表現規制をマッピングする全ての変数宣言 Node に処理を試みるので、変数宣言 Node に対応する正規表現規制マッピングをあらかじめ用意します。export const getTypeRegExpChecker = (regExpChecker: RegExpChecker): TypeRegExpChecker => ({[ts.TypeFlags.Object]: (identifier, isArrayTypeNode)=> {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`},[ts.TypeFlags.Boolean]: regExpChecker.boolean,[ts.TypeFlags.Number]: regExpChecker.number,[ts.TypeFlags.String]: regExpChecker.string,[ts.TypeFlags.BooleanLiteral]: regExpChecker.boolean,[ts.TypeFlags.NumberLiteral]: regExpChecker.number,[ts.TypeFlags.StringLiteral]: regExpChecker.string})
4-2. ts.TypeFlags に対応する正規表現規制をマッピングするBooleanLiteral 推論と、Boolean推論は TypeFlags の種類が異なります。いずれも同じ扱いとしたいツールの利便上から、内部マッピングで対応します。export const getTypeRegExpChecker = (regExpChecker: RegExpChecker): TypeRegExpChecker => ({[ts.TypeFlags.Object]: (identifier, isArrayTypeNode)=> {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`},[ts.TypeFlags.Boolean]: regExpChecker.boolean,[ts.TypeFlags.Number]: regExpChecker.number,[ts.TypeFlags.String]: regExpChecker.string,[ts.TypeFlags.BooleanLiteral]: regExpChecker.boolean,[ts.TypeFlags.NumberLiteral]: regExpChecker.number,[ts.TypeFlags.StringLiteral]: regExpChecker.string})
4-2. ts.TypeFlags に対応する正規表現規制をマッピングするこの正規表現規制マッピングは、コンフィグファイルで上書きできる様に設計しています。export const getTypeRegExpChecker = (regExpChecker: RegExpChecker): TypeRegExpChecker => ({[ts.TypeFlags.Object]: (identifier, isArrayTypeNode)=> {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`},[ts.TypeFlags.Boolean]: regExpChecker.boolean,[ts.TypeFlags.Number]: regExpChecker.number,[ts.TypeFlags.String]: regExpChecker.string,[ts.TypeFlags.BooleanLiteral]: regExpChecker.boolean,[ts.TypeFlags.NumberLiteral]: regExpChecker.number,[ts.TypeFlags.StringLiteral]: regExpChecker.string})
4-2. ts.TypeFlags に対応する正規表現規制をマッピングする「array」の扱いは一工夫必要です。配列推論されている値は、ts.TypeFlags 上では「ts.TypeFlags.Object」として扱われます。これは、ts.Type に含まれる symbol.name を調べることで'Array' という文字列を取得できるので、これを判断材料としています。const { flags, symbol }: ts.Type = checker.getTypeAtLocation(node)const isArrayTypeNode = symbol.name === 'Array'
4-2. ts.TypeFlags に対応する正規表現規制をマッピングするts.TypeFlags.Object は、Array や Object を表すので、単純に正規表現だけでなく、判定関数を実行します。[ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`}
4-2. ts.TypeFlags に対応する正規表現規制をマッピングするfalse を返すものは違反していない ts.Node と判断。違反がある場合は与えられた正規表現規制を、期待値としてエラー文字列出力します。[ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`}
4-2. ts.TypeFlags に対応する正規表現規制をマッピングするこの判定関数をコンフィグに公開することで、更に自由に(詳細に)ルールを書くことも出来るでしょう。[ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {if (!isArrayTypeNode) return falseconst res = identifier.match(regExpChecker.array)if (res) return falsereturn ` ${regExpChecker.array}`}
4-3. ts.SourceFile 毎にトラバースファイル単位(ts.SourceFile 単位)で実行する、トラバース関数です。switch (node.kind) {case ts.SyntaxKind.VariableDeclaration:if (ts.isVariableDeclaration(node)) {const erorrMessage = checkNode(checker, typeRegExpChecker, node)if (erorrMessage) {const diagnostic = getDiagnostic(source, node, erorrMessage)console.log(diagnostic)diagnostics.push(diagnostic)}}break}
4-3. ts.SourceFile 毎にトラバースnode が ts.VariableDeclaration の場合、checkNode 関数を実行します。switch (node.kind) {case ts.SyntaxKind.VariableDeclaration:if (ts.isVariableDeclaration(node)) {const erorrMessage = checkNode(checker, typeRegExpChecker, node)if (erorrMessage) {const diagnostic = getDiagnostic(source, node, erorrMessage)console.log(diagnostic)diagnostics.push(diagnostic)}}break}
4-4. ts.VariableDeclaration 毎にチェックcheckNode 関数でtypeRegExpChecker と突合します。ts.Type の取得正規表現規制の取得変数名の取得export function checkNode(checker: ts.TypeChecker,typeRegExpChecker: TypeRegExpChecker,node: T) {const type = checker.getTypeAtLocation(node)const { flags, symbol } = typeconst check = typeRegExpChecker[flags]if (!check) return falseconst identifier = node.name.getText()if (typeof check === 'function') {const isArrayTypeNode = symbol.name === 'Array'return check(identifier, isArrayTypeNode, node)}return checkByRegExp(identifier, check)}
5. ts.TypeChecker の展望
5. ts.TypeChecker の展望今回のツールは、ガイドラインに準拠するためのサポートツールでしたが、その展望は様々です。JavaScript の記述として誤りではないものの、その潜在的なリスクから特定の記述を弾きたい場合に有用です。
5. ts.TypeChecker の展望例えば「条件分岐には真偽値しか許容しない」といった「特定構文 + 特定型」の規制も出来るでしょう。■ 文字列を条件分岐に指定してしまった■ 数値を条件分岐に指定してしまったこれらの要因に起因する事故は、機械的に防ぐことが出来そうです。
5. ts.TypeChecker の展望ts.TypeChecker を利用することで、AST のメタ情報を超えた、より強力な linter が期待できます。これは、型システムを持つ TypeScript にしか出来ないことなので、積極的に活用していきたいですね。
ご静聴ありがとうございました