Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

PHPのちょっとした静的解析をGo+GitHub Actionsで実施するようにしてみた

こんにちは、エンジニアの尾宇江です。

goでプルリク作ってリリース作業を改善してみた。のエントリーに引き続き、Goを使ったちょっとした改善をした話です。

最初に

書いたコードをcommitした後、「あープリントデバッグで仕込んでたvar_dump消し忘れた」ってことありますよね?
あと、「設定ファイルのURLをexample.comのままにしてcommitしちゃった」ということも良くありますよね?
私は日常的にやらかしていました…

対策

気をつけても抜ける時は抜けるので、自動化しよう!!
ということで、CIでチェックするようにしました。

対象となるリポジトリはPHP(Laravel)なのですが、今回はYappliのメイン言語のGoをつかってチェックすることにしました!

実装したコード

プリントデバッグが残ってないかチェックするスクリプト

package main

import (
    "bufio"
    "errors"
    "flag"
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

var directories = flag.String("directories", "", "directories to search. if you specify more than one, separate them with commas. eg. app,vendor")

func main() {
    flag.Parse()

    if err := Main(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func Main() error {
    find := false
    for _, v := range strings.Split(*directories, ",") {
        b, err := walk(v)
        if err != nil {
            return err
        }
        if b && !find {
            find = true
        }
    }

    if find {
        return errors.New("found a print debug, check the output above")
    }

    return nil
}

func walk(root string) (bool, error) {
    fmt.Printf("# root: %s\n", root)

    find := false
    err := filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        switch filepath.Ext(path) {
        case ".php":
            fmt.Printf("## path: %s\n", path)
            b, err := search(path)
            if err != nil {
                return err
            }
            if b && !find {
                find = true
            }
        }

        return nil
    })

    return find, err
}

func search(path string) (bool, error) {
    file, err := os.Open(path)
    if err != nil {
        return false, err
    }

    fs := bufio.NewScanner(file)
    i := 0
    find := false
    for fs.Scan() {
        i++
        if isPrintDebug(fs.Text()) {
            fmt.Printf("- [ ] find print debug@%s:%d, %s\n", path, i, strings.TrimSpace(fs.Text()))
            find = true
        }
    }

    return find, fs.Err()
}

func isPrintDebug(s string) bool {
    for _, v := range []string{"print", "print_r", "var_dump", "var_export", "echo"} {
        if strings.Contains(s, v) {
            i := strings.Index(s, v) + len(v)

            // コメント行は無視する
            if strings.TrimSpace(s)[:2] == "//" {
                continue
            }

            // ユーザ定義関数は無視する
            if s[i:i+1] != "(" && s[i:i+1] != " " {
                continue
            }

            switch v {
            case "print_r", "var_export":
                // trueがないとNG
                return strings.Index(s[i:], ", true)") == -1
            default:
                return true
            }

        }
    }
    return false
}

envファイルにexample.comが含まれていないかをチェックするスクリプト

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    if err := Main(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func Main() error {
    if err := checkNGWords(".env.local"); err != nil {
        return err
    }
    if err := checkNGWords(".env.prod"); err != nil {
        return err
    }
    return nil
}

func checkNGWords(name string) error {
    file, err := os.Open(name)
    if err != nil {
        return err
    }

    fs := bufio.NewScanner(file)
    i := 0
    for fs.Scan() {
        i++
        for _, v := range ngWords() {
            if strings.Contains(fs.Text(), v) {
                return fmt.Errorf("%s found on line:%d@%s", v, i, name)
            }
        }
    }
    return fs.Err()
}

func ngWords() []string {
    return []string{
        "example.com",
    }
}

GitHub Actionsの設定

name: Search Print Debug
on:
  push:

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v2
        with:
          go-version: ^1.17
      - uses: actions/checkout@v2

  search:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: go run tools/search_print_debug/main.go -directories=app,tests

今回はビルドせずに直接ファイルを実行しています。 config-checkerもrunの部分が、- run: go run tools/config-checker/main.go となるだけなので、詳細は割愛します。

ディレクトリ構成

$ tree -a -P 'search_print_debug.yml|check_config.yml|main.go' --prune
.
├── .github
│   └── workflows
│       └── search_print_debug.yml
└── tools
    ├── config-checker
    │   └── main.go
    └── search_print_debug
        └── main.go

まとめ

という感じで、簡易的ではありますがvar_dumpの消し忘れや、残しておきたくない設定値のチェックの自動化ができるようになりました!

ちょっとした改善ですが、誰でもやりかねないミスをチェックしてくれるということで、 導入後は結構な安心感を生み出してくれています!