PHP 8 の一部の新機能について PHP_CodeSniffer が期待した動作をしないので 3.6.0 を待つ

はじめに

PHP 8.0.0 がリリースされてしばらく経ちました。
各種ライブラリや FW も PHP 8 対応されており、PHP 8 ライフも順調です。
ただ、お世話になっているPHP_CodeSnifferで一部まだうまく動作しない部分があったので紹介します。

先に言ってしまうと、PHP_CodeSniffer の 3.5.7 や 3.5.8 などの早い段階ですでに対応はなされており、基本的に PHP 8 上で動作しないというわけではありません。
新しい機能に合わせたインデント等を揃える綺麗な書き方について考慮されていないということです。

環境

実行環境は次の通りです。

  • PHP: 8.0.1
  • PHP_CodeSniffer: 3.5.8

また、コーディングスタンダードとしては個人的に一番お世話になっている PSR12 を採用して、以下の通りに実行します。

phpcs --standard=PSR12 /path/to/code/myfile.php

実例

一部の大きな機能だけ試してみようと思います。
具体的には PHP: PHP 8.0.0 Release Announcement で大きく紹介されている機能です。

Named arguments

次のコードに対して実行します。

htmlspecialchars($string, double_encode: false);
結果

OK

結論

問題なく動作するようです。

Attributes

次のコードに対して実行します。

class PostsController
{
    #[Route("/api/posts/{id}", methods: ["GET"])]
    public function get($id)
    {
        /* ... */
    }
}
結果

OK

結論

問題なく動作するようです。

Constructor property promotion

次のコードに対して実行します。

class Point
{
    public function __construct(
        public float $x = 0.0,
        public float $y = 0.0,
        public float $z = 0.0,
    ) {
    }
}
結果
---------------------------------------------------------------------------------------------------
 4 | ERROR | [x] Line indented incorrectly; expected 4 spaces, found 8
 5 | ERROR | [x] Line indented incorrectly; expected 4 spaces, found 8
 6 | ERROR | [x] Line indented incorrectly; expected 4 spaces, found 8
---------------------------------------------------------------------------------------------------

インデントがおかしいと怒られてしまいました。
具体的には Generic.WhiteSpace.ScopeIndent.IncorrectExact というルールです。

phpcbf で修正できそうなので修正してみます。

phpcbf を実行した結果、コードはこうなりました。

class Point
{
    public function __construct(
    public float $x = 0.0,
    public float $y = 0.0,
    public float $z = 0.0,
    ) {
    }
}

そして、これに対してphpcsを実行すると、こうなりました。

-------------------------------------------------------------------------------------------------------
 4 | ERROR | [x] Multi-line function declaration not indented correctly; expected 8 spaces but found 4
 5 | ERROR | [x] Multi-line function declaration not indented correctly; expected 8 spaces but found 4
 6 | ERROR | [x] Multi-line function declaration not indented correctly; expected 8 spaces but found 4
-------------------------------------------------------------------------------------------------------

さっきとは異なって、 Squiz.Functions.MultiLineFunctionDeclaration.Indent というインデントに関するエラーです。

結論

Constructor property promotion はインデントに関して期待した動作にならないようです。
そのため、Generic.WhiteSpace.ScopeIndent.IncorrectExact は一時的に除外する必要がありそうです。

Union types

次のコードに対して実行します。

class Number
{
    public function __construct(
        int|float $number
    ) {
    }
}
結果
---------------------------------------------------------------------------------------------------
 4 | ERROR | [x] Expected at least 1 space before "|"; 0 found
 4 | ERROR | [x] Expected at least 1 space after "|"; 0 found
---------------------------------------------------------------------------------------------------

Union Types の | の前後のスペースが無いというエラーが出ました。
具体的には、PSR12.Operators.OperatorSpacing.NoSpaceBeforePSR12.Operators.OperatorSpacing.NoSpaceAfter というエラーです。
これは、ビット演算子| と混同した結果だと思われます。

phpcbf で修正できそうなので修正してみます。

phpcbf を実行した結果、コードはこうなりました。

class Number
{
    public function __construct(
        int | float $number
    ) {
    }
}

そして、これに対して phpcs を実行するとエラーはなくなりました。

もちろん、Union Types としての意味は失われていません。

結論

Union Types の | の前後にスペースを入れれば問題ありません。
ただ、Union Types の書き方としてスペースを入れたくないのであれば、PSR12.Operators.OperatorSpacing.NoSpaceBeforePSR12.Operators.OperatorSpacing.NoSpaceAfter は一時的に除外する必要がありそうです。

Match expression

次のコードに対して実行します。

echo match (8.0) {
  '8.0' => "Oh no!",
  8.0 => "This is what I expected",
};
結果

OK

結論

問題なく動作するようです。

Nullsafe operator

次のコードに対して実行します。

$country = $session?->user?->getAddress()?->country;
結果

OK

結論

問題なく動作するようです。

どうすればいいか

もし、PHP 8 を使っていてコーディングスタンダードとして PSR12 を選択するなら、最低限次のように一部のルールを除外した方が良いかもしれません。

<?xml version="1.0"?>
<ruleset>
    <rule ref="PSR12">
        <exclude name="Generic.WhiteSpace.ScopeIndent.IncorrectExact"/>
    </rule>
    <file>app</file>
    <file>tests</file>
</ruleset>

Union Types の書き方で、| の前後にスペースを入れたくないのであれば、さらに、こうした方が良いかもしれません。

<?xml version="1.0"?>
<ruleset>
    <rule ref="PSR12">
        <exclude name="Generic.WhiteSpace.ScopeIndent.IncorrectExact"/>
        <exclude name="PSR12.Operators.OperatorSpacing.NoSpaceBefore"/>
        <exclude name="PSR12.Operators.OperatorSpacing.NoSpaceAfter"/>
    </rule>
    <file>app</file>
    <file>tests</file>
</ruleset>

万が一これらのルールを除外してしまっていたがためにコーディングルールに準拠できていない部分に気づけなかったとしても、これらルールは phpcbf で自動修正できるため、PHP_CodeSniffer の対応と今後のバージョンアップによってこれらの除外が不要になったときに自動で修正できるはずなので、あまり気にしなくても良さそうです。

3.6.0 に期待

PHP 8 のサポートについては議論されており、match 式や attributes に関してもまだ足りない部分があるようで、様々な対応が 3.6.0 に含まれているようです。
github.com

もちろん、先程出てきた Constructor property promotion や Union types での問題点も既に解消されているようです。
Generic.WhiteSpace.ScopeIndent false positive when using PHP 8.0 constructor property promotion · Issue #3167 · squizlabs/PHP_CodeSniffer · GitHub
PHP 8.0 | Support union types in PHP Tokenizer and File class utility methods by jrfnl · Pull Request #3032 · squizlabs/PHP_CodeSniffer · GitHub

そんな3.6.0のマイルストーンは現時点で 93% まで完了しており、リリースが待ち遠しいです。
3.6.0 Milestone · GitHub

おわりに

もちろん、コーディングスタンダードを PSR12 以外にした場合や、サンプルコードの書き方を変えた場合は、また別の問題が出てくるかもしれません。
が、普段 PHP 8 と PHP_CodeSniffer を使っていて遭遇した問題もこれらが多かったので、紹介の範囲はこれだけにとどめました。
先にあげたPHP 8 サポートの issue や PR を覗くと、逆にどういうコードで期待した動作にならないかわかるかもしれません。

こういうときに OSS 貢献できればいいのですが、さすがに難易度高いな〜 雑魚だな自分〜と思ってしまいます...