$30 off During Our Annual Pro Sale. View Details »

- Regular expression & Type - Naming Rule Linter

Takepepe
March 04, 2020

- Regular expression & Type - Naming Rule Linter

#tsc_api_study #1

正規表現と型推論を突合し、命名規則にルールを導入するツールを紹介

Takepepe

March 04, 2020
Tweet

More Decks by Takepepe

Other Decks in Technology

Transcript

  1. - RegExp & Type -
    Naming Rule Linter
    #tsc_api_study @Takepepe

    View Slide

  2. About Me
    ■ Takefumi Yoshii / @Takepepe
    ■ DeNA / DeSC Healthcare
    ■ Frontend Developer
    2

    View Slide

  3. Agenda
    ■ 1. 推論内容が見える「ts.TypeChecker」
    ■ 2. TypeScript AST Viewer が教えてくれること
    ■ 3. 正規表現で命名を規制する
    ■ 4. 処理の流れ
    ■ 5. ts.TypeChecker の展望
    3

    View Slide

  4. 1. 推論内容が見える「ts.TypeChecker」

    View Slide

  5. 1. 推論内容が見える「ts.TypeChecker」
    昨年末の Advent Calendar 投稿ネタとして、
    ts.TypeChecker を使った「anycop」
    というツールを作りました。
    anycop: https://www.npmjs.com/package/anycop
    Qiita記事:https://qiita.com/Takepepe/items/3353159894ed57b6f0a8

    View Slide

  6. 1. 推論内容が見える「ts.TypeChecker」
    これは CLI で、any推論となっている
    宣言箇所を洗い出す代物です。
    プロジェクト全体の型安全カバレッジを算出し、
    CIを利用したワークフローに導入できます。
    anycop: https://www.npmjs.com/package/anycop
    Qiita記事:https://qiita.com/Takepepe/items/3353159894ed57b6f0a8

    View Slide

  7. 1. 推論内容が見える「ts.TypeChecker」
    ツール内部で TypeScript Compiler API を使っていて、
    ts.TypeChecker がこの機能の実現に貢献しました。
    ts.TypeChecker は
    「VSCodeにおけるDXの勘所をNode.js で享受できるAPI」
    と言っても過言ではないでしょう。

    View Slide

  8. 1. 推論内容が見える「ts.TypeChecker」
    「マウスオーバーしたら、推論内容が見えるアレ」を、
    Node.js アプリケーションに落とし込むことが出来る
    と想像すると分かりやすいです。
    どの様に扱うのか、
    まずは可視化されたものを見ていきます。

    View Slide

  9. 2. TypeScript AST Viewer が教えてくれること

    View Slide

  10. 2. TypeScript AST Viewer が教えてくれること
    TypeScript AST Viewer は TSCompilerAPI を扱う上で必携ツールですね。

    View Slide

  11. 2. TypeScript AST Viewer が教えてくれること
    興味対象の ts.Node がどの様に表現されているのか分かります。

    View Slide

  12. 2. TypeScript AST Viewer が教えてくれること
    例えば「const flag = false」という
    変数宣言があった場合。
    この変数に適用されている型推論は
    TreeViewer(画面中央) の
    VariableDeclaration を
    選択ことすることで調べられます。

    View Slide

  13. 2. TypeScript AST Viewer が教えてくれること
    PropertiesViewer(画面右)に
    表示されている「Type」の内訳を確認すると
    「flags:512 (BooleanLiteral)」が
    適用されていることが確認できます。

    View Slide

  14. 2. TypeScript AST Viewer が教えてくれること
    この 512 という値は、ts.TypeFlags の
    enum に格納されている列挙値です。
    そこには、object 以外に判別できる
    数種類の型が列挙されています。
    参照:https://github.com/microsoft/TypeScript/blob/master/lib/typescript.d.ts#L2334-L2382

    View Slide

  15. 2. TypeScript AST Viewer が教えてくれること
    ts.TypeChecker のすごさは、
    興味対象 ts.Node の型推論を拾えることです。
    PropertiesViewer の「Type」に表示されている内容は、
    ts.TypeChecker で拾える内容そのものです。

    View Slide

  16. ifStatememt で絞り込まれた値「n」も、
    「推論内容を絞り込んだ状態で」取得できます。

    View Slide

  17. 2. TypeScript AST Viewer が教えてくれること
    興味対象の ts.Node がどの様なものであるのか把握し、
    Node.js でどの様に取り扱うのか?
    アイディア次第でこれまでの linter では不可能だった規制を
    実現することが出来ます。
    ワクワクしてきましたね。

    View Slide

  18. 3. 正規表現で命名を規制する

    View Slide

  19. 3. 正規表現で命名を規制する
    この API を使い「wordcop」というツールを作りました。
    次の三点を突合し、望まない命名を機械的に弾きます。
    ■ 変数名名称
    ■ 推論適用されている型
    ■ 正規表現
    https://www.npmjs.com/package/wordcop

    View Slide

  20. 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
    }
    }

    View Slide

  21. 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
    }
    }

    View Slide

  22. 3. 正規表現で命名を規制する
    npm に上がってるので試してみて貰えると嬉しいです。
    $ yarn add -D wordcop

    View Slide

  23. 4. 処理の流れ

    View Slide

  24. 4. 処理の流れ
    ■ 1. ts.TypeChecker を取得する
    ■ 2. ts.TypeFlags に対応する正規表現規制をマッピングする
    ■ 3. ts.SourceFile 毎にトラバース
    ■ 4. ts.VariableDeclaration 毎にチェック

    View Slide

  25. 4-1. ts.TypeChecker を取得する
    はじめに ts.TypeChecker を取得します。
    ts.TypeChecker は ts.Program から取得することができる
    ts.Program と対のインスタンスです。
    const checker: ts.TypeChecker = program.getTypeChecker()

    View Slide

  26. 4-1. ts.TypeChecker を取得する
    エントリーポイントで生成した
    ts.TypeChecker インスタンス(checker)を
    アプリケーション内で引き回します。
    const checker: ts.TypeChecker = program.getTypeChecker()

    View Slide

  27. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    全ての変数宣言 Node に処理を
    試みるので、変数宣言 Node に
    対応する正規表現規制マッピン
    グをあらかじめ用意します。
    export const getTypeRegExpChecker = (
    regExpChecker: RegExpChecker
    ): TypeRegExpChecker => ({
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode)
    => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${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
    })

    View Slide

  28. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    BooleanLiteral 推論と、Boolean
    推論は TypeFlags の
    種類が異なります。
    いずれも同じ扱いとしたい
    ツールの利便上から、
    内部マッピングで対応します。
    export const getTypeRegExpChecker = (
    regExpChecker: RegExpChecker
    ): TypeRegExpChecker => ({
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode)
    => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${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
    })

    View Slide

  29. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    この正規表現規制マッピングは、
    コンフィグファイルで
    上書きできる様に設計しています。
    export const getTypeRegExpChecker = (
    regExpChecker: RegExpChecker
    ): TypeRegExpChecker => ({
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode)
    => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${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
    })

    View Slide

  30. 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'

    View Slide

  31. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    ts.TypeFlags.Object は、Array や Object を表すので、
    単純に正規表現だけでなく、判定関数を実行します。
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${regExpChecker.array}`
    }

    View Slide

  32. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    false を返すものは違反していない ts.Node と判断。
    違反がある場合は与えられた正規表現規制を、
    期待値としてエラー文字列出力します。
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${regExpChecker.array}`
    }

    View Slide

  33. 4-2. ts.TypeFlags に対応する正規表現規制をマッピングする
    この判定関数をコンフィグに公開することで、
    更に自由に(詳細に)ルールを書くことも
    出来るでしょう。
    [ts.TypeFlags.Object]: (identifier, isArrayTypeNode) => {
    if (!isArrayTypeNode) return false
    const res = identifier.match(regExpChecker.array)
    if (res) return false
    return ` ${regExpChecker.array}`
    }

    View Slide

  34. 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
    }

    View Slide

  35. 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
    }

    View Slide

  36. 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 } = type
    const check = typeRegExpChecker[flags]
    if (!check) return false
    const identifier = node.name.getText()
    if (typeof check === 'function') {
    const isArrayTypeNode = symbol.name === 'Array'
    return check(identifier, isArrayTypeNode, node)
    }
    return checkByRegExp(identifier, check)
    }

    View Slide

  37. 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 } = type
    const check = typeRegExpChecker[flags]
    if (!check) return false
    const identifier = node.name.getText()
    if (typeof check === 'function') {
    const isArrayTypeNode = symbol.name === 'Array'
    return check(identifier, isArrayTypeNode, node)
    }
    return checkByRegExp(identifier, check)
    }

    View Slide

  38. 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 } = type
    const check = typeRegExpChecker[flags]
    if (!check) return false
    const identifier = node.name.getText()
    if (typeof check === 'function') {
    const isArrayTypeNode = symbol.name === 'Array'
    return check(identifier, isArrayTypeNode, node)
    }
    return checkByRegExp(identifier, check)
    }

    View Slide

  39. 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 } = type
    const check = typeRegExpChecker[flags]
    if (!check) return false
    const identifier = node.name.getText()
    if (typeof check === 'function') {
    const isArrayTypeNode = symbol.name === 'Array'
    return check(identifier, isArrayTypeNode, node)
    }
    return checkByRegExp(identifier, check)
    }

    View Slide

  40. 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 } = type
    const check = typeRegExpChecker[flags]
    if (!check) return false
    const identifier = node.name.getText()
    if (typeof check === 'function') {
    const isArrayTypeNode = symbol.name === 'Array'
    return check(identifier, isArrayTypeNode, node)
    }
    return checkByRegExp(identifier, check)
    }

    View Slide

  41. 5. ts.TypeChecker の展望

    View Slide

  42. 5. ts.TypeChecker の展望
    今回のツールは、ガイドラインに準拠するためのサポートツールでしたが、
    その展望は様々です。
    JavaScript の記述として誤りではないものの、
    その潜在的なリスクから特定の記述を弾きたい場合に有用です。

    View Slide

  43. 5. ts.TypeChecker の展望
    例えば「条件分岐には真偽値しか許容しない」
    といった「特定構文 + 特定型」の規制も出来るでしょう。
    ■ 文字列を条件分岐に指定してしまった
    ■ 数値を条件分岐に指定してしまった
    これらの要因に起因する事故は、機械的に防ぐことが出来そうです。

    View Slide

  44. 5. ts.TypeChecker の展望
    ts.TypeChecker を利用することで、
    AST のメタ情報を超えた、
    より強力な linter が期待できます。
    これは、型システムを持つ TypeScript にしか
    出来ないことなので、積極的に活用していきたいですね。

    View Slide

  45. ご静聴ありがとうございました

    View Slide