В этой статье можно узнать что такое статические анализаторы кода, какова от них польза в 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).