Strict typing and static analysis

A presentation at PHPSW in January 2020 in Bristol, UK by Rob Allen

Slide 1

Slide 1

Strict typing & static analysis Rob Allen January 2020

Slide 2

Slide 2

Use Types to Help Focus on What You’re Doing, Not How You’re Doing It Anthony Ferrara, 2016 Rob Allen ~ @akrabat

Slide 3

Slide 3

PHP Types boolean float array callable resource integer string object iterable NULL Rob Allen ~ @akrabat

Slide 4

Slide 4

PHP is a dynamically typed language Rob Allen ~ @akrabat

Slide 5

Slide 5

Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); Rob Allen ~ @akrabat

Slide 6

Slide 6

Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” Rob Allen ~ @akrabat

Slide 7

Slide 7

Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” var_dump(“10” + 5); Rob Allen ~ @akrabat

Slide 8

Slide 8

Type Juggling PHP will automatically convert types when it can. var_dump(“Two + three = ” . 5); // string(15) “Two + three = 5” var_dump(“10” + 5); // int(15) Rob Allen ~ @akrabat

Slide 9

Slide 9

Rob Allen ~ @akrabat

Slide 10

Slide 10

But… $result = 10 + “10,000”; var_dump($result); Rob Allen ~ @akrabat

Slide 11

Slide 11

But… $result = 10 + “10,000”; var_dump($result); // int(20) Rob Allen ~ @akrabat

Slide 12

Slide 12

But… $result = “false”; if ($result) { echo “TRUE”; } else { echo “FALSE”; } Rob Allen ~ @akrabat

Slide 13

Slide 13

But… $result = “false”; if ($result) { echo “TRUE”; } else { echo “FALSE”; } // TRUE Rob Allen ~ @akrabat

Slide 14

Slide 14

Rob Allen ~ @akrabat

Slide 15

Slide 15

You can’t trust a computer! Rob Allen ~ @akrabat

Slide 16

Slide 16

Test all the things Rob Allen ~ @akrabat

Slide 17

Slide 17

Validate your inputs function foo($i, $b) { if (! is_int($i)) { throw new Exception(‘$i must be an integer’); } if (! is_bool($b)) { throw new Exception(‘$b must be a boolean’); } // … } & write tests for the new code-paths Rob Allen ~ @akrabat

Slide 18

Slide 18

A type system solves this class of errors Rob Allen ~ @akrabat

Slide 19

Slide 19

If we filter input by a type — you can think of it as a subcategory of all available input — many of the tests become obsolete. @brendt_gd Rob Allen ~ @akrabat

Slide 20

Slide 20

Function Type Declarations function foo(int $i, bool $b) : string { // … } • $i is always an integer • $b is always a boolean • foo() will always return a string Rob Allen ~ @akrabat

Slide 21

Slide 21

This function is much easier to reason about Rob Allen ~ @akrabat

Slide 22

Slide 22

Coercion of Typed Declarations function foo(int $i, bool $b) : string { // … } foo(“1”, 1); // $i = int(1) // $b = bool(true) Rob Allen ~ @akrabat

Slide 23

Slide 23

Strictly Typed Declarations declare(strict_types=1); foo(“1”, 1); Fatal error: Uncaught TypeError: Argument 1 passed to foo() must be of the type int, string given Rob Allen ~ @akrabat

Slide 24

Slide 24

This reduces cognitive load Rob Allen ~ @akrabat

Slide 25

Slide 25

Typed Properties in PHP 7.4! We can now add types to class properties class Person { public int $age; } Rob Allen ~ @akrabat

Slide 26

Slide 26

Initialisation of Typed Properties $liz = new Person(); var_dump($liz->age); Rob Allen ~ @akrabat

Slide 27

Slide 27

Initialisation of Typed Properties $liz = new Person(); var_dump($liz->age); Fatal error: Uncaught Error: Typed property Person::$age must not be accessed before initialization Rob Allen ~ @akrabat

Slide 28

Slide 28

Coercion of Typed Properties class Person { /* … */} $liz = new Person(); $liz->age = “93”; // int(93) Rob Allen ~ @akrabat

Slide 29

Slide 29

Strictly Typed Properties declare(strict_types=1); $liz = new Person(); $liz->age = “93”; Fatal error: Uncaught TypeError: Typed property Person::$age must be int, string used Rob Allen ~ @akrabat

Slide 30

Slide 30

But that’s all at runtime… Rob Allen ~ @akrabat

Slide 31

Slide 31

Static Analysis checks our code before we run it Rob Allen ~ @akrabat

Slide 32

Slide 32

Static Analysis Static code analysers simply reads your code and points out errors They ensure: • • • • • no syntax errors classes, methods, functions constants, variables exist no arguments or variables unused DocBlocks make sense data passed to methods are the correct type Rob Allen ~ @akrabat

Slide 33

Slide 33

Static Analysers • Phan • PHPStan • Psalm Rob Allen ~ @akrabat

Slide 34

Slide 34

Phan • • • • • Developed by Rasmus Lerdorf & Etsy Stable Parses entire code base and then analyses Requires php-ast extension Doesn’t parse /** @var Foo $foo */; use assert() instead Rob Allen ~ @akrabat

Slide 35

Slide 35

Phan 1. composer require —dev “phan/phan” 2. vendor/bin/phan —init —init-level=1 3. vendor/bin/phan —memory-limit 2G Rob Allen ~ @akrabat

Slide 36

Slide 36

Phan output Rob Allen ~ @akrabat

Slide 37

Slide 37

Phan output Rob Allen ~ @akrabat

Slide 38

Slide 38

PHPStan • • • • • Developed by Ondřej Mirtes Fast Can analyse subset of code base Autoloads classes Support for framework specific magic methods Rob Allen ~ @akrabat

Slide 39

Slide 39

PHPStan 1. composer require —dev “phpstan/phpstan” 2. vendor/bin/phpstan analyse app lib —level=max Rob Allen ~ @akrabat

Slide 40

Slide 40

PHPStan output Rob Allen ~ @akrabat

Slide 41

Slide 41

PHPStan output Rob Allen ~ @akrabat

Slide 42

Slide 42

Psalm • • • • • Developed by Vimeo Comprehensive Can analyse subset of code base Autoloads classes Support custom PHPDoc tags (e.g. @psalm-param) Rob Allen ~ @akrabat

Slide 43

Slide 43

Psalm 1. composer require —dev “vimeo/psalm” 2. vendor/bin/psalm —init 3. vendor/bin/psalm Rob Allen ~ @akrabat

Slide 44

Slide 44

Psalm output Rob Allen ~ @akrabat

Slide 45

Slide 45

Psalm output Rob Allen ~ @akrabat

Slide 46

Slide 46

Observations • All are good at finding type issues • All three also found found different issues • Phan found unused use statements • PHPStan found logic errors • Psalm is more type-picky, especially with mixed Rob Allen ~ @akrabat

Slide 47

Slide 47

What Problems Do Static Analysers Find? Rob Allen ~ @akrabat

Slide 48

Slide 48

Report 1 class Acl extends BaseAcl { protected $siteKey = ”; protected $settings; public function __construct(array $settings) { $this->settings = $settings; } Rob Allen ~ @akrabat

Slide 49

Slide 49

Report 1 class Acl extends BaseAcl { protected $siteKey = ”; protected $settings; public function __construct(array $settings) { $this->settings = $settings; } INFO: MissingPropertyType - Property Acl::$siteKey does not have a declared type - consider string Rob Allen ~ @akrabat

Slide 50

Slide 50

Report 1: Fix (PHP 7.4) class Acl extends BaseAcl { protected string $siteKey = ”; protected $settings; Add property’s type Rob Allen ~ @akrabat

Slide 51

Slide 51

Report 1: Fix (PHP 7.3) class Acl extends BaseAcl { /** @var string */ protected $siteKey = ”; protected $settings; Use a docblock to declare property’s type Rob Allen ~ @akrabat

Slide 52

Slide 52

Report 2 public function setSiteKey($siteKey) { $this->siteKey = $siteKey; return $this; } Rob Allen ~ @akrabat

Slide 53

Slide 53

Report 2 public function setSiteKey($siteKey) { $this->siteKey = $siteKey; return $this; } INFO: MissingParamType - Parameter $siteKey has no provided type INFO: MissingReturnType - Method setSiteKey does not have a return type, expecting Acl Rob Allen ~ @akrabat

Slide 54

Slide 54

Report 2: Fix public function setSiteKey(string $siteKey) { $this->siteKey = $siteKey; return $this; } Add parameter’s type Rob Allen ~ @akrabat

Slide 55

Slide 55

Report 2: Fix public function setSiteKey(string $siteKey): Acl { $this->siteKey = $siteKey; return $this; } Add parameter’s type & return type Rob Allen ~ @akrabat

Slide 56

Slide 56

An bona fide potential bug! public function populate(): void { $siteKey = $this->getSiteKey() ?: 1; $routes = $this->router->slugRoutes($siteKey); foreach ($routes as $route) { Rob Allen ~ @akrabat

Slide 57

Slide 57

An bona fide potential bug! public function populate(): void { $siteKey = $this->getSiteKey() ?: 1; $routes = $this->router->slugRoutes($siteKey); foreach ($routes as $route) { ERROR: InvalidScalarArgument Argument 1 of slugRoutes expects null|string, int(1)|non-empty-string provided Rob Allen ~ @akrabat

Slide 58

Slide 58

Collections class ChoiceService { public function fetchChoices(): array { … } … usage: foreach ($service->fetchChoices() as $choice) { … } Rob Allen ~ @akrabat

Slide 59

Slide 59

Collections /** * @return Choice[] */ public function fetchChoices(): array { … } Declare type of array returned using a docblock Rob Allen ~ @akrabat

Slide 60

Slide 60

Collections /** * @return Choice[] */ public function fetchChoices(): array { … } ERROR: InvalidReturnStatement - The type ‘array{0: Choice|null}’ does not match the declared return type ‘array<array-key, Choice>’ for ChoiceService::fetchChoices Rob Allen ~ @akrabat

Slide 61

Slide 61

False positive? // loadByKey() returns: ?Choice $choice = $choiceService->loadByKey($keyName); if (!$choice) { $this->redirectToHome(); // throws an Exception } $data = $choice->getArrayCopy(); return $this->format($data); Rob Allen ~ @akrabat

Slide 62

Slide 62

False positive? // loadByKey() returns: ?Choice $choice = $choiceService->loadByKey($keyName); if (!$choice) { $this->redirectToHome(); // throws an Exception } $data = $choice->getArrayCopy(); return $this->format($data); Cannot call method getArrayCopy on possibly null value Rob Allen ~ @akrabat

Slide 63

Slide 63

False positive?: Fix /** * @psalm-return never-return */ public function redirectToHome(): void { // … } Guarantees that the function exits or throws an exception Rob Allen ~ @akrabat

Slide 64

Slide 64

Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function Rob Allen ~ @akrabat

Slide 65

Slide 65

Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function @psalm-seal-properties: Prevent __set/__get not declared with @property Rob Allen ~ @akrabat

Slide 66

Slide 66

Psalm annotations @psalm-var, @psalm-param & @psalm-return: Psalm versions for types not understood by phpDocumenter never-return adds an implicit exit at end of function @psalm-seal-properties: Prevent __set/__get not declared with @property @psalm-suppress: Suppress specific Psalm issues Full list: https://psalm.dev/docs/annotating_code/supported_annotations Rob Allen ~ @akrabat

Slide 67

Slide 67

Applying Static Analysis to Your Project Rob Allen ~ @akrabat

Slide 68

Slide 68

Applying to Your Project • Add to CI Rob Allen ~ @akrabat

Slide 69

Slide 69

Applying to Your Project • Add to CI • Use levels and fix the “easy” items first • Set to lowest level and fix all issues • Increase level and repeat Rob Allen ~ @akrabat

Slide 70

Slide 70

1745 errors found 3229 other issues found Rob Allen ~ @akrabat

Slide 71

Slide 71

Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml Rob Allen ~ @akrabat

Slide 72

Slide 72

Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings Rob Allen ~ @akrabat

Slide 73

Slide 73

Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings 3. Fix warnings as you can (and allow no new errors!) Rob Allen ~ @akrabat

Slide 74

Slide 74

Baselines 1. Log all current errors to a baseline file: vendor/bin/psalm —set-baseline=baseline.xml 2. Subsequent psalm runs will treat all errors in the baseline file as warnings 3. Fix warnings as you can (and allow no new errors!) 4. Update baseline as you go: vendor/bin/psalm —update-baseline Rob Allen ~ @akrabat

Slide 75

Slide 75

To Sum Up Rob Allen ~ @akrabat

Slide 76

Slide 76

Takeaways • • • • • • PHP 7’s type declarations reduce cognitive load Upgrade to PHP 7.4 for typed properties! Static analysis finds bugs in your code Use plugins to give additional info to your static analyser Autofixers are great! e.g. psalter, rector Add static analysis to your CI Rob Allen ~ @akrabat

Slide 77

Slide 77

Thank you! Rob Allen ~ @akrabat