paint-brush
Static Analyzers in PHP by@sercit
473 reads
473 reads

Static Analyzers in PHP

by Semen RusinJuly 29th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Static analyzer is very useful thing, which you can easily implement in your project. Here you can find the comparison table and some examples of their differences
featured image - Static Analyzers in PHP
Semen Rusin HackerNoon profile picture

Intro

In this article I’ll try to tell you about static analyzers and what they can bring to the code, to share with you possible difficulties, and to show you how the developer and analyzer work together.


For my examples, I’ll use PHP, although solutions such as SonarQube have implementations in many other languages as well.


So, let’s begin.

What Are Analyzers For?

Code analysis is an integral part of any developing process, on an equal level with testing and code reviews. Frequently, developers review the same code tens of times, checking that everything written fits the standards of code writing in the project, in the team, and in the company.


In order to offload developers and speed up manual code analysis, various code analyzers were created.


If the analyzer is set up correctly, the development process is accelerated, and the code quality increases.


Static code analysis allows you to view the entire application code at once without the developer's participation which makes it different from manual or dynamic analysis. Static analysis, as the name says, does not run the code, but only looks through it for errors in the code, such as incorrectly specified types in function arguments, unused variables, etc.


Static code analysis is performed by any team, even when developing alone. When writing code, every developer examines it for possible problems and smells. Because of this, a developer may make a mistake in the business logic when checking additional, technical parts of the work.

Solutions for Static Code Analysis in PHP

There are a lot of static analyzers available for PHP at the moment. Today, we will go through them with a short review of each, and then compare them so that you can choose the right one for your project.

Short Review

PHPStan

Founded in 2016, it is one of the most popular static analyzers in PHP. At the moment, the project is supported by several developers, and the number of downloads of the package has exceeded 127 million.


Among the main differences of PHPStan is the built-in support for plugins (available for example to work with modern frameworks). Has the ability to specify the level of checks. Does not have error types which is sometimes inconvenient when you need to enable or disable a particular one.


Also has a paid GUI version of the application instead of CLI, which is important for some people.

Phan

In development since 2015, Phan has over 5 million downloads. Phan is much less popular but has serious weight as Rasmus Lerdorf, one of the creators of PHP, was also involved in its development.


Among the advantages of Phan are its ability to act as a Language Server and real static. Phan does not attach the files it needs. It first builds the project tree using the PHP-ast library and then analyzes the files it needs. This method allows for faster analysis but has drawbacks.


For example, Phan cannot count PHPDoc blocks inside methods, which often leads to false positives.

Psalm

Founded by Matt Brown of Vimeo in 2014, Psalm is also one of the top solutions for PHP. The project currently has over 33 million downloads.


Can mention logic assertion checks and security hole checks as key features. Can perform some simple edits by itself. It also has an advantage over PHPStan, as it does not load unused files when checking part of the code base.


On the plus side, it has excellent documentation where you can find all types of errors, as well as play in the sandbox and send to a colleague if necessary. It also has its own set of custom PHPDoc blocks for better analysis, e.g., type analysis.

Mess Detector

Created in 2005, Mess Detector has already been downloaded more than 65 million times. Mess Detector differs from all the analyzers mentioned before in that it analyzes code from the point of view of finding code with smells or without clean code principles.


With its help, we can find out which classes we need to refactor because they have a lot of code/logical operations, or we can also see places not following design principles and clean code.

PHP_CodeSniffer

Its first version was released back in 2003, which puts it among the oldest static analyzers in PHP. Despite this, it is still being updated and has been downloaded more than 193 million times so far.


The main difference with CodeSniffer is that it checks code for code style, which reduces its capabilities but gives it the speed of execution and the ability to fix many errors.

Comparison

Since all analyzers have a lot of possibilities, it is sometimes difficult to make a choice among the described solutions. Let's try to arrange the data by analyzers with the help of the table:

Feature

PhpStan

Psalm

Phan

Mess Detector

CodeSniffer

Sandbox

+

+

+

-

-

CodeStyle

-

-

-

-

+

Variable Types

+

+

+

-

+

Method return types

+

+

+

-

-

Language Server

extension

+

plugin

-

-

IDE Integration

+

+

w/o PHPStorm

+

+

Plugins

+

+

+

-

-

Speed

Fast, speed decreases as the complexity of the project increases

Fast

Medium

Fast

Fast

Autofixer

+

Psalter

+

-

+

Method/Class size check

-

-

-

+

-

Сheck for unenforceable conditions

+

+

+

-

-

PHPDoc check

+

+

+/-

-

+

Union types check

+

+

+

-

+

Array shapes
support

+

+

-

-

-

Possible Problems During Implementation

The main problems during implementation are developers' unwillingness to get used to a new tool and incorrect initial settings. Due to the fact that when setting a more strict level of checks, the static analyzer often generates a huge number of errors, which greatly affects the psychological state of the team. Each static analyzer has 3 tools to solve this problem:


  • Level - indicates the level of severity of startup checks. The easiest way to reduce the number of startup errors. After adding startup to the standard development process, you can gradually increase the level.


  • Baseline - writes errors during the check to a file so that the analyzer does not generate errors that are already in the file during the next runs. With the help of this tool, you can remove old errors at once and keep an eye on not adding new ones. You can also create a task pool to gradually get rid of errors from Baseline.


  • Diff - helps to check only the code that has been modified by the developer. Due to the use of links, namespaces, and autoloaders, it is not always a preferable option because it cannot check the work of the whole application.


  • However, the speed of the analyzer's work in this case motivates the developer to run it more often. It is not worth using it in CI because you need to make sure that the whole application works correctly before sending it to the extension.

    Examples

    To test PHPStan, Phan, and Psalm, I used a large file with more than 700 lines. After the test launch of all three analyzers, it turned out that there were only 11 common places in the code where all three analyzers produced a Warning or Error.

    Comparison amount of errors and warnings

    Let's go through the examples where some analyzers worked and others did not.

    PHPStan

    function getData(): ?Data;
    function setData(Data $data): void;
    $data = $user->getData(); 
    $anotherUser->setData($data);
    //PHPStan  Cannot call method setData() on \User|null.
    
    
    public function getPaginatedList(array $ids, int $page, int $pageSize): PaginatedList
    // PHPStan Method App\Service\Service::getPaginatedList() has parameter // $ids with no value type specified in iterable type array.          
    // 💡 See:  //https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-it//erable-type 
    
    /** @var AnotherService */
    private $anotherService;
    private $logger;
    //PHPStan Property App\Service\Service::$logger has no type specified
    

Phan

if (!empty($data['user']['address'])) {
    $this->geoService->searchByStreet($data['user']['address']);
}
//Phan: PhanTypeMismatchArgumentNullable Argument 1 ($address) is $data['user']['address'] of type ?false|?string but \App\Service\GeoService::searchByStreet() takes string defined at src/Service/GeoService.php:35 (expected type to be non-nullable)
/** @var EntityManagerInterface */
private $entityManager;

public function __construct(
    EntityManagerInterface $entityManager
) {
    $this->entityManager = $entityManager;    
}

$repository = $this->entityManager->getRepository(User::class);
//Phan: PhanUndeclaredMethod Call to undeclared method \Doctrine\ORM\EntityManagerInterface::getRepository

Psalm

$doc = new DocumentDto(
$data->getDocument()['series'],
$data->getDocument()['number'],
new \DateTimeImmutable(
    $data->getDocument()['issuing_date']
));
//Psalm: INFO: PossiblyNullArgument - src/Service/Service.php:301:20 - Argument 2 of App\Dto\DocumentDto::__construct cannot be null, possibly null value provided (see https://psalm.dev/078)
//  $data->getDocument()['number'],
public function doSomething(): ?string;

public function upsertAndDoSomething(
    int $id,
    Info $info
): string {
    return $this->doSomething();
}
//INFO: InvalidNullableReturnType - src/Service/Service.php:113:8 - The declared return type 'string' for App\Service\Service::upsertAndDoSomething is not nullable, but 'null|string' contains null (see https://psalm.dev/144)
): string {

PHPMD

Clean Code

FILE: /app/src/Validator/Validator.php
------------------------------------------------------------------------
 42   | VIOLATION | Avoid using static access to class 'Enum' in method 'validate'.
 55   | VIOLATION | Avoid assigning values to variables in if clauses and the like (line '61', column '13').
 55   | VIOLATION | Avoid assigning values to variables in if clauses and the like (line '66', column '15').
 67   | VIOLATION | Avoid using static access to class '\App\Helpers\Helper' in method 'map'.
 70   | VIOLATION | Avoid using static access to class '\App\Requests\Request' in method 'map'.

Design

FILE: /app/src/Service/Service.php
-----------------------------------------------
 30 | VIOLATION | The class Service has a coupling between objects value of 24. Consider to reduce the number of dependencies under 13.

Unused Code

FILE: /app/src/Service/Service.php
------------------------------------------------
 31  | VIOLATION | Avoid unused parameters such as '$token'.
 32  | VIOLATION | Avoid unused parameters such as '$user'.
 33  | VIOLATION | Avoid unused parameters such as '$url'.

Codesize

FILE: /app/src/Consumer/Consumer.php
----------------------------------------------
 63   | VIOLATION | The method __construct has 10 parameters. Consider reducing the number of parameters to less than 10.
 92   | VIOLATION | The method execute() has a Cyclomatic Complexity of 22. The configured cyclomatic complexity threshold is 10.
 92   | VIOLATION | The method execute() has an NPath complexity of 6401. The configured NPath complexity threshold is 200.
 92   | VIOLATION | The method execute() has 124 lines of code. Current threshold is set to 100. Avoid really long methods.

PHPCS

FILE: src/Service/Service.php
------------------------------------------------------------------------------------------------------------------------------
FOUND 15 ERRORS AND 9 WARNINGS AFFECTING 22 LINES
------------------------------------------------------------------------------------------------------------------------------
 144 | WARNING | [ ] Line exceeds 120 characters; contains 151 characters
 184 | WARNING | [ ] Line exceeds 120 characters; contains 128 characters
 328 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 336 | WARNING | [ ] Line exceeds 120 characters; contains 130 characters
 366 | WARNING | [ ] Line exceeds 120 characters; contains 127 characters
 395 | ERROR   | [x] Empty lines are not allowed in multi-line function calls
 440 | ERROR   | [x] Opening brace must be the last content on the line
 440 | ERROR   | [x] Closing brace must be on a line by itself
 440 | ERROR   | [x] Each PHP statement must be on a line by itself
 481 | WARNING | [ ] Line exceeds 120 characters; contains 149 characters
 486 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 488 | WARNING | [ ] Line exceeds 120 characters; contains 122 characters
 503 | ERROR   | [x] Opening parenthesis of a multi-line function call must be the last content on the line
 545 | WARNING | [ ] Line exceeds 120 characters; contains 122 characters
 546 | WARNING | [ ] Line exceeds 120 characters; contains 138 characters
 587 | WARNING | [ ] Line exceeds 120 characters; contains 144 characters
 588 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 594 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 602 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 619 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 626 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 634 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 679 | ERROR   | [x] The first expression of a multi-line control structure must be on the line after the opening parenthesis
 724 | ERROR   | [x] Expected 1 space after closing brace; newline found
------------------------------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 15 MARKED SNIFF VIOLATIONS AUTOMATICALLY
------------------------------------------------------------------------------------------------------------------------------

Collective Use

As you can see in the examples - at the moment, every analyzer cannot meet all the desires and requirements of code coverage. That's why many teams that use such tools use several tools instead of one.


It helps to cover most of the errors and also to look at one and the same error from different sides, which makes it possible to fix it faster.

Conclusions

Despite the fact that static analyzers appeared quite a long time ago, due to the specifics of their implementation, they perform their work differently and have different results too. Each development team can choose the tool it needs.


In my opinion, using PHP_CodeSniffer is mandatory for all projects, and as for the choice between PHPStan/Phan/Psalm, we decided to use two of them in our project - Psalm and Phan because Psalm in our realities showed similar results to PHPStan, but has typed errors, which allows us to easily integrate the result with other services, such as Slack or Jira.