Статический анализ для PHP

В этой статье можно узнать что такое статические анализаторы кода, какова от них польза в php, какие для этого есть готовые инструменты. Будет кратко описан каждый инструмент, его возможности и процесс работы с ним.

Содержание статьи

Для чего нужны статические анализаторы

Статические анализаторы кода обрабатывают код (не выполняя его при этом) для поиска в нем ошибок и даже уязвимостей. Они проводят различные проверки. Например, на существование классов, функций и переменных. Проверяют соответствие типов параметров функций в их описании и того как эти функции вызываются. Учитывают комментарии phpDoc.

Если вы создаете свои проекты на php в развитой среде разработки, например, PHPStorm (Intellij IDEA), то вероятно заметили, что она подсвечивает проблемные места в коде. Но инструментов подобного рода больше. Также для проектов (особенно open source) с большим числом разработчиков (каждый со своей IDE или редактором) полезно иметь такие проверки, которые будут запущены для каждой ветки системы контроля версий, которую планируется объединить с основной веткой. Поэтому описанные здесь инструменты не привязаны к конкретной IDE, хотя и могут в отдельных случаях быть интегрированными в таковые.

PhpStan

PhpStan — один из самых популярных инструментов в этом роде. Он устанавливается как composer пакет

composer require --dev phpstan/phpstan

и запускает проверки консольной командой с указанием папок, которые необходимо проверить:

vendor/bin/phpstan analyse app tests
 72/72 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ --------------------------------------------------------------------- 
  Line   app/Http/Controllers/Api/AuthController.php                          
 ------ --------------------------------------------------------------------- 
  34     Call to static method once() on an unknown class Auth.               
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols   
 ------ --------------------------------------------------------------------- 

 ------ ------------------------------------------------------------------------- 
  Line   app/Providers/AppServiceProvider.php                                     
 ------ ------------------------------------------------------------------------- 
  18     Call to static method defaultStringLength() on an unknown class Schema.  
         💡 Learn more at https://phpstan.org/user-guide/discovering-symbols       
 ------ ------------------------------------------------------------------------- 

                                                                                                                        
 [ERROR] Found 2 errors                                                                                                 
                                                                                                                        

У phpStan есть 10 уровней проверки. По-умолчанию используется уровень — 0 (только базовые проверки). В данной статье используется проект laravel-realworld-example-app как подопытный для запуска проверок. Для этого проекта на уровне 0 можно получить всего 2 ошибки. На уровне 1 — уже 47.

vendor/bin/phpstan analyse -l 1 app tests
....
....
 ------ -------------------------------------------------------- 
  Line   tests/TestCase.php                                      
 ------ -------------------------------------------------------- 
  21     Function factory invoked with 1 parameter, 0 required.  
 ------ -------------------------------------------------------- 

                                                                                                                        
 [ERROR] Found 47 errors                                                                                                
                             

На уровне 6 — 243 ошибки.

vendor/bin/phpstan analyse -l 6 app tests
....
....
 ------ --------------------------------------------------------------- 
  Line   tests/TestCase.php                                             
 ------ --------------------------------------------------------------- 
  11     Property Tests\TestCase::$loggedInUser has no type specified.  
  13     Property Tests\TestCase::$user has no type specified.          
  15     Property Tests\TestCase::$headers has no type specified.       
  21     Function factory invoked with 1 parameter, 0 required.         
 ------ --------------------------------------------------------------- 

                                                                                                                        
 [ERROR] Found 243 errors                                                                                               
                                       

Указывать уровень и папки для проверки, как видно, можно прямо в команде запуска. Однако удобнее создать конфигурационный файл в формате yaml, где описать эти и многие другие настройки.

У PhpStan имеется возможность расширять функционал при помощи системы плагинов и есть интеграция с PhpStorm (IDEA).

Psalm

Psalm также устанавливается как пакет и запускает проверки при помощи консольной программы

composer require --dev vimeo/psalm

Но конкретно на нашем подопытном возникла проблема

Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires vimeo/psalm ^4.22 -> satisfiable by vimeo/psalm[4.22.0].
    - vimeo/psalm 4.22.0 requires sebastian/diff ^3.0 || ^4.0 -> found sebastian/diff[3.0.0, 3.0.1, 3.0.2, 3.0.3, 4.0.0, ..., 4.0.4] but the package is fixed to 2.0.1 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.

Для vimeo/psalm ^4.22 требуется sebastian/diff ^3.0 || ^4.0, однако из-за phpunit/phpunit 6 требуется sebastian/diff ^2.0. Поэтому, исключительно, чтобы побыстрее перейти к тому, что нас интересует сделаем в composer.json изменение уровня допустимой стабильности и разрешим phpunit версии 7.

...
    "minimum-stability" : "dev",
    "prefer-stable" : true,
...
    "phpunit/phpunit": "~7.0",

После чего установка vimeo/psalm должна пройти без проблем.

После нужно будет создать конфигурационный файл командой:

./vendor/bin/psalm --init
Calculating best config level based on project files
Target PHP version: 7.0 (inferred from composer.json)
Scanning files...
Analyzing files...

EEEEEEEEEEE░EEEE░░E░░░E░░░E░E░░EEEEEEEEE░E░E░EEEEEEEEEEE

Detected level 7 as a suitable initial default
Config file created successfully. Please re-run psalm.

Как можно видеть из выдачи команды подобран уровень (error level) 7. После этого можно производить запуск проверок:

./vendor/bin/psalm
Target PHP version: 7.0 (inferred from composer.json)
Scanning files...
Analyzing files...

░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░E░░E░░░░E░E░░░░░░

ERROR: ParamNameMismatch - \app/Exceptions/Handler.php:36:38\ - Argument 1 of App\Exceptions\Handler::report has wrong name $exception, expecting $e as defined by Illuminate\Contracts\Debug\ExceptionHandler::report (see https://psalm.dev/230)
    public function report(Exception $exception)


ERROR: ParamNameMismatch - \app/Exceptions/Handler.php:48:48\ - Argument 2 of App\Exceptions\Handler::render has wrong name $exception, expecting $e as defined by Illuminate\Contracts\Debug\ExceptionHandler::render (see https://psalm.dev/230)
    public function render($request, Exception $exception)


ERROR: NullReference - \app/Http/Controllers/Api/ApiController.php:45:41\ - Cannot call method collection on null value (see https://psalm.dev/016)
            $data = $this->transformer->collection($data);


ERROR: NullReference - \app/Http/Controllers/Api/ApiController.php:67:37\ - Cannot call method paginate on null value (see https://psalm.dev/016)
        $data = $this->transformer->paginate($paginated);


ERROR: UndefinedClass - \app/Http/Controllers/Api/AuthController.php:34:15\ - Class, interface or enum named Auth does not exist (see https://psalm.dev/019)
        if (! Auth::once($credentials)) {


ERROR: UndefinedClass - \app/Providers/AppServiceProvider.php:18:9\ - Class, interface or enum named Schema does not exist (see https://psalm.dev/019)
        Schema::defaultStringLength(191);


------------------------------
6 errors found
------------------------------
196 other issues found.
You can display them with --show-info=true
------------------------------
Psalm can automatically fix 11 of these issues.
Run Psalm again with 
--alter --issues=MissingReturnType,MissingParamType --dry-run
to see what it can fix.
------------------------------

Checks took 5.13 seconds and used 385.706MB of memory
Psalm was able to infer types for 81.6010% of the codebase

Есть возможность произвести более тонкую настройку Psalm через файл psalm.xml.

У данного пакета есть поддержка различных IDE. В том числе PhpStorm и VSCode. Можно расширять функционал плагинами.

Phan

Phan использует расширение php-ast, которое нужно будет установить отдельно. В противном случае мы получим ошибку «ERROR: The php-ast extension must be loaded in order for Phan to work.» После установки php расширения следует уже привычная нам установка composer пакета.

composer require phan/phan

Далее необходимо создать файл конфигурации. Конфигурация происходит в php файле .phan/config.php. В нем, прежде всего, нам нужно будет указать папки для проверки — directory_list. Этот файл можно создать вручную или командой

# --init-level can be anywhere from 1 (strictest) to 5 (least strict)
./vendor/phan/phan/phan --init --init-level=5

И после указания нужных настроек можно будет запускать

./vendor/phan/phan/phan
   analyze ████████████████████████████████████████████████████████████ 100.0% 82MB/82MB
app/Article.php:10 PhanUndeclaredExtendedClass Class extends undeclared class \Illuminate\Database\Eloquent\Model
app/Article.php:39 PhanUndeclaredProperty Reference to undeclared property \App\Article->tags (Did you mean \App\Article->tags())
app/Article.php:48 PhanUndeclaredTypeParameter Parameter $query has undeclared type \Illuminate\Database\Eloquent\Builder
app/Article.php:48 PhanUndeclaredTypeReturnType Return type of scopeLoadRelations() is undeclared type \Illuminate\Database\Eloquent\Builder
app/Article.php:50 PhanUndeclaredClassMethod Call to method with from undeclared class \Illuminate\Database\Eloquent\Builder
....
....
app/User.php:81 PhanUndeclaredClassMethod Call to method pluck from undeclared class \Illuminate\Database\Eloquent\Relations\BelongsToMany
app/User.php:83 PhanUndeclaredStaticMethod Static call to undeclared method \App\Article::loadRelations
app/User.php:103 PhanUndeclaredMethod Call to undeclared method \App\User::getKey

Как можно видеть Phan тоже поддерживает уровни строгости проверок. Также имеется возможность расширения функционала плагинами. А вот про интеграцию с IDE неизвестно ничего.

Проверка оформления кода

Завершающим штрихом к проверкам статическими анализаторам кода будет проверка оформления кода. Популярным решением для этого является пакет PhpCS (PHP_CodeSniffer).

PHP_CodeSniffer это набор из двух скриптов; главный скрипт (phpcs) проверяет PHP, JavaScript and CSS файлы и определяет нарушения определенных стандартов оформления кода, а второй скрипт (phpcbf) автоматически исправляет нарушения, когда это возможно.

Еще раз отмечу, что PHP_CodeSniffer не является статическим анализатором, но будет полезен как дополнительный инструмент поддержания кодовой базы в порядке при работе многих людей над общим проектом.

composer require "squizlabs/php_codesniffer=*"
/vendor/bin/phpcs app

FILE: /home/igor/projects/laravel-realworld-example-app/app/RealWorld/Filters/Filter.php
---------------------------------------------------------------------------------------------------------
FOUND 11 ERRORS AFFECTING 9 LINES
---------------------------------------------------------------------------------------------------------
  2 | ERROR | [ ] Missing file doc comment
  9 | ERROR | [ ] Missing doc comment for class Filter
 11 | ERROR | [ ] Missing short description in doc comment
 16 | ERROR | [ ] Missing short description in doc comment
 24 | ERROR | [ ] Missing parameter comment
 40 | ERROR | [x] Opening parenthesis of a multi-line function call must be the last content on the line
 40 | ERROR | [x] Expected 1 space after FUNCTION keyword; 0 found
 45 | ERROR | [x] Closing parenthesis of a multi-line function call must be on a line by itself
 63 | ERROR | [ ] Missing parameter comment
 63 | ERROR | [x] Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
 64 | ERROR | [ ] Tag @return cannot be grouped with parameter tags in a doc comment
---------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
---------------------------------------------------------------------------------------------------------


FILE: /home/igor/projects/laravel-realworld-example-app/app/RealWorld/Filters/Filterable.php
-----------------------------------------------------------------------------------------------
FOUND 8 ERRORS AFFECTING 5 LINES
-----------------------------------------------------------------------------------------------
  2 | ERROR | [ ] Missing file doc comment
  5 | ERROR | [ ] Missing doc comment for trait Filterable
 10 | ERROR | [ ] Missing parameter comment
 10 | ERROR | [x] Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
 11 | ERROR | [ ] Missing parameter comment
 11 | ERROR | [x] Expected 32 spaces after parameter type; 1 found
 11 | ERROR | [x] Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
 12 | ERROR | [ ] Tag @return cannot be grouped with parameter tags in a doc comment
-----------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 3 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-----------------------------------------------------------------------------------------------
....
....
./vendor/bin/phpcbf app

PHPCBF RESULT SUMMARY
-----------------------------------------------------------------------------------------------------------------------
FILE                                                                                                   FIXED  REMAINING
-----------------------------------------------------------------------------------------------------------------------
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Filters/Filter.php                     4      7
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Filters/Filterable.php                 3      5
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Filters/ArticleFilter.php              3      8
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Slug/Slug.php                          10     16
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Slug/HasSlug.php                       3      6
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Paginate/Paginate.php                  2      5
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Favorite/HasFavorite.php               4      9
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Favorite/Favoritable.php               3      5
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Follow/Followable.php                  7      12
/home/igor/projects/laravel-realworld-example-app/app/RealWorld/Transformers/Transformer.php           6      10
/home/igor/projects/laravel-realworld-example-app/app/Http/Middleware/AuthenticateWithJWT.php          6      8
/home/igor/projects/laravel-realworld-example-app/app/Http/Middleware/RedirectIfAuthenticated.php      3      6
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Auth/RegisterController.php     6      6
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/CommentController.php       10     13
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/FavoriteController.php      2      7
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/UserController.php          1      5
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/ArticleController.php       14     15
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/ProfileController.php       3      9
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/ApiController.php           26     36
/home/igor/projects/laravel-realworld-example-app/app/Http/Controllers/Api/AuthController.php          4      7
/home/igor/projects/laravel-realworld-example-app/app/Console/Kernel.php                               2      4
/home/igor/projects/laravel-realworld-example-app/app/Providers/RouteServiceProvider.php               5      2
/home/igor/projects/laravel-realworld-example-app/app/Providers/BroadcastServiceProvider.php           1      2
/home/igor/projects/laravel-realworld-example-app/app/Exceptions/Handler.php                           14     17
/home/igor/projects/laravel-realworld-example-app/app/Helpers/Helper.php                               3      6
/home/igor/projects/laravel-realworld-example-app/app/Article.php                                      8      4
-----------------------------------------------------------------------------------------------------------------------
A TOTAL OF 153 ERRORS WERE FIXED IN 26 FILES
-----------------------------------------------------------------------------------------------------------------------

Time: 380ms; Memory: 10MB

Настойки проверок PHP_CodeSniffer указываются в xml файле. Имеется интеграция с PhpStorm (IDEA).

Leave a Reply

Ваш адрес email не будет опубликован.