coc.nvimでPHPStanやPHP_CodeSnifferを使う

coc.nvimはVimのLSPクライアント・プラグインの一つです。

https://github.com/neoclide/coc.nvim

cocは自分自身がVimプラグインでありながら、拡張機能（エクステンション）の機構を持っており、LSPサーバをエクステンションをとしてインストールすることで機能を追加することができます。

PHPのLSPサーバのcocエクステンションもいくつかあります。例えばPHPのLSPサーバであるIntelephenseのcocエクステンションとしてcoc-phplsがあります。

https://github.com/marlonfan/coc-phpls

https://github.com/bmewburn/vscode-intelephense

coc-phplsをインストールするとcoc経由でPHPの自動補完・定義ジャンプ・ホバー（ヒント）表示・バグ検出などの機能を使えるようになります。

coc-phplsだけでもある程度のバグ検知は可能ですが、Intelephenseが提供するバグ検出は現時点ではPHPStanに比べると弱く、またPHP_CodeSnifferのようにコーディング規約違反を検出するものではありません。

こういうときに使えるのがdiagnostic-languageserverのcocエクステンションであるcoc-diagnosticです。

https://github.com/iamcco/coc-diagnostic

https://github.com/iamcco/diagnostic-languageserver

diagnostic-languageserverは各種linterと統合することで診断機能を提供する汎用言語サーバです。そのcocエクステンションであるcoc-diagnosticを使えば、PHPStanやPHP_CodeSnifferをcoc経由で使えるようになります。

## インストール方法
1. coc-diagnosticをインストールする。

    ```vim
    :CocInstall coc-diagnostic
    ```

2. PHPStan、PHP_CodeSnifferをインストールする。

    ```console
    composer require --dev phpstan/phpstan
    ```
    ```console
    composer require squizlabs/php_codesniffer
    ```

3. `coc-settings.json`に下記を設定する。

```{~/.vim/coc-settings.json}
{
    "diagnostic-languageserver.filetypes": {
        "php": ["phpcs", "phpstan"]
    }
}
```

coc-diagnosticは[デフォルトの設定](https://github.com/iamcco/coc-diagnostic/blob/master/src/config.ts#L306)で`./vendor/bin/phpstan`や`./vendor/bin/phpcs`を実行するようになっているので、Composerでローカルインストールしたのであればこれだけで使えるはずです。

こんな感じでcoc経由でPHPStanやPHP_CodeSnifferの出力が見れると思います。

![スクリーンショット 2021-03-22 014726.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9588/210005d7-ea95-aaae-507a-1e76b2c79fd1.png)

## linterの設定をカスタマイズする
coc-diagnosticはlinterの設定をカスタマイズすることもできます。下記に例を載せます。

```{~/.vim/coc-settings.json}
{
    "diagnostic-languageserver.mergeConfig": true,
    "diagnostic-languageserver.linters": {
        "phpcs": {
            "command": "./tools/phpcs",
            "args": ["--report=emacs", "-s", "-"],
        },
        "phpstan": {
            "command": "./tools/phpstan"
        }
    }
}
```

`diagnostic-languageserver.mergeConfig`を`true`に設定すると、追加した設定をデフォルトの設定とマージしてくれるので、差分だけを記載することができます。

上記の例では`command`で実行ファイルのパスをComposerではなくPHIVEでローカルインストールした場合のパス（`./tools/phpstan`や`./tools/phpcs`）に変更しています。また、PHP_CodeSnifferのデフォルトの`args`から`"--standard=PSR2"`を削除することで、プロジェクトディレクトリ配下にある`phpcs.xml`を使うようにしています。

## Dockerコンテナ上のPHPを使う
私はDocker（Laradock）を使って開発しており、ローカルにはPHPをインストールしていません。なので当然ですが、ローカルのPHPStanやPHP_CodeSnifferを実行できないため、上記の設定では動きません。こんな場合でも設定をひと工夫すれば、少し無理やりですがDockerコンテナ上のPHPを使って動かすことができます。

```{~/.vim/coc-settings.json}
{
    "diagnostic-languageserver.mergeConfig": true,
    "diagnostic-languageserver.linters": {
        "ngmy.laradock.phive.phpcs": {
            "command": "docker",
            "debounce": 100,
            "rootPatterns": ["composer.json", "composer.lock", "vendor", ".git"],
            "args": ["exec", "-u", "laradock", "laradock_workspace_1", "php", "tools/phpcs", "--report=emacs"],
            "offsetLine": 0,
            "offsetColumn": 0,
            "sourceName": "phpcs",
            "formatLines": 1,
            "formatPattern": [
                "(^.*):(\\d+):(\\d+):\\s+(.*)\\s+-\\s+(.*)(\\r|\\n)*$",
                {
                    "sourceName": 1,
                    "sourceNameFilter": true,
                    "line": 2,
                    "column": 3,
                    "message": 5,
                    "security": 4
                }
            ],
            "securities": {
                "error": "error",
                "warning": "warning"
            }
        },
        "ngmy.laradock.phive.phpstan": {
            "command": "docker",
            "debounce": 100,
            "rootPatterns": ["composer.json", "composer.lock", "vendor", ".git"],
            "args": ["exec", "-u", "laradock", "laradock_workspace_1", "php", "tools/phpstan", "analyse", "--error-format", "raw", "--no-progress"],
            "offsetLine": 0,
            "offsetColumn": 0,
            "sourceName": "phpstan",
            "formatLines": 1,
            "formatPattern": [
                "^/var/www/([^:]+):(\\d+):(.*)(\\r|\\n)*$",
                {
                    "sourceName": 1,
                    "sourceNameFilter": true,
                    "line": 2,
                    "message": 3
                }
            ]
        }
    },
    "diagnostic-languageserver.filetypes": {
        "php": ["ngmy.laradock.phive.phpcs", "ngmy.laradock.phive.phpstan"]
    }
}
```

PHPStanとPHP_CodeSnifferの新しいlinter設定を追加しています。設定内容はデフォルトの設定をベースにしています。デフォルトの設定を上書きしていない理由は、Docker越しに使うという用途が特殊なので、デフォルトの設定を残しておきたいと思ったからです。設定名はなんでも良いのですが、デフォルトの設定と被らないようにしています。

`command`と`args`を使って`docker exec`を実行することで、Dockerコンテナ（私の場合はLaradockのworkspaceコンテナ）でPHPStanやPHP_CodeSnifferを実行するようにしています。

PHPStanのデフォルトの`args`の最後にある`"%file"`を削除しています。`%file`はdiagnostic-languageserverが用意している`args`で使える変数で、Vimで開いているファイルのフルパスに展開されます。他にも[いくつかの変数](https://github.com/iamcco/diagnostic-languageserver#args-additional-syntax)が用意されています。削除する理由は、フルパスに展開されるとローカルのパスとDockerコンテナ上のパスが食い違ってしまい解析できないためです。削除すると、プロジェクトディレクトリ配下の`phpstan.neon`を使うようになるので、そこに記載されている全対象ファイルに対してPHPStanが実行されるようになります。

全対象ファイルに対して実行されたPHPStanの出力を今Vimで開いているファイルに対してだけ表示するために、`formatPattern`の正規表現を変更して相対パスを抽出して`SourceName`として使用しています。さらに複数ファイルを開いたときに出力が混ざらないように、`sourceNameFilter`を`true`にしています。

もし相対パスに展開してくれる変数があればそれで済む話ですが、[今のところない](https://github.com/iamcco/diagnostic-languageserver/issues/27)ようなので、こんな設定をしています。

同じ理由で、PHP_CodeSnifferも`args`から`"-s", "-"`を削除して、`formatPattern`を変更しています。

`diagnostic-languageserver.mergeConfig`を`true`にしているのはコンテナ名を上書きできるようにするためです（後述）。

全対象ファイルに対して実行しているせいか少し処理が遅い気がしますが、数十ファイル程度の小さなプロジェクトであれば許容範囲内に感じています。

## 任意のDockerコンテナ上のPHPを使う
プロジェクトごとにLaradockをインストールしている場合、プロジェクトごとにコンテナ名が異なると思います。そういうときはプロジェクトディレクトリの配下に`.vim/coc-settings.json`を作ります。

cocは`~/.vim/coc-settings.json`とプロジェクトディレクトリ配下の`.vim/coc-settings.json`をマージしてくれます。この辺りの仕様については[ここ](https://github.com/neoclide/coc.nvim/wiki/Using-the-configuration-file#configuration-file-resolve)に記載があります。

```{some-project/.vim/coc-settings.json}
{
    "diagnostic-languageserver.linters": {
        "ngmy.laradock.phive.phpcs": {
            "args": ["exec", "-u", "laradock", "laradock-some-project_workspace_1", "php", "tools/phpcs", "--report=emacs"]
        },
        "ngmy.laradock.phive.phpstan": {
            "args": ["exec", "-u", "laradock", "laradock-some-project_workspace_1", "php", "tools/phpstan", "analyse", "--error-format", "raw", "--no-progress"],
        }
    }
}
```

`args`でコンテナ名を変更しています。

こんな感じの設定を、cocを使いたい各プロジェクトに書いています。

以上です。

## 2021年5月7日追記
diagnostic-languageserverに相対パスに展開してくれる変数`%relativepath`が追加されたので、Dockerコンテナ上のPHPを使う場合、下記の設定で単一のファイルに対してのみlinterを実行できるようになりました。

```{~/.vim/coc-settings.json}
{
    "diagnostic-languageserver.mergeConfig": true,
    "diagnostic-languageserver.linters": {
        "ngmy.laradock.phive.phpcs": {
            "command": "docker",
            "debounce": 100,
            "rootPatterns": ["composer.json", "composer.lock", "vendor", ".git"],
            "args": ["exec", "-u", "laradock", "laradock-workspace_1", "php", "tools/phpcs", "--report=emacs", "%relativepath"],
            "offsetLine": 0,
            "offsetColumn": 0,
            "sourceName": "phpcs",
            "formatLines": 1,
            "formatPattern": [
                "^.*:(\\d+):(\\d+):\\s+(.*)\\s+-\\s+(.*)(\\r|\\n)*$",
                {
                    "line": 1,
                    "column": 2,
                    "message": 4,
                    "security": 3
                }
            ],
            "securities": {
                "error": "error",
                "warning": "warning"
            }
        },
        "ngmy.laradock.phive.phpstan": {
            "command": "docker",
            "debounce": 100,
            "rootPatterns": ["composer.json", "composer.lock", "vendor", ".git"],
            "args": ["exec", "-u", "laradock", "laradock-workspace_1", "php", "tools/phpstan", "analyse", "--error-format", "raw", "--no-progress", "%relativepath"],
            "offsetLine": 0,
            "offsetColumn": 0,
            "sourceName": "phpstan",
            "formatLines": 1,
            "formatPattern": [
                "^[^:]+:(\\d+):(.*)(\\r|\\n)*$",
                {
                    "line": 1,
                    "message": 2
                }
            ]
        }
    },
    "diagnostic-languageserver.filetypes": {
        "php": ["ngmy.laradock.phive.phpcs", "ngmy.laradock.phive.phpstan"]
    }
}
```

```{some-project/.vim/coc-settings.json}
{
    "diagnostic-languageserver.linters": {
        "ngmy.laradock.phive.phpcs": {
            "args": ["exec", "-u", "laradock", "laradock-some-project_workspace_1", "php", "tools/phpcs", "--report=emacs", "%relativepath"]
        },
        "ngmy.laradock.phive.phpstan": {
            "args": ["exec", "-u", "laradock", "laradock-some-project_workspace_1", "php", "tools/phpstan", "analyse", "--error-format", "raw", "--no-progress", "%relativepath"],
        }
    }
}
```