1

AST

Wśród wielu nowości w PHP 7 pojawiło się też AST - abstrakcyjne drzewo składni które jest wykorzystywane wewnątrz języka do analizy składni. Po doinstalowaniu rozszerzeniaphp-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 zostala przyjęta i przepisana przez Andrew Morssisona (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ć.
 

Klasa w PHP z drobnym błędem:

require_once 'vendor/autoload.php';

use \Ramsey\Uuid\Uuid;

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 \ramsey\uuid\uuid
User.php:16 PhanUndeclaredClassMethod Call to method uuid4 from undeclared class \ramsey\uuid\uuid
User.php:24 PhanTypeMismatchReturn Returning type \ramsey\uuid\uuid 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 \ramsey\uuid\uuid|\ramsey\uuid\uuidinterface but getId() is declared to return int
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:34 PhanUndeclaredClassMethod Call to method convertToBase10 from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:36 PhanUndeclaredClassMethod Call to method __construct from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:46 PhanUndeclaredTypeParameter Parameter of undeclared type \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:49 PhanUndeclaredClassMethod Call to method __construct from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Number/BigNumberConverter.php:52 PhanUndeclaredClassMethod Call to method convertFromBase10 from undeclared class \moontoast\math\bignumber
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 \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:40 PhanUndeclaredClassMethod Call to method __construct from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:41 PhanUndeclaredClassMethod Call to method multiply from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:43 PhanUndeclaredClassMethod Call to method __construct from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:44 PhanUndeclaredClassMethod Call to method multiply from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:46 PhanUndeclaredClassMethod Call to method add from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/Converter/Time/BigNumberTimeConverter.php:50 PhanUndeclaredClassMethod Call to method convertToBase from undeclared class \moontoast\math\bignumber
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 \moontoast\math\bignumber
vendor/ramsey/uuid/src/DegradedUuid.php:36 PhanUndeclaredClassMethod Call to method subtract from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/DegradedUuid.php:37 PhanUndeclaredClassMethod Call to method divide from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/DegradedUuid.php:38 PhanUndeclaredClassMethod Call to method round from undeclared class \moontoast\math\bignumber
vendor/ramsey/uuid/src/DegradedUuid.php:39 PhanUndeclaredClassMethod Call to method getValue from undeclared class \moontoast\math\bignumber
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 \ramsey\uuid\converter\timeconverterinterface::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 \ramsey\uuid\converter\timeconverterinterface::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 \randomlib\generator
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:41 PhanUndeclaredTypeParameter Parameter of undeclared type \randomlib\generator
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:46 PhanUndeclaredClassMethod Call to method __construct from undeclared class \randomlib\factory
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:48 PhanUndeclaredClassMethod Call to method getMediumStrengthGenerator from undeclared class \randomlib\factory
vendor/ramsey/uuid/src/Generator/RandomLibAdapter.php:60 PhanUndeclaredClassMethod Call to method generate from undeclared class \randomlib\generator
vendor/ramsey/uuid/src/Generator/SodiumRandomGenerator.php:34 PhanUndeclaredFunction Call to undeclared function \sodium\randombytes_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 \ramsey\uuid\uuidinterface->codec
vendor/ramsey/uuid/src/Uuid.php:212 PhanUndeclaredProperty Reference to undeclared property \ramsey\uuid\uuidinterface->converter
vendor/ramsey/uuid/src/Uuid.php:213 PhanUndeclaredProperty Reference to undeclared property \ramsey\uuid\uuidinterface->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) : \ramsey\uuid\uuidinterface 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) : \ramsey\uuid\uuidinterface 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) : \ramsey\uuid\uuidinterface 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) : \ramsey\uuid\uuidinterface 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) : \ramsey\uuid\uuidinterface 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 \ramsey\uuid\uuid|\ramsey\uuid\uuidinterface but getId() is declared to return int


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

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ń:

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/',
    ],
];


Podsumowanie

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.

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 dopiero 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

Autor: Tomasz Tybulewicz, Speednet Sp. z o.o.