Phan - statyczny analizator PHP

Tomasz Tybulewicz
March 3, 2016

AST

Wśród wielu nowości w PHP 7 pojawiło się m.in. ASTabstrakcyjne drzewo składni, które jest wykorzystywane wewnątrz języka do analizy składni. Po doinstalowaniu rozszerzenia php-ast, drzewo to jest dostępne również dla kodu PHP w Twojej aplikacji. Właśnie z tego korzysta Phan podczas analizy kodu.

Phan

W listopadzie 2014 Rasmus Lerdorf ogłosił rozpoczęcie prac nad nowym analizatorem statycznym wykorzystującym AST, pół roku później pokazał wersję “proof of concept” która po upływie następnych 6 miesięcy została przejęta i przepisana przez Andrew Morrisona (współpracownika Rasmusa w Etsy). Od tego czasu Andrew rozwija kod udostępniony na GitHub i przygotowuje się do wypuszczenia pierwszej stabilnej wersji.

Użycie Phan w projekcie

Podczas analizy Phan znajduje wiele różnych typów błędów – poza “normalnymi” błędami składni znajduje też prawdopodobne pomyłki programisty. Takie błędy które są poprawnym kodem PHP, ale zachodzi podejrzenie że intencje programisty były inne niż te które zostały zaprogramowane. Obszerny opis wykrywanych pomyłek można znaleźć na odpowiedniej stronie projektu, ale żeby zgłaszane błędy miały sens trzeba trochę się przygotować.

<?php

require_once 'vendor/autoload.php';

use RamseyUuidUuid;

class User
{
    /**
     * @var Uuid
     */
    private $id;

    public function __construct()
    {
        $this->id = Uuid::uuid4();
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }
}

Spróbujmy sprawdzić tę klasę:

phan User.php

Dostajemy listę błędów:

User.php:12 PhanUndeclaredTypeProperty Property of undeclared type ramseyuuiduuid
User.php:16 PhanUndeclaredClassMethod Call to method uuid4 from undeclared class ramseyuuiduuid
User.php:24 PhanTypeMismatchReturn Returning type ramseyuuiduuid but getId() is declared to return int

Pierwsze dwa błędy oznaczają, że Phan nie zna klasy Uuid, do której odwołujemy się w kodzie. Wystarczy dodać odpowiednie pliki do analizy:

phan -l vendor User.php

Parametr -l dodaje katalog vendor do listy analizowanych plików.

User.php:24 PhanTypeMismatchReturn Returning type ramseyuuiduuid|ramseyuuiduuidinterface but getId() is declared to return int
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:34 PhanUndeclaredClassMethod Call to method convertToBase10 from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:36 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:46 PhanUndeclaredTypeParameter Parameter of undeclared type moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:49 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:52 PhanUndeclaredClassMethod Call to method convertFromBase10 from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Number/DegradedNumberConverter.php:50 PhanSignatureMismatch Declaration of function toHex(mixed $integer) : void should be compatible with function toHex(mixed $integer) : string defined in vendor/ramsey/uuid/src/Converter/NumberConverterInterface.php:43
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:38 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:40 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:41 PhanUndeclaredClassMethod Call to method multiply from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:43 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:44 PhanUndeclaredClassMethod Call to method multiply from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:46 PhanUndeclaredClassMethod Call to method add from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:50 PhanUndeclaredClassMethod Call to method convertToBase from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Converter/Time/DegradedTimeConverter.php:35 PhanSignatureMismatch Declaration of function calculateTime(string $seconds, string $microSeconds) : void should be compatible with function calculateTime(string $seconds, string $microSeconds) : string[] defined in vendor/ramsey/uuid/src/Converter/TimeConverterInterface.php:32
vendor/ramsey/uuid/src/DegradedUuid.php:35 PhanUndeclaredClassMethod Call to method __construct from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/DegradedUuid.php:36 PhanUndeclaredClassMethod Call to method subtract from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/DegradedUuid.php:37 PhanUndeclaredClassMethod Call to method divide from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/DegradedUuid.php:38 PhanUndeclaredClassMethod Call to method round from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/DegradedUuid.php:39 PhanUndeclaredClassMethod Call to method getValue from undeclared class moontoastmathbignumber
vendor/ramsey/uuid/src/Generator/CombGenerator.php:91 PhanTypeMismatchReturn Returning type string but timestamp() is declared to return int
vendor/ramsey/uuid/src/Generator/DefaultTimeGenerator.php:88 PhanTypeMismatchArgument Argument 1 (seconds) is int but ramseyuuidconvertertimeconverterinterface::calculatetime() takes string defined at vendor/ramsey/uuid/src/Converter/TimeConverterInterface.php:32
vendor/ramsey/uuid/src/Generator/DefaultTimeGenerator.php:88 PhanTypeMismatchArgument Argument 2 (microSeconds) is int but ramseyuuidconvertertimeconverterinterface::calculatetime() takes string defined at vendor/ramsey/uuid/src/Converter/TimeConverterInterface.php:32
vendor/ramsey/uuid/src/Generator/PeclUuidRandomGenerator.php:33 PhanUndeclaredFunction Call to undeclared function uuid_create()
vendor/ramsey/uuid/src/Generator/PeclUuidRandomGenerator.php:35 PhanUndeclaredFunction Call to undeclared function uuid_parse()
vendor/ramsey/uuid/src/Generator/PeclUuidTimeGenerator.php:34 PhanUndeclaredFunction Call to undeclared function uuid_create()
vendor/ramsey/uuid/src/Generator/PeclUuidTimeGenerator.php:36 PhanUndeclaredFunction Call to undeclared function uuid_parse()
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:31 PhanUndeclaredTypeProperty Property of undeclared type randomlibgenerator
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:41 PhanUndeclaredTypeParameter Parameter of undeclared type randomlibgenerator
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:46 PhanUndeclaredClassMethod Call to method __construct from undeclared class randomlibfactory
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:48 PhanUndeclaredClassMethod Call to method getMediumStrengthGenerator from undeclared class randomlibfactory
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:60 PhanUndeclaredClassMethod Call to method generate from undeclared class randomlibgenerator
vendor/ramsey/uuid/src/Generator/SodiumRandomGenerator.php:34 PhanUndeclaredFunction Call to undeclared function sodiumrandombytes_buf()
vendor/ramsey/uuid/src/Provider/Node/FallbackNodeProvider.php:55 PhanTypeMismatchReturn Returning type null but getNode() is declared to return string
vendor/ramsey/uuid/src/Provider/Node/SystemNodeProvider.php:35 PhanTypeMismatchReturn Returning type null but getNode() is declared to return string
vendor/ramsey/uuid/src/Uuid.php:211 PhanUndeclaredProperty Reference to undeclared property ramseyuuiduuidinterface->codec
vendor/ramsey/uuid/src/Uuid.php:212 PhanUndeclaredProperty Reference to undeclared property ramseyuuiduuidinterface->converter
vendor/ramsey/uuid/src/Uuid.php:213 PhanUndeclaredProperty Reference to undeclared property ramseyuuiduuidinterface->fields
vendor/ramsey/uuid/src/Uuid.php:233 PhanSignatureMismatch Declaration of function equals($other) should be compatible with function equals(object $other) : bool defined in vendor/ramsey/uuid/src/UuidInterface.php:51
vendor/ramsey/uuid/src/UuidFactory.php:163 PhanSignatureMismatch Declaration of function fromBytes($bytes) should be compatible with function fromBytes(string $bytes) : ramseyuuiduuidinterface defined in vendor/ramsey/uuid/src/UuidFactoryInterface.php:68
vendor/ramsey/uuid/src/UuidFactory.php:168 PhanSignatureMismatch Declaration of function fromString($uuid) should be compatible with function fromString(string $uuid) : ramseyuuiduuidinterface defined in vendor/ramsey/uuid/src/UuidFactoryInterface.php:76
vendor/ramsey/uuid/src/UuidFactory.php:181 PhanSignatureMismatch Declaration of function uuid1($node = null, $clockSeq = null) should be compatible with function uuid1(int|null|string $node = null, int|null $clockSeq = null) : ramseyuuiduuidinterface defined in vendor/ramsey/uuid/src/UuidFactoryInterface.php:33
vendor/ramsey/uuid/src/UuidFactory.php:189 PhanSignatureMismatch Declaration of function uuid3($ns, $name) should be compatible with function uuid3(string $ns, string $name) : ramseyuuiduuidinterface defined in vendor/ramsey/uuid/src/UuidFactoryInterface.php:43
vendor/ramsey/uuid/src/UuidFactory.php:206 PhanSignatureMismatch Declaration of function uuid5($ns, $name) should be compatible with function uuid5(string $ns, string $name) : ramseyuuiduuidinterface defined in vendor/ramsey/uuid/src/UuidFactoryInterface.php:60

Uuups, cała masa błędów w załączonej bibliotece, trzeba ją wykluczyć z analizy:

phan -l vendor -3 vendor User.php

Kolejny parametr: -3 to lista katalogów które nie będą analizowane pod względem poprawności, tym razem wynik jest zgodny z planem:

User.php:24 PhanTypeMismatchReturn Returning type ramseyuuiduuid|ramseyuuiduuidinterface but getId() is declared to return int

Co w końcu znajduje oczekiwany problem – niezgodność deklaracji PhpDoc z działaniem metody.

Optymalizacja Phan

Dodanie całego katalogu vendors do parsowania jest bardzo wygodne, ale przy ciut większych projektach wydłuża czas analizy oraz zwiększa apetyt na pamięć (w moich testach doszedłem do 30s analizy i 2GB pamięci).

Skorzystałem z możliwości pliku konfiguracyjnego Phan (domyślna lokalizacja to .phan/config.php) – wszystkie możliwe ustawienia są opisane w pliku Config.php, ja potrzebowałem trzech ustawień:

<?php
return [
    'file_list' => [
        'app/AppKernel.php',
        'vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php',
        // 87 innych plików z vendor
        'vendor/symfony/symfony/src/Symfony/Component/Yaml/Yaml.php',
    ],
    'directory_list' => [
        'src',
    ],
    'exclude_analysis_directory_list' => [
        'vendor/',
    ],
];

W tym przykładzie do analizy dodałem cały katalog src, ze sprawdzania poprawności wyłączyłem vendors/ oraz dodałem do parsowania 90 plików (w większości z katalogu vendors). Dzięki tej konfiguracji czas badania źródeł projektu spadł do 2 sekund. Co jakiś czas (przy odwołaniu się do nowego interface’u) trzeba dodać kolejne pliki do file_list ale jesteśmy gotowi na taki obowiązek.

Podsumowanie

Phan jest u nas kolejnym narzędziem, którym badamy kod, cenimy w nim to że znajduje problemy pomijane przez pozostałe narzędzia (choćby wspomniana analiza PhpDoc).

Phan jest dynamicznie rozwijany i dopier zbliża się do pierwszej stabilnej wersji, oznacza to że często najnowszy kod (z gałęzi master) nie działa tak jak trzeba – po aktualizacji pojawiają się fałszywe błędy w kodzie które są wynikiem pomyłki w Phan. Na szczęście developerzy projektu szybko reagują na zgłoszenia błędów i w ciągu jednego dnia poprawiają zgłoszone usterki.

Po osiągnięciu dojrzałości projektu, Phan będzie bardzo dobrym uzupełnieniem narzędzi wspierających CI oraz przeglądy kodu, w tej chwili używamy go “ręcznie” analizując kod przed dodaniem do repozytorium.

Linki

Chcesz poznać nas lepiej? Dowiedz się, co nas wyróżnia.