• 技术文章 >后端开发 >php教程

    【Phan】代码静态扫描

    藏色散人藏色散人2019-05-15 09:33:47转载1583
    很多时候,最大的优势在某些情况下就会变成最大的劣势。PHP 语法非常灵活,也不用编译。但是在项目比较复杂的时候,可能会导致一些意想不到的 bug。

    背景分析

    不知道你的项目是否有遇到过类似的线上故障呢?比如

    继承类语法错误导致的故障

    文件1

    class Animal
    {
        public $hasLeg = false;
    }

    文件2

    include "Animal.php";
    class Dog extends Animal
    {
        protected $hasLeg = false;
    }
    $dog = new Dog();
    php Dog.php
    Fatal error: Access level to Dog::$hasLeg must be public (as in class Animal) in /Users/mengkang/vagrant-develop/project/untitled1/Dog.php on line 5

    09cbff756d0da9edde2a9a9e294f121.png

    php入门到就业线上直播课:进入学习

    (注意 IDE 并没有提示有预发错误的哟,我专门截图)

    今天在看代码的时候看到一个变量一直重复查询,就是用户是否是管理员的身份。我想既然这样,不然在第一次用的地方就放入到成员变量里,免得后面都重复查询。

    结果发现我在父类定义的变量名$isAdmin,之前的代码已经在某一个子类里面单独定义过了。父类里是public属性,而子类里是private导致了这个故障。

    如果是 java 这种错误,无法编译通过。但是 php 不需要编译,只要测试没有覆盖到刚刚修改的文件就不会发现这个问题,既是优势也是弱势。

    参数不符合预期

    159fc171249eb29153d0b2426c2d91e.png

    有时候a.php,b.php,c.php三个文件都引用d.php的的一个函数,但是修改了d.php里面的一个函数的参数个数,如果前面使用的3个文件里面的没有改全,只改了a.php,而测试的时候又没有覆盖到b.php和c.php,那么上线了,就会触发bug和错误了。

    错把数组当对象

    你可能认为这种错误太低级了,不可能发生在自己身上,但是根据我的经验的确会发生,高强度的需求之下,很容易复制粘贴一些东西,只复制一半。而且恰巧因为某些逻辑判断,自己在日常环境开发的时候,出现问题的地方没有被执行到。

    比如下面这段代码:

    $article = $this->getParam('article');
    // 假设下面这段代码是复制的
    $isPowerEditer = "xxxxx 演示代码";
    if(!$isPowerEditer){
        if ($article->getUserId() != $uid)
        {
            ...
        }
    }

    因为复制的来源处,$article是一个对象,所以调用了getUserId的方法。但是上面的$article是一个从客户端获取的参数,不是对象。

    Call to a member function getUserId() on a non-object

    而自己测试的时候,因为if(!$isPowerEditer)的判断导致没有执行到里面去。直到上线之后才发现问题。

    错把对象当数组

    ac3ac228a321cf3233e87e9bcaa3947.png

    Cannot use object of type DataObject\Article as array

    不禁反思,如果这个项目是 java 的,肯定不会出现上面两个问题了,因为在项目构建的时候就已经没法通过了。

    不存在的数组

    4b26208a24c34bb8b7d445a1a664134.png

    这也不飘红?多写了个s呢,可能因为外面包了一个empty所以IDE没有标记为错误吧。所以我们不能太相信IDE。

    思考与改进

    自造轮子实验

    进一步思考,我们是否能够做一个工具来自己模拟编译呢?写了一个小 demo ,依赖nikic/php-parser

    https://github.com/nikic/PHP-Parser

    PHP-Parser 可以把PHP代码解析为AST,方便我们做语法分析。比如上面的例子

    文件1

    class Animal
    {
        public $hasLeg = false;
    }

    文件2(Dog.php)

    include "Animal.php";
    class Dog extends Animal
    {
        protected $hasLeg = false;
    }
    $dog = new Dog();

    我们利用 PHP-Parser 做了语法解析检测,代码如下:

    include dirname(__DIR__)."/vendor/autoload.php";
    use PhpParser\Error;
    use PhpParser\Node\Stmt\Property;
    use PhpParser\ParserFactory;
    use PhpParser\Node\Stmt\Class_;
    $code = file_get_contents("Dog.php");
    $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP5);
    try {
        $ast = $parser->parse($code);
    } catch (Error $error) {
        echo "Parse error: {$error->getMessage()}\n";
        return;
    }
    $classCheck = new ClassCheck($ast);
    $classCheck->extendsCheck();
    class ClassCheck{
        /**
         * @var Class_[]|null
         */
        private $classTable;
        public function __construct($nodes)
        {
            foreach ($nodes as $node){
                if ($node instanceof Class_){
                    $name = $node->name;
                    if (!isset($this->classTable[$name])) {
                        $this->classTable[$name] = $node;
                    }else{
                        // 报错哪里类重复了
                        echo $node->getLine();
                    }
                }
            }
        }
        public function extendsCheck(){
            foreach ($this->classTable as $node){
                if (!$node->extends){
                    continue;
                }
                $parentClassName = $node->extends->getFirst();
                if (!isset($this->classTable[$parentClassName])) {
                    exit($parentClassName."不存在");
                }
                $parentNode = $this->classTable[$parentClassName];
                foreach ($node->stmts as $stmt){
                    if ($stmt instanceof Property){
                        // 查看该属性是否存在于父类中
                        $this->propertyCheck($stmt,$parentNode);
                    }
                }
            }
        }
        /**
         * @param Property $property
         * @param Class_ $parentNode
         */
        private function propertyCheck($property,$parentNode){
            foreach ($parentNode->stmts as $stmt){
                if ($stmt instanceof Property){
                    if ($stmt->props[0]->name != $property->props[0]->name){
                        continue;
                    }
                    if ($stmt->isProtected() && $property->isPrivate()) {
                        echo $stmt->getLine()."\n";
                        echo $property->getLine()."\n";
                    }
                }
            }
        }
    }

    原理能就是对解析出来的AST继续做分析,但是前人栽树后人乘凉,这样的完整工具已经有大神帮我们做好了。

    使用现有工具

    https://github.com/phan/phan

    可以说它与上面介绍的nikic/php-parser师出同门,依赖nikic/php-astPHP扩展

    先安装php-ast扩展

    大概描述安装步骤

    git clone https://github.com/nikic/php-ast
    cd php-ast/
    phpize
    sudo ./configure --enable-ast
    sudo make
    sudo make install
    cd /etc/php.d
    # 引入扩展
    sudo vim ast.ini
    # 就能看到扩展啦
    php -m | grep ast

    安装 composer

    大概描述安装步骤

    curl -sS https://getcomposer.org/installer | php

    安装plan

    mkdir test
    cd test
    ~/composer.phar require --dev "phan/phan:1.x"

    实验

    实验1

    新建个项目,随便写个有问题的代码

    路径是src/a.php

    <?php
    class A extends B
    {
        public function a1()
        {
            return $this->a2(1);
        }
        /**
         * @param array $b
         *
         * @return int
         */
        private function a2($b)
        {
            return $b + 1;
        }
    }

    写个shell脚本

    #!/bin/bash
    function log()
    {
        echo -e -n "\033[01;35m[YUNQI] \033[01;31m"
        echo $@
        echo -e -n "\033[00m"
    }
    Color_Text()
    {
      echo -e " \e[0;$2m$1\e[0m"
    }
    Echo_Red()
    {
      echo $(Color_Text "$1" "31")
    }
    Echo_Green()
    {
      echo $(Color_Text "$1" "32")
    }
    Echo_Yellow()
    {
      echo $(Color_Text "$1" "33")
    }
    : > file.list
    for file in $(ls src/*)
    do
      echo $file >> file.list
    done
    Echo_Green "file list:\n"
    Echo_Green "========================\n"
    cat file.list
    Echo_Green "========================\n"
    Echo_Yellow "Phan run\n"
    Echo_Yellow "========================\n"
    ./vendor/bin/phan -f file.list -o res.out
    Echo_Yellow "========================\n"
    Echo_Red "error log\n"
    Echo_Red "========================\n"
    cat res.out
    Echo_Red "========================\n"

    执行结果

    案例中的错误

    1.类不存在

    2.参数类型错误

    3.语法运算类型推断

    1794fb71fadacc2d91df8da0b124c56.png

    实验2

    新增一个src/b.php

    <?php
    class B{
    }

    执行结果

    能过自动查找到class B了,不用我们做自动加载规则的指定

    d2f7a8f2dca8da87f24775cec9e45b2.png

    实验3

    刚刚两个都是测试的单独的脚本,没有测试项目,其实Plan已经支持了。假如我有一个项目如下

    656645af70b2035eb8bf1707986baef.png

    我在composer.json里面指定自动加载规则

    {
      "require-dev": {
        "phan/phan": "1.x"
      },
      "autoload": {
        "psr-4": {
          "Mk\\": "src"
        }
      }
    }

    然后在项目根目录执行

    ./vendor/bin/phan --init --init-level=3

    然后就会生成默认的配置文件在.phan目录里,最后就可以执行静态检测命令了

    ./vendor/bin/phan --progress-bar

    d033fb376300e7d98bdfc33700a3382.png

    如图所示呢,说明根据项目的自动加载规则A,B,C三个类呢都被扫描到了。

    以上就是【Phan】代码静态扫描的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:aliyun,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    自己动手写 PHP MVC 框架:点击学习

    快速了解MVC架构、了解框架底层运行原理

    专题推荐:Phan
    上一篇:PHP文件怎么解压和压缩?(代码示例) 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• C#使用Selenium+PhantomJS抓取数据详解• phantomjs导出html到pdf的方法实例分享• php调用phantomjs给微信小程序分享• 关于php如何调用phantomjs给微信小程序分享的问题
    1/1

    PHP中文网