A la recherche d'incompatibilités avec PHP 7

analyse statique, AST, PHP Parser, php7cc

Rdv AFUP - 23 fev 2016 - Loïck Piera

Loïck Piera

Développeur web chez

@pyrech

@pyrech

Introduction

Meilleur moyen de s'assurer la compatibilité à 100 % ?

  • Une suite de test complète
  • 100 % de code coverage

Standard, non ?

Analyse statique complémentaire

  • Donne un 1er aperçu
  • Toutes les incompatibilités en one-shot

Analyses statiques à la rescousse

Principe

  • Pas d'exécution
  • Analyse "limitée" surtout pour un langage fortement dynamique

Limites


function foo(Foo $foo) {
    $foo->bar();
}
                    

VS


function bar($bar) {
    $method = 'yolo_' . $this->getMethodName();
    $$bar->$method();
}
                        

Méthodes d'analyse

  • Regex
  • Manipulations de token (via token_get_all())
  • Arbre Syntaxique Abstrait (Abstract Syntax Tree ou AST)

AST

Les AST

Beaucoup en ont peur

On va voir que c'est simple à manipuler...

A quoi ça ressemble ?

$b = $a + 5;
=>

Assignment (
    var: Variable (
        name: b
    )
    expression: Binary Operation Plus (
        left: Variable (
            name: a
        )
        right: Scalar Left Value (
            value: 5
        )
    )
)
                            
=> Représentation d'un AST

Avantages de l'AST

  • Abstrait certaines syntaxes :
    • $foo
    • $$bar
    • ${'foobar'}
    • ${!${''}=barfoo()}
  • Autre représentation de votre code
  • Parcours simplifié avec le pattern Visitor

Parser du PHP en PHP

  • Coucou PHP Parser
  • Lib de référence
  • Créée par Nikita Popov

Utilisation du parser


$parser = new \PhpParser\Parser(new \PhpParser\Lexer());

foreach ($files as $file) {
    try {
        $content = file_get_contents($file);

        if (!empty($content)) {
            echo sprintf('Parsing du fichier "%s"', $file);

            // Parsing et création des AST
            $stmts = $parser->parse($content);

            // Traversée des AST
            // ...
        }
    } catch (\PhpParser\Error $e) {
        echo 'Parse Error: ', $e->getMessage();
    }
}
					

Parcours d'AST


$parser = new \PhpParser\Parser(new \PhpParser\Lexer());
$traverser = new \PhpParser\NodeTraverser();

$traverser->addVisitor(...);

// …

// Parsing et création de l'AST
$stmts = $parser->parse($content);

// Traversée des AST
$traverser->traverse($stmts);
					

Les visiteurs, c'est OKKKAY

Interface \PhpParser\NodeVisitor :


public function beforeTraverse(array $nodes);
public function enterNode(\PhpParser\Node $node);
public function leaveNode(\PhpParser\Node $node);
public function afterTraverse(array $nodes);
					

php7cc

Le(s) projet(s)

jolicode/php7-checker

sstalle/php7cc

php7cc

  • Outil CLI
  • Installable par phar, Composer (global / local), Docker
  • php7cc [--except=vendor] [--extensions=php,inc,lib] src/

Liste des erreurs

Redéfinition de classes globales (Error, TypeError, ReflectionType, etc) Multiple default cases Constructeur PHP 4 Bitwise shift par un nombre négatif Option 'salt' dans password_hash() Nom de paramètre de fonction dupliqué Classe nommée bool, int, float, etc Utilisation de $HTTP_RAW_POST_DATA String contenant une notation hexadecimal invalide Redéfinition de fonctions globales (intdiv, random_int, etc)
Appel de fonctions supprimées (mysql_*, mssql_*, mcrypt_*, etc) New assignement par référence Eval modifier 'e' dans preg_replace

Et bien d'autres...

Exemples de détection

New assignement par référence


class Foo {}
$foo =& new Foo();
                    

PHP 5 :

Deprecated: Assigning the return value of new by reference is deprecated in /path/to/script.php on line 3

PHP 7 :

Parse error: syntax error, unexpected 'new' (T_NEW) in /path/to/script.php on line 3

use PhpParser\Node\Expr;

class NewAssignmentByReferenceVisitor extends AbstractVisitor {
    public function enterNode(\PhpParser\Node $node) {
        if ($node instanceof Expr\AssignRef && $node->expr instanceof Expr\New_) {
            $this->addContextMessage('Result of new is assigned by reference', $node);
        }
    }
}
					

Redéfinition de paramètre


function foo($bar, $bar) {}
                    

PHP 5 :

OK

PHP 7 :

Fatal error: Redefinition of parameter $bar in /path/to/script.php on line 2

use PhpParser\Node;

class DuplicateFunctionParameterVisitor extends AbstractVisitor {
    public function enterNode(Node $node) {
        if (!$node instanceof Node\FunctionLike) return;

        $parametersNames = array();

        foreach ($node->getParams() as $parameter) {
            $currentParameterName = $parameter->name;

            if (!isset($parametersNames[$currentParameterName])) {
                $parametersNames[$currentParameterName] = false;
            }
            elseif (!$parametersNames[$currentParameterName]) {
                $this->addContextMessage(
                    sprintf('Duplicate function parameter name "%s"', $currentParameterName),
                    $node
                );
                $parametersNames[$currentParameterName] = true;
            }
        }
    }
}
					

Constructeur PHP 4


class Foo {
    public function Foo() {}
}
                    

PHP 5 :

OK

PHP 7 :

Conservés mais dépréciés et limité aux cas existants avant PHP 5

  • Aucun constructeur PHP 5+ présent
  • Pas de namespace (ajouté en PHP 5.3)
Deprecated: Methods with the same name as their class will not be constructors in a
future version of PHP; Foo has a deprecated constructor in /path/to/script.php on line 3

class PHP4ConstructorVisitor extends AbstractVisitor {
    public function enterNode(Node $node) {
        if (!$node instanceof Node\Stmt\Class_) return;

        $currentClassName = $node->name;
        $hasPhp4Constructor = $hasPhp5Constructor = false;
        $php4ConstructorNode = null;

        // Anonymous class can't use php4 constructor by definition
        if (empty($currentClassName)) return;
        // Checks if class is namespaced
        if (count($node->namespacedName->parts) > 1) return;

        foreach ($node->stmts as $stmt) {
            if (!$stmt instanceof Node\Stmt\ClassMethod) continue;

            if ($stmt->name === '__construct') $hasPhp5Constructor = true;

            if ($stmt->name === $currentClassName) {
                $hasPhp4Constructor = true;
                $php4ConstructorNode = $stmt;
            }
        }
        if ($hasPhp4Constructor && !$hasPhp5Constructor) {
            $this->addContextMessage('PHP 4 constructors deprecated', $php4ConstructorNode);
        }
    }
}
					

Conclusion

Conclusion

  • L'analyse statique et les AST, c'est sympa
  • Ne pas hésiter à dumper les noeuds
  • Utiliser php7cc en + de vos tests pour une migration en douceur

On recrute

Viens faire de la trottinette avec nous

Sources

Merci de votre attention

Des questions ?

Let's drink