<?php
/**
 * Flight: An extensible micro-framework.
 *
 * @copyright   Copyright (c) 2011, Mike Cao <mike@mikecao.com>
 * @license     MIT, http://flightphp.com/license
 */

namespace flight;

use flight\core\Loader;
use flight\core\Dispatcher;

/**
 * The Engine class contains the core functionality of the framework.
 * It is responsible for loading an HTTP request, running the assigned services,
 * and generating an HTTP response.
 *
 * Core methods
 * @method void start() Starts engine
 * @method void stop() Stops framework and outputs current response
 * @method void halt(int $code = 200, string $message = '') Stops processing and returns a given response.
 *
 *
 * Routing
 * @method void route(string $pattern, callable $callback, bool $pass_route = false) Routes a URL to a callback function.
 * @method \flight\net\Router router() Gets router
 *
 * Views
 * @method void render(string $file, array $data = null, string $key = null) Renders template
 * @method \flight\template\View view() Gets current view
 *
 * Request-response
 * @method \flight\net\Request request() Gets current request
 * @method \flight\net\Response response() Gets current response
 * @method void error(\Exception $e) Sends an HTTP 500 response for any errors.
 * @method void notFound() Sends an HTTP 404 response when a URL is not found.
 * @method void redirect(string $url, int $code = 303)  Redirects the current request to another URL.
 * @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) Sends a JSON response.
 * @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) Sends a JSONP response.
 *
 * HTTP caching
 * @method void etag($id, string $type = 'strong') Handles ETag HTTP caching.
 * @method void lastModified(int $time) Handles last modified HTTP caching.
 */
class Engine {
    /**
     * Stored variables.
     *
     * @var array
     */
    protected $vars;

    /**
     * Class loader.
     *
     * @var Loader
     */
    protected $loader;

    /**
     * Event dispatcher.
     *
     * @var Dispatcher
     */
    protected $dispatcher;

    /**
     * Constructor.
     */
    public function __construct() {
        $this->vars = array();

        $this->loader = new Loader();
        $this->dispatcher = new Dispatcher();

        $this->init();
    }

    /**
     * Handles calls to class methods.
     *
     * @param string $name Method name
     * @param array $params Method parameters
     * @return mixed Callback results
     * @throws \Exception
     */
    public function __call($name, $params) {
        $callback = $this->dispatcher->get($name);

        if (is_callable($callback)) {
            return $this->dispatcher->run($name, $params);
        }

        if (!$this->loader->get($name)) {
            throw new \Exception("{$name} must be a mapped method.");
        }

        $shared = (!empty($params)) ? (bool)$params[0] : true;

        return $this->loader->load($name, $shared);
    }

    /*** Core Methods ***/

    /**
     * Initializes the framework.
     */
    public function init() {
        static $initialized = false;
        $self = $this;

        if ($initialized) {
            $this->vars = array();
            $this->loader->reset();
            $this->dispatcher->reset();
        }

        // Register default components
        $this->loader->register('request', '\flight\net\Request');
        $this->loader->register('response', '\flight\net\Response');
        $this->loader->register('router', '\flight\net\Router');
        $this->loader->register('view', '\flight\template\View', array(), function($view) use ($self) {
            $view->path = $self->get('flight.views.path');
            $view->extension = $self->get('flight.views.extension');
        });

        // Register framework methods
        $methods = array(
            'start','stop','route','halt','error','notFound',
            'render','redirect','etag','lastModified','json','jsonp'
        );
        foreach ($methods as $name) {
            $this->dispatcher->set($name, array($this, '_'.$name));
        }

        // Default configuration settings
        $this->set('flight.base_url', null);
        $this->set('flight.case_sensitive', false);
        $this->set('flight.handle_errors', true);
        $this->set('flight.log_errors', false);
        $this->set('flight.views.path', './views');
        $this->set('flight.views.extension', '.php');

        // Startup configuration
        $this->before('start', function() use ($self) {
            // Enable error handling
            if ($self->get('flight.handle_errors')) {
                set_error_handler(array($self, 'handleError'));
                set_exception_handler(array($self, 'handleException'));
            }

            // Set case-sensitivity
            $self->router()->case_sensitive = $self->get('flight.case_sensitive');
        });

        $initialized = true;
    }

    /**
     * Custom error handler. Converts errors into exceptions.
     *
     * @param int $errno Error number
     * @param int $errstr Error string
     * @param int $errfile Error file name
     * @param int $errline Error file line number
     * @throws \ErrorException
     */
    public function handleError($errno, $errstr, $errfile, $errline) {
        if ($errno & error_reporting()) {
            throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
        }
    }

    /**
     * Custom exception handler. Logs exceptions.
     *
     * @param \Exception $e Thrown exception
     */
    public function handleException($e) {
        if ($this->get('flight.log_errors')) {
            error_log($e->getMessage());
        }

        $this->error($e);
    }

    /**
     * Maps a callback to a framework method.
     *
     * @param string $name Method name
     * @param callback $callback Callback function
     * @throws \Exception If trying to map over a framework method
     */
    public function map($name, $callback) {
        if (method_exists($this, $name)) {
            throw new \Exception('Cannot override an existing framework method.');
        }

        $this->dispatcher->set($name, $callback);
    }

    /**
     * Registers a class to a framework method.
     *
     * @param string $name Method name
     * @param string $class Class name
     * @param array $params Class initialization parameters
     * @param callback $callback Function to call after object instantiation
     * @throws \Exception If trying to map over a framework method
     */
    public function register($name, $class, array $params = array(), $callback = null) {
        if (method_exists($this, $name)) {
            throw new \Exception('Cannot override an existing framework method.');
        }

        $this->loader->register($name, $class, $params, $callback);
    }

    /**
     * Adds a pre-filter to a method.
     *
     * @param string $name Method name
     * @param callback $callback Callback function
     */
    public function before($name, $callback) {
        $this->dispatcher->hook($name, 'before', $callback);
    }

    /**
     * Adds a post-filter to a method.
     *
     * @param string $name Method name
     * @param callback $callback Callback function
     */
    public function after($name, $callback) {
        $this->dispatcher->hook($name, 'after', $callback);
    }

    /**
     * Gets a variable.
     *
     * @param string $key Key
     * @return mixed
     */
    public function get($key = null) {
        if ($key === null) return $this->vars;

        return isset($this->vars[$key]) ? $this->vars[$key] : null;
    }

    /**
     * Sets a variable.
     *
     * @param mixed $key Key
     * @param string $value Value
     */
    public function set($key, $value = null) {
        if (is_array($key) || is_object($key)) {
            foreach ($key as $k => $v) {
                $this->vars[$k] = $v;
            }
        }
        else {
            $this->vars[$key] = $value;
        }
    }

    /**
     * Checks if a variable has been set.
     *
     * @param string $key Key
     * @return bool Variable status
     */
    public function has($key) {
        return isset($this->vars[$key]);
    }

    /**
     * Unsets a variable. If no key is passed in, clear all variables.
     *
     * @param string $key Key
     */
    public function clear($key = null) {
        if (is_null($key)) {
            $this->vars = array();
        }
        else {
            unset($this->vars[$key]);
        }
    }

    /**
     * Adds a path for class autoloading.
     *
     * @param string $dir Directory path
     */
    public function path($dir) {
        $this->loader->addDirectory($dir);
    }

    /*** Extensible Methods ***/

    /**
     * Starts the framework.
     * @throws \Exception
     */
    public function _start() {
        $dispatched = false;
        $self = $this;
        $request = $this->request();
        $response = $this->response();
        $router = $this->router();

        // Allow filters to run
        $this->after('start', function() use ($self) {
            $self->stop();
        });

        // Flush any existing output
        if (ob_get_length() > 0) {
            $response->write(ob_get_clean());
        }

        // Enable output buffering
        ob_start();

        // Route the request
        while ($route = $router->route($request)) {
            $params = array_values($route->params);

            // Add route info to the parameter list
            if ($route->pass) {
                $params[] = $route;
            }

            // Call route handler
            $continue = $this->dispatcher->execute(
                $route->callback,
                $params
            );

            $dispatched = true;

            if (!$continue) break;

            $router->next();

            $dispatched = false;
        }

        if (!$dispatched) {
            $this->notFound();
        }
    }

    /**
     * Stops the framework and outputs the current response.
     *
     * @param int $code HTTP status code
     * @throws \Exception
     */
    public function _stop($code = null) {
        $response = $this->response();

        if (!$response->sent()) {
            if ($code !== null) {
                $response->status($code);
            }

            $response->write(ob_get_clean());

            $response->send();
        }
    }

    /**
     * Routes a URL to a callback function.
     *
     * @param string $pattern URL pattern to match
     * @param callback $callback Callback function
     * @param boolean $pass_route Pass the matching route object to the callback
     */
    public function _route($pattern, $callback, $pass_route = false) {
        $this->router()->map($pattern, $callback, $pass_route);
    }

    /**
     * Stops processing and returns a given response.
     *
     * @param int $code HTTP status code
     * @param string $message Response message
     */
    public function _halt($code = 200, $message = '') {
        $this->response()
            ->clear()
            ->status($code)
            ->write($message)
            ->send();
        exit();
    }

    /**
     * Sends an HTTP 500 response for any errors.
     *
     * @param \Exception|\Throwable $e Thrown exception
     */
    public function _error($e) {
        $msg = sprintf('
500 Internal Server Error
'.
            '
%s (%s)
'.
            '
%s
',
            $e->getMessage(),
            $e->getCode(),
            $e->getTraceAsString()
        );

        try {
            $this->response()
                ->clear()
                ->status(500)
                ->write($msg)
                ->send();
        }
        catch (\Throwable $t) { // PHP 7.0+
            exit($msg);
        } catch(\Exception $e) { // PHP < 7
            exit($msg);
        }
    }

    /**
     * Sends an HTTP 404 response when a URL is not found.
     */
    public function _notFound() {
        $this->response()
            ->clear()
            ->status(404)
            ->write(
                '
404 Not Found
'.
                '
The page you have requested could not be found.
'.
                str_repeat(' ', 512)
            )
            ->send();
    }

    /**
     * Redirects the current request to another URL.
     *
     * @param string $url URL
     * @param int $code HTTP status code
     */
    public function _redirect($url, $code = 303) {
        $base = $this->get('flight.base_url');

        if ($base === null) {
            $base = $this->request()->base;
        }

        // Append base url to redirect url
        if ($base != '/' && strpos($url, '://') === false) {
            $url = $base . preg_replace('#/+#', '/', '/' . $url);
        }

        $this->response()
            ->clear()
            ->status($code)
            ->header('Location', $url)
            ->send();
    }

    /**
     * Renders a template.
     *
     * @param string $file Template file
     * @param array $data Template data
     * @param string $key View variable name
     * @throws \Exception
     */
    public function _render($file, $data = null, $key = null) {
        if ($key !== null) {
            $this->view()->set($key, $this->view()->fetch($file, $data));
        }
        else {
            $this->view()->render($file, $data);
        }
    }

    /**
     * Sends a JSON response.
     *
     * @param mixed $data JSON data
     * @param int $code HTTP status code
     * @param bool $encode Whether to perform JSON encoding
     * @param string $charset Charset
     * @param int $option Bitmask Json constant such as JSON_HEX_QUOT
     * @throws \Exception
     */
    public function _json(
        $data,
        $code = 200,
        $encode = true,
        $charset = 'utf-8',
        $option = 0
    ) {
        $json = ($encode) ? json_encode($data, $option) : $data;

        $this->response()
            ->status($code)
            ->header('Content-Type', 'application/json; charset='.$charset)
            ->write($json)
            ->send();
    }

    /**
     * Sends a JSONP response.
     *
     * @param mixed $data JSON data
     * @param string $param Query parameter that specifies the callback name.
     * @param int $code HTTP status code
     * @param bool $encode Whether to perform JSON encoding
     * @param string $charset Charset
     * @param int $option Bitmask Json constant such as JSON_HEX_QUOT
     * @throws \Exception
     */
    public function _jsonp(
        $data,
        $param = 'jsonp',
        $code = 200,
        $encode = true,
        $charset = 'utf-8',
        $option = 0
    ) {
        $json = ($encode) ? json_encode($data, $option) : $data;

        $callback = $this->request()->query[$param];

        $this->response()
            ->status($code)
            ->header('Content-Type', 'application/javascript; charset='.$charset)
            ->write($callback.'('.$json.');')
            ->send();
    }

    /**
     * Handles ETag HTTP caching.
     *
     * @param string $id ETag identifier
     * @param string $type ETag type
     */
    public function _etag($id, $type = 'strong') {
        $id = (($type === 'weak') ? 'W/' : '').$id;

        $this->response()->header('ETag', $id);

        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
            $_SERVER['HTTP_IF_NONE_MATCH'] === $id) {
            $this->halt(304);
        }
    }

    /**
     * Handles last modified HTTP caching.
     *
     * @param int $time Unix timestamp
     */
    public function _lastModified($time) {
        $this->response()->header('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $time));

        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
            strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time) {
            $this->halt(304);
        }
    }
}


<?php
/**
 * Flight: An extensible micro-framework.
 *
 * @copyright   Copyright (c) 2011, Mike Cao <mike@mikecao.com>
 * @license     MIT, http://flightphp.com/license
 */

namespace flight\core;

/**
 * The Loader class is responsible for loading objects. It maintains
 * a list of reusable class instances and can generate a new class
 * instances with custom initialization parameters. It also performs
 * class autoloading.
 */
class Loader {
    /**
     * Registered classes.
     *
     * @var array
     */
    protected $classes = array();

    /**
     * Class instances.
     *
     * @var array
     */
    protected $instances = array();

    /**
     * Autoload directories.
     *
     * @var array
     */
    protected static $dirs = array();

    /**
     * Registers a class.
     *
     * @param string $name Registry name
     * @param string|callable $class Class name or function to instantiate class
     * @param array $params Class initialization parameters
     * @param callback $callback Function to call after object instantiation
     */
    public function register($name, $class, array $params = array(), $callback = null) {
        unset($this->instances[$name]);

        $this->classes[$name] = array($class, $params, $callback);
    }

    /**
     * Unregisters a class.
     *
     * @param string $name Registry name
     */
    public function unregister($name) {
        unset($this->classes[$name]);
    }

    /**
     * Loads a registered class.
     *
     * @param string $name Method name
     * @param bool $shared Shared instance
     * @return object Class instance
     * @throws \Exception
     */
    public function load($name, $shared = true) {
        $obj = null;

        if (isset($this->classes[$name])) {
            list($class, $params, $callback) = $this->classes[$name];

            $exists = isset($this->instances[$name]);

            if ($shared) {
                $obj = ($exists) ?
                    $this->getInstance($name) :
                    $this->newInstance($class, $params);

                if (!$exists) {
                    $this->instances[$name] = $obj;
                }
            }
            else {
                $obj = $this->newInstance($class, $params);
            }

            if ($callback && (!$shared || !$exists)) {
                $ref = array(&$obj);
                call_user_func_array($callback, $ref);
            }
        }

        return $obj;
    }

    /**
     * Gets a single instance of a class.
     *
     * @param string $name Instance name
     * @return object Class instance
     */
    public function getInstance($name) {
        return isset($this->instances[$name]) ? $this->instances[$name] : null;
    }

    /**
     * Gets a new instance of a class.
     *
     * @param string|callable $class Class name or callback function to instantiate class
     * @param array $params Class initialization parameters
     * @return object Class instance
     * @throws \Exception
     */
    public function newInstance($class, array $params = array()) {
        if (is_callable($class)) {
            return call_user_func_array($class, $params);
        }

        switch (count($params)) {
            case 0:
                return new $class();
            case 1:
                return new $class($params[0]);
            case 2:
                return new $class($params[0], $params[1]);
            case 3:
                return new $class($params[0], $params[1], $params[2]);
            case 4:
                return new $class($params[0], $params[1], $params[2], $params[3]);
            case 5:
                return new $class($params[0], $params[1], $params[2], $params[3], $params[4]);
            default:
                try {
                    $refClass = new \ReflectionClass($class);
                    return $refClass->newInstanceArgs($params);
                } catch (\ReflectionException $e) {
                    throw new \Exception("Cannot instantiate {$class}", 0, $e);
                }
        }
    }

    /**
     * @param string $name Registry name
     * @return mixed Class information or null if not registered
     */
    public function get($name) {
        return isset($this->classes[$name]) ? $this->classes[$name] : null;
    }

    /**
     * Resets the object to the initial state.
     */
    public function reset() {
        $this->classes = array();
        $this->instances = array();
    }

    /*** Autoloading Functions ***/

    /**
     * Starts/stops autoloader.
     *
     * @param bool $enabled Enable/disable autoloading
     * @param array $dirs Autoload directories
     */
    public static function autoload($enabled = true, $dirs = array()) {
        if ($enabled) {
            spl_autoload_register(array(__CLASS__, 'loadClass'));
        }
        else {
            spl_autoload_unregister(array(__CLASS__, 'loadClass'));
        }

        if (!empty($dirs)) {
            self::addDirectory($dirs);
        }
    }

    /**
     * Autoloads classes.
     *
     * @param string $class Class name
     */
    public static function loadClass($class) {
        $class_file = str_replace(array('\\', '_'), '/', $class).'.php';

        foreach (self::$dirs as $dir) {
            $file = $dir.'/'.$class_file;
            if (file_exists($file)) {
                require $file;
                return;
            }
        }
    }

    /**
     * Adds a directory for autoloading classes.
     *
     * @param mixed $dir Directory path
     */
    public static function addDirectory($dir) {
        if (is_array($dir) || is_object($dir)) {
            foreach ($dir as $value) {
                self::addDirectory($value);
            }
        }
        else if (is_string($dir)) {
            if (!in_array($dir, self::$dirs)) self::$dirs[] = $dir;
        }
    }
}


<?php
/**
 * Flight: An extensible micro-framework.
 *
 * @copyright   Copyright (c) 2011, Mike Cao <mike@mikecao.com>
 * @license     MIT, http://flightphp.com/license
 */

namespace flight\template;

/**
 * The View class represents output to be displayed. It provides
 * methods for managing view data and inserts the data into
 * view templates upon rendering.
 */
class View {
    /**
     * Location of view templates.
     *
     * @var string
     */
    public $path;

    /**
     * File extension.
     *
     * @var string
     */
    public $extension = '.php';

    /**
     * View variables.
     *
     * @var array
     */
    protected $vars = array();

    /**
     * Template file.
     *
     * @var string
     */
    private $template;

    /**
     * Constructor.
     *
     * @param string $path Path to templates directory
     */
    public function __construct($path = '.') {
        $this->path = $path;
    }

    /**
     * Gets a template variable.
     *
     * @param string $key Key
     * @return mixed Value
     */
    public function get($key) {
        return isset($this->vars[$key]) ? $this->vars[$key] : null;
    }

    /**
     * Sets a template variable.
     *
     * @param mixed $key Key
     * @param string $value Value
     */
    public function set($key, $value = null) {
        if (is_array($key) || is_object($key)) {
            foreach ($key as $k => $v) {
                $this->vars[$k] = $v;
            }
        }
        else {
            $this->vars[$key] = $value;
        }
    }

    /**
     * Checks if a template variable is set.
     *
     * @param string $key Key
     * @return boolean If key exists
     */
    public function has($key) {
        return isset($this->vars[$key]);
    }

    /**
     * Unsets a template variable. If no key is passed in, clear all variables.
     *
     * @param string $key Key
     */
    public function clear($key = null) {
        if (is_null($key)) {
            $this->vars = array();
        }
        else {
            unset($this->vars[$key]);
        }
    }

    /**
     * Renders a template.
     *
     * @param string $file Template file
     * @param array $data Template data
     * @throws \Exception If template not found
     */
    public function render($file, $data = null) {
        $this->template = $this->getTemplate($file);

        if (!file_exists($this->template)) {
            throw new \Exception("Template file not found: {$this->template}.");
        }

        if (is_array($data)) {
            $this->vars = array_merge($this->vars, $data);
        }

        extract($this->vars);

        include $this->template;
    }

    /**
     * Gets the output of a template.
     *
     * @param string $file Template file
     * @param array $data Template data
     * @return string Output of template
     */
    public function fetch($file, $data = null) {
        ob_start();

        $this->render($file, $data);
        $output = ob_get_clean();

        return $output;
    }

    /**
     * Checks if a template file exists.
     *
     * @param string $file Template file
     * @return bool Template file exists
     */
    public function exists($file) {
        return file_exists($this->getTemplate($file));
    }

    /**
     * Gets the full path to a template file.
     *
     * @param string $file Template file
     * @return string Template file location
     */
    public function getTemplate($file) {
        $ext = $this->extension;

        if (!empty($ext) && (substr($file, -1 * strlen($ext)) != $ext)) {
            $file .= $ext;
        }

        if ((substr($file, 0, 1) == '/')) {
            return $file;
        }

        return $this->path.'/'.$file;
    }

    /**
     * Displays escaped output.
     *
     * @param string $str String to escape
     * @return string Escaped string
     */
    public function e($str) {
        echo htmlentities($str);
    }
}


<?php
namespace Hal\Application;

use Hal\Application\Config\Config;
use Hal\Component\Ast\NodeTraverser;
use Hal\Component\Issue\Issuer;
use Hal\Component\Output\Output;
use Hal\Component\Output\ProgressBar;
use Hal\Metric\Class_\ClassEnumVisitor;
use Hal\Metric\Class_\Complexity\CyclomaticComplexityVisitor;
use Hal\Metric\Class_\Complexity\KanDefectVisitor;
use Hal\Metric\Class_\Component\MaintainabilityIndexVisitor;
use Hal\Metric\Class_\Coupling\ExternalsVisitor;
use Hal\Metric\Class_\Structural\LcomVisitor;
use Hal\Metric\Class_\Structural\SystemComplexityVisitor;
use Hal\Metric\Class_\Text\HalsteadVisitor;
use Hal\Metric\Class_\Text\LengthVisitor;
use Hal\Metric\Metrics;
use Hal\Metric\Package\PackageAbstraction;
use Hal\Metric\Package\PackageCollectingVisitor;
use Hal\Metric\Package\PackageDependencies;
use Hal\Metric\Package\PackageDistance;
use Hal\Metric\Package\PackageInstability;
use Hal\Metric\System\Changes\GitChanges;
use Hal\Metric\System\Coupling\Coupling;
use Hal\Metric\System\Coupling\DepthOfInheritanceTree;
use Hal\Metric\System\Coupling\PageRank;
use Hal\Metric\System\Packages\Composer\Composer;
use Hal\Metric\System\UnitTesting\UnitTesting;
use PhpParser\Error;
use PhpParser\ParserFactory;

/**
 * Class Analyze
 * @package Hal\Application
 */
class Analyze
{

    /**
     * @var Output
     */
    private $output;

    /**
     * @var Config
     */
    private $config;

    /**
     * @var Issuer
     */
    private $issuer;

    /**
     * Analyze constructor.
     * @param Output $output
     */
    public function __construct(Config $config, Output $output, Issuer $issuer)
    {
        $this->output = $output;
        $this->config = $config;
        $this->issuer = $issuer;
    }

    /**
     * Runs analyze
     */
    public function run($files)
    {
        // config
        ini_set('xdebug.max_nesting_level', 3000);

        $metrics = new Metrics();

        // traverse all
        $whenToStop = function() {
            return true;
        };

        // prepare parser
        $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
        $traverser = new NodeTraverser(false, $whenToStop);
        $traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver());
        $traverser->addVisitor(new ClassEnumVisitor($metrics));
        $traverser->addVisitor(new CyclomaticComplexityVisitor($metrics));
        $traverser->addVisitor(new ExternalsVisitor($metrics));
        $traverser->addVisitor(new LcomVisitor($metrics));
        $traverser->addVisitor(new HalsteadVisitor($metrics));
        $traverser->addVisitor(new LengthVisitor($metrics));
        $traverser->addVisitor(new CyclomaticComplexityVisitor($metrics));
        $traverser->addVisitor(new MaintainabilityIndexVisitor($metrics));
        $traverser->addVisitor(new KanDefectVisitor($metrics));
        $traverser->addVisitor(new SystemComplexityVisitor($metrics));
        $traverser->addVisitor(new PackageCollectingVisitor($metrics));

        // create a new progress bar (50 units)
        $progress = new ProgressBar($this->output, sizeof($files));
        $progress->start();

        foreach ($files as $file) {
            $progress->advance();
            $code = file_get_contents($file);
            $this->issuer->set('filename', $file);
            try {
                $stmts = $parser->parse($code);
                $this->issuer->set('statements', $stmts);
                $traverser->traverse($stmts);
            } catch (Error $e) {
                $this->output->writeln(sprintf('<error>Cannot parse %s</error>', $file));
            }
            $this->issuer->clear('filename');
            $this->issuer->clear('statements');
        }

        $progress->clear();

        $this->output->write('Executing system analyzes...');

        //
        // System analyses
        (new PageRank())->calculate($metrics);
        (new Coupling())->calculate($metrics);
        (new DepthOfInheritanceTree())->calculate($metrics);

        //
        // Package analyses
        (new PackageDependencies())->calculate($metrics);
        (new PackageAbstraction())->calculate($metrics);
        (new PackageInstability())->calculate($metrics);
        (new PackageDistance())->calculate($metrics);

        //
        // File analyses
        (new GitChanges($this->config, $files))->calculate($metrics);

        //
        // Unit test
        (new UnitTesting($this->config, $files))->calculate($metrics);

        //
        // Composer
        (new Composer($this->config, $files))->calculate($metrics);

        $this->output->clearln();

        return $metrics;
    }
}


<?php

/*
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Hal\Component\Tree\Operator;

use Hal\Component\Tree\Graph;
use Hal\Component\Tree\Node;

/**
 * Class CycleDetector
 * @package Hal\Component\Tree\Util
 * @see http://www.geeksforgeeks.org/detect-cycle-in-a-graph/
 */
class CycleDetector
{
    /**
     * Check if graph contains cycle
     *
     * Each node in cycle is flagged with the "cyclic" attribute
     *
     * @param Graph $graph
     * @return bool
     */
    public function isCyclic(Graph $graph)
    {
        // prepare stack
        $recursionStack = [];
        foreach ($graph->all() as $node) {
            $recursionStack[$node->getKey()] = false;
        }

        // start analysis
        $isCyclic = false;
        foreach ($graph->getEdges() as $edge) {
            if ($r = $this->detectCycle($edge->getFrom(), $recursionStack)) {
                $edge->cyclic = true;
                $isCyclic = true;
            }

            $recursionStack[$node->getKey()] = false;
        }

        $graph->resetVisits();

        return $isCyclic;
    }

    /**
     * @param Node $node
     * @param $recursionStack
     * @return bool
     */
    private function detectCycle(Node $node, &$recursionStack)
    {
        if (!$node->visited) {
            // mark the current node as visited and part of recursion stack
            $recursionStack[$node->getKey()] = true;
            $node->visited = true;

            // recur for all the vertices adjacent to this vertex
            foreach ($node->getEdges() as $edge) {
                if ($edge->getTo() === $node) {
                    continue;
                }

                if (!$edge->getTo()->visited && $this->detectCycle($edge->getTo(), $recursionStack)) {
                    $edge->cyclic = $edge->getTo()->cyclic = true;
                    return true;
                } elseif ($recursionStack[$edge->getTo()->getKey()]) {
                    $edge->cyclic = $edge->getTo()->cyclic = true;
                    return true;
                }
            }
        }
        // remove the vertex from recursion stack
        $recursionStack[$node->getKey()] = false;
        return false;
    }
}


<?php

/*
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Hal\Component\Tree\Operator;

use Hal\Component\Tree\Graph;
use Hal\Component\Tree\Node;

class SizeOfTree
{
    /**
     * @var Graph
     */
    private $graph;

    /**
     * SizeOfTree constructor.
     * @param Graph $graph
     */
    public function __construct(Graph $graph)
    {
        if ((new CycleDetector())->isCyclic($graph)) {
            throw new \LogicException('Cannot get size informations of cyclic graph');
        }

        $this->graph = $graph;
    }

    /**
     * Get depth of node
     *
     * @param Node $node
     * @return int
     */
    public function getDepthOfNode(Node $node)
    {

        $edges = $node->getEdges();

        if (0 === sizeof($edges)) {
            return 0;
        }

        // our tree is not binary : interface can have more than one parent
        $max = 0;
        foreach ($edges as $edge) {

            if ($edge->getFrom() == $node) {
                continue;
            }

            $n = 1 + $this->getDepthOfNode($edge->getFrom());
            if ($n > $max) {
                $max = $n;
            }
        }

        return $max;
    }

    /**
     * Get depth of node
     *
     * @param Node $node
     * @return int
     */
    public function getNumberOfChilds(Node $node, $uniqs = false)
    {

        $edges = $node->getEdges();

        if (0 === sizeof($edges)) {
            return 0;
        }

        // our tree is not binary : interface can have more than one parent
        $max = 0;
        $n = 0;

        foreach ($edges as $edge) {

            if ($edge->getTo() == $node) {
                continue;
            }

            if (true == $uniqs && $edge->getTo()->visited) {
                continue;
            }
            $edge->getTo()->visited = true;

            $n += 1 + $this->getNumberOfChilds($edge->getTo(), $uniqs);

            $edge->getTo()->visited = false;

            if ($n > $max) {
                $max = $n;
            }
        }

        return $max;
    }

    /**
     * Get average height of graph
     *
     * @return float
     */
    public function getAverageHeightOfGraph()
    {
        $ns = [];
        foreach ($this->graph->getRootNodes() as $node) {
            array_push($ns, $this->getLongestBranch($node));
        }
        return round(array_sum($ns) / max(1, sizeof($ns)), 2);
    }

    /**
     * @param Node $node
     * @return int
     */
    public function getLongestBranch(Node $node)
    {
        $max = 1;
        foreach ($node->getEdges() as $edge) {

            if ($node == $edge->getTo()) {
                // only descendants
                continue;
            }

            $n = 1 + $this->getLongestBranch($edge->getTo());

            if ($n > $max) {
                $max = $n;
            }
        }

        return $max;
    }
}


<?php

namespace Hal\Component\Tree;

class GraphDeduplicated extends Graph
{
    /**
     * @var array list of already present edges in this graph
     */
    private $edgesMap = [];

    /**
     * @param Node $from
     * @param Node $to
     *
     * @return $this
     */
    public function addEdge(Node $from, Node $to)
    {
        $key = $from->getUniqueId().'->'.$to->getUniqueId();

        if (isset($this->edgesMap[$key])) {
            return $this;
        }

        $this->edgesMap[$key] = true;

        return parent::addEdge($from, $to);
    }
}


<?php

namespace Hal\Application\Config\File;

use Hal\Application\Config\Config;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;

class ConfigFileReaderJson implements ConfigFileReaderInterface
{
    /**
     * @var string
     */
    private $filename;

    /**
     * ConfigFileReaderJson constructor.
     */
    public function __construct($filename)
    {
        $this->filename = $filename;
    }

    /**
     * @param Config $config
     *
     * @return void
     */
    public function read(Config $config)
    {
        $jsonText = file_get_contents($this->filename);

        if (false === $jsonText) {
            throw new \InvalidArgumentException("Cannot read configuration file '{$this->filename}'");
        }

        $jsonData = json_decode($jsonText, true);

        if (false === $jsonData) {
            throw new \InvalidArgumentException("Bad json file '{$this->filename}'");
        }

        $jsonDataImploded = $this->collapseArray($jsonData);

        foreach ($jsonDataImploded as $key => $value) {
            $config->set($key, $value);
        }
    }

    /**
     * Collapses array into a one-dimensional one by imploding nested keys with '-'
     *
     * @param array $arr
     *
     * @return array
     */
    private function collapseArray(array $arr)
    {
        $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($arr));
        $result = [];
        foreach ($iterator as $leafValue) {
            $keys = [];
            foreach (range(0, $iterator->getDepth()) as $depth) {
                $keys[] = $iterator->getSubIterator($depth)->key();
            }
            $result[join('-', $keys)] = $leafValue;
        }

        return $result;
    }
}


<?php

/*
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Hal\Component\File;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;

/**
 * File finder
 *
 * @author Jean-François Lépine <https://twitter.com/Halleck45>
 */
class Finder
{

    /**
     * Follow symlinks
     */
    const FOLLOW_SYMLINKS = RecursiveDirectoryIterator::FOLLOW_SYMLINKS;

    /**
     * Extensions to match
     *
     * @var array
     */
    private $extensions = [];

    /**
     * Subdirectories to exclude
     *
     * @var array
     */
    private $excludedDirs = [];

    /**
     * Flags for RecursiveDirectoryIterator
     *
     * @var integer
     */
    private $flags;

    /**
     * @param string[] $extensions   regex of file extensions to include
     * @param string[] $excludedDirs regex of directories to exclude
     * @param integer $flags
     */
    public function __construct(array $extensions = ['php'], array $excludedDirs = [], $flags = null)
    {
        $this->extensions = $extensions;
        $this->excludedDirs = $excludedDirs;
        $this->flags = $flags;
    }

    /**
     * Find files in path
     *
     * @param array $paths
     * @return array
     * @internal param string $path
     */
    public function fetch(array $paths)
    {
        $files = array();
        foreach ($paths as $path) {
            if (is_dir($path)) {
                $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
                $directory = new RecursiveDirectoryIterator($path, $this->flags);
                $iterator = new RecursiveIteratorIterator($directory);

                $filterRegex = sprintf(
                    '`^%s%s%s$`',
                    preg_quote($path, '`'),
                    !empty($this->excludedDirs) ? '((?!' . implode('|', array_map('preg_quote', $this->excludedDirs)) . ').)+' : '.+',
                    '\.(' . implode('|', $this->extensions) . ')'
                );

                $filteredIterator = new RegexIterator(
                    $iterator,
                    $filterRegex,
                    \RecursiveRegexIterator::GET_MATCH
                );

                foreach ($filteredIterator as $file) {
                    $files[] = $file[0];
                }
            } elseif (is_file($path)) {
                $files[] = $path;
            }
        }
        return $files;
    }
}


<?php
namespace Hal\Metric\Class_\Structural;

use Hal\Component\Tree\GraphDeduplicated;
use Hal\Component\Tree\Node as TreeNode;
use Hal\Metric\Helper\MetricClassNameGenerator;
use Hal\Metric\Metrics;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use PhpParser\NodeVisitorAbstract;

/**
 * Lack of cohesion of methods
 *
 * Class ExternalsVisitor
 * @package Hal\Metric\Class_\Coupling
 */
class LcomVisitor extends NodeVisitorAbstract
{

    /**
     * @var Metrics
     */
    private $metrics;

    /**
     * ClassEnumVisitor constructor.
     * @param Metrics $metrics
     */
    public function __construct(Metrics $metrics)
    {
        $this->metrics = $metrics;
    }

    /**
     * @inheritdoc
     */
    public function leaveNode(Node $node)
    {
        if ($node instanceof Stmt\Class_ || $node instanceof Stmt\Trait_) {

            // we build a graph of internal dependencies in class
            $graph = new GraphDeduplicated();
            $class = $this->metrics->get(MetricClassNameGenerator::getName($node));

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

                    if (!$graph->has($stmt->name . '()')) {
                        $graph->insert(new TreeNode($stmt->name . '()'));
                    }
                    $from = $graph->get($stmt->name . '()');

                    \iterate_over_node($stmt, function ($node) use ($from, &$graph) {

                        if ($node instanceof Node\Expr\PropertyFetch && isset($node->var->name) && $node->var->name == 'this') {
                            $name = getNameOfNode($node);
                            // use of attribute $this->xxx;
                            if (!$graph->has($name)) {
                                $graph->insert(new TreeNode($name));
                            }
                            $to = $graph->get($name);
                            $graph->addEdge($from, $to);
                            return;
                        }

                        if ($node instanceof Node\Expr\MethodCall) {
                            if (!$node->var instanceof Node\Expr\New_ && isset($node->var->name) && getNameOfNode($node->var) === 'this') {
                                // use of method call $this->xxx();
                                // use of attribute $this->xxx;
                                $name = getNameOfNode($node->name) . '()';
                                if (!$graph->has($name)) {
                                    $graph->insert(new TreeNode($name));
                                }
                                $to = $graph->get($name);
                                $graph->addEdge($from, $to);
                                return;
                            }
                        }

                    });
                }
            }

            // we count paths
            $paths = 0;
            foreach ($graph->all() as $node) {
                $paths += $this->traverse($node);
            }

            $class->set('lcom', $paths);
        }
    }

    /**
     * Traverse node, and return 1 if node has not been visited yet
     *
     * @param TreeNode $node
     * @return int
     */
    private function traverse(TreeNode $node)
    {
        if ($node->visited) {
            return 0;
        }
        $node->visited = true;

        foreach ($node->getAdjacents() as $adjacent) {
            $this->traverse($adjacent);
        }

        return 1;
    }
}


<?php

namespace Hal\Application\Config\File;

use Hal\Application\Config\Config;

class ConfigFileReaderIni implements ConfigFileReaderInterface
{
    /**
     * @var string
     */
    private $filename;

    /**
     * ConfigFileReaderIni constructor.
     *
     * @param string $filename
     */
    public function __construct($filename)
    {
        $this->filename = $filename;
    }

    /**
     * @param Config $config
     *
     * @return void
     */
    public function read(Config $config)
    {
        $options = parse_ini_file($this->filename);

        if (false === $options) {
            throw new \InvalidArgumentException("Cannot parse configuration file '{$this->filename}'");
        }

        foreach ($options as $name => $value) {
            $config->set($name, $value);
        }
    }
}


<?php

namespace Hal\Application\Config\File;

class ConfigFileReaderFactory
{
    /**
     * @param string $filename
     *
     * @return ConfigFileReaderInterface
     */
    public static function createFromFileName($filename)
    {
        if (!is_file($filename) || !is_readable($filename)) {
            throw new \InvalidArgumentException("Cannot read configuration file '{$filename}'");
        }

        switch (pathinfo($filename, PATHINFO_EXTENSION)) {
            case 'json':
                return new ConfigFileReaderJson($filename);
            case 'ini':
                return new ConfigFileReaderIni($filename);
                break;
            default:
                throw new \InvalidArgumentException("Unsupported config file format: '$filename'");
        }
    }
}


<?php

namespace Hal\Metric\Helper;

use PhpParser\Node;

/**
 * Performs
 */
class MetricClassNameGenerator
{
    /**
     * @param Node|Node\Stmt\Class_|Node\Stmt\Interface_ $node
     *
     * @return string
     */
    public static function getName(Node $node)
    {
        return ($node instanceof Node\Stmt\Class_ && $node->isAnonymous()) ?
            'anonymous@' . spl_object_hash($node) :
            $node->namespacedName->toString();
    }
}


<?php

/*
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Hal\Component\Tree;

class Graph implements \Countable
{
    /**
     * @var Node[]
     */
    private $datas = array();

    /**
     * @var Edge[]
     */
    private $edges = array();

    /**
     * @param Node $node
     * @return $this
     */
    public function insert(Node $node)
    {
        if ($this->has($node->getKey())) {
            throw new GraphException(sprintf('node %s is already present', $node->getKey()));
        }
        $this->datas[$node->getKey()] = $node;
        return $this;
    }

    /**
     * @param Node $from
     * @param Node $to
     * @return $this
     */
    public function addEdge(Node $from, Node $to)
    {
        if (!$this->has($from->getKey())) {
            throw new GraphException('from is not is in the graph');
        }
        if (!$this->has($to->getKey())) {
            throw new GraphException('to is not is in the graph');
        }

        $edge = new Edge($from, $to);
        $from->addEdge($edge);
        $to->addEdge($edge);
        array_push($this->edges, $edge);

        return $this;
    }

    /**
     * @return string
     */
    public function asString()
    {
        $string = '';
        foreach ($this->all() as $node) {
            $string .= sprintf("%s;\n", $node->getKey());
        }
        foreach ($this->getEdges() as $edge) {
            $string .= sprintf("%s;\n", $edge->asString());
        }
        return $string;
    }

    /**
     * @return Edge[]
     */
    public function getEdges()
    {
        return $this->edges;
    }

    /**
     * @param $key
     * @return Node|null
     */
    public function get($key)
    {
        return $this->has($key) ? $this->datas[$key] : null;
    }

    /**
     * @param $key
     * @return bool
     */
    public function has($key)
    {
        return isset($this->datas[$key]);
    }

    /**
     * @return int
     */
    public function count()
    {
        return sizeof($this->datas);
    }

    /**
     * @return Node[]
     */
    public function all()
    {
        return $this->datas;
    }

    /**
     * @return $this
     */
    public function resetVisits()
    {
        foreach ($this->all() as $node) {
            $node->visited = false;
        }
        return $this;
    }

    /**
     * Get the list of all root nodes
     *
     *      we can have array of roots : graph can be a "forest"
     *
     * @return array
     */
    public function getRootNodes()
    {
        $roots = [];
        foreach ($this->all() as $node) {

            $isRoot = true;

            foreach ($node->getEdges() as $edge) {
                if ($edge->getTo() == $node) {
                    $isRoot = false;
                }
            }

            if ($isRoot) {
                array_push($roots, $node);
            }
        }

        return $roots;
    }
}


<?php
namespace Hal\Metric\System\Packages\Composer;

use Hal\Application\Config\Config;
use Hal\Component\File\Finder;
use Hal\Metric\Metrics;
use Hal\Metric\ProjectMetric;

/**
 * Class Composer
 * @package Hal\Metric\System\Packages\Composer
 */
class Composer
{

    /**
     * @var Config
     */
    private $config;

    /**
     * GitChanges constructor.
     * @param array $files
     */
    public function __construct(Config $config, array $files)
    {
        $this->config = $config;
    }

    /**
     * @param Metrics $metrics
     * @throws ConfigException
     */
    public function calculate(Metrics $metrics)
    {
        $projectMetric = new ProjectMetric('composer');
        $projectMetric->set('packages', []);
        $metrics->attach($projectMetric);
        $packages = [];
        $rawRequirements = $this->getComposerJsonRequirements();
        $rawInstalled = $this->getComposerLockInstalled(\array_keys($rawRequirements));

        $packagist = new Packagist();
        foreach ($rawRequirements as $requirement => $version) {

            $package = $packagist->get($requirement);

            $packages[$requirement] = (object)array(
                'name' => $requirement,
                'required' => $version,
                'installed' => isset($rawInstalled[$requirement]) ? $rawInstalled[$requirement] : null,
                'latest' => $package->latest,
                'license' => $package->license,
                'homepage' => $package->homepage,
                'zip' => $package->zip,
            );
        }

        $projectMetric->set('packages', $packages);
        $projectMetric->set('packages-installed', $rawInstalled);
    }

    /**
     * Returns the requirements defined in the composer(-dist)?.json file.
     * @return array
     */
    protected function getComposerJsonRequirements()
    {
        $rawRequirements = [[]];

        // find composer.json files
        $finder = new Finder(['json'], $this->config->get('exclude'));
        $files = $finder->fetch($this->config->get('files'));

        foreach ($files as $filename) {
            if (!\preg_match('/composer(-dist)?\.json/', $filename)) {
                continue;
            }
            $composerJson = (object)\json_decode(\file_get_contents($filename));

            if (!isset($composerJson->require)) {
                continue;
            }

            $rawRequirements[] = (array)$composerJson->require;
        }

        return \call_user_func_array('array_merge', $rawRequirements);
    }

    /**
     * Returns the installed packages from the composer.lock file.
     * @param array $rootPackageRequirements List of requirements to match installed packages only with requirements.
     * @return array
     */
    protected function getComposerLockInstalled($rootPackageRequirements)
    {
        $rawInstalled = [[]];

        // Find composer.lock file
        $finder = new Finder(['lock'], $this->config->get('exclude'));
        $files = $finder->fetch($this->config->get('files'));

        // List all composer.lock found in the project.
        foreach ($files as $filename) {
            if (false === \strpos($filename, 'composer.lock')) {
                continue;
            }
            $composerLockJson = (object)\json_decode(\file_get_contents($filename));

            if (!isset($composerLockJson->packages)) {
                continue;
            }

            $installed = [];
            foreach ($composerLockJson->packages as $package) {
                if (!\in_array($package->name, $rootPackageRequirements, true)) {
                    continue;
                }

                $installed[$package->name] = \preg_replace('#[^.\d]#', '', $package->version);
            }

            $rawInstalled[] = $installed;
        }

        return \call_user_func_array('array_merge', $rawInstalled);
    }
}


<?php

/*
 * (c) Jean-François Lépine <https://twitter.com/Halleck45>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Hal\Component\Output;

    /**
     * Class CliOutput
     * @package Hal\Component\Output
     */
    /**
     * Class CliOutput
     * @package Hal\Component\Issue
     */
/**
 * Class ProgressBar
 * @package Hal\Component\Output
 */
class ProgressBar
{

    /**
     * @var Output
     */
    private $output;

    /**
     * @var int
     */
    private $max;

    /**
     * @var int
     */
    private $current = 0;

    /**
     * ProgressBar constructor.
     * @param Output $output
     * @param int $max
     */
    public function __construct(Output $output, $max)
    {
        $this->output = $output;
        $this->max = $max;
    }

    /**
     * Start progress bar
     */
    public function start()
    {
        $this->current = 0;
    }

    /**
     * Advance progress bar
     */
    public function advance()
    {
        $this->current++;

        if ($this->hasAnsi()) {
            $percent = round($this->current / $this->max * 100);
            $this->output->write("\x0D");
            $this->output->write("\x1B[2K");
            $this->output->write(sprintf('... %s%% ...', $percent));
        } else {
            $this->output->write('.');
        }
    }

    /**
     * Clear console
     */
    public function clear()
    {
        if ($this->hasAnsi()) {
            $this->output->write("\x0D");
            $this->output->write("\x1B[2K");
            $this->output->clearln();
        }
    }

    /**
     * Detects ANSI support
     *
     * @return bool
     */
    protected function hasAnsi()
    {
        if (DIRECTORY_SEPARATOR === '\\') {
            return
                0 >= version_compare('10.0.10586',
                    PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD)
                || false !== getenv('ANSICON')
                || 'ON' === getenv('ConEmuANSI')
                || 'xterm' === getenv('TERM');
        }

        return function_exists('posix_isatty') && @posix_isatty($this->stream);
    }
}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\Security;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Flag any non-validated/sanitized input ( _GET / _POST / etc. ).
 *
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/69
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.4.0  This class now extends the WordPressCS native `Sniff` class.
 * @since   0.5.0  Method getArrayIndexKey() has been moved to the WordPressCS native `Sniff` class.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   1.0.0  This sniff has been moved from the `VIP` category to the `Security` category.
 */
class ValidatedSanitizedInputSniff extends Sniff {

	/**
	 * Check for validation functions for a variable within its own parenthesis only.
	 *
	 * @var boolean
	 */
	public $check_validation_in_scope_only = false;

	/**
	 * Custom list of functions that sanitize the values passed to them.
	 *
	 * @since 0.5.0
	 *
	 * @var string|string[]
	 */
	public $customSanitizingFunctions = array();

	/**
	 * Custom sanitizing functions that implicitly unslash the values passed to them.
	 *
	 * @since 0.5.0
	 *
	 * @var string|string[]
	 */
	public $customUnslashingSanitizingFunctions = array();

	/**
	 * Cache of previously added custom functions.
	 *
	 * Prevents having to do the same merges over and over again.
	 *
	 * @since 0.5.0
	 * @since 0.11.0 - Changed from static to non-static.
	 *               - Changed the format from simple bool to array.
	 *
	 * @var array
	 */
	protected $addedCustomFunctions = array(
		'sanitize'        => array(),
		'unslashsanitize' => array(),
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_VARIABLE,
			\T_DOUBLE_QUOTED_STRING,
			\T_HEREDOC,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_token( $stackPtr ) {

		$superglobals = $this->input_superglobals;

		// Handling string interpolation.
		if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $stackPtr ]['code']
			|| \T_HEREDOC === $this->tokens[ $stackPtr ]['code']
		) {
			$interpolated_variables = array_map(
				function ( $symbol ) {
					return '$' . $symbol;
				},
				$this->get_interpolated_variables( $this->tokens[ $stackPtr ]['content'] )
			);
			foreach ( array_intersect( $interpolated_variables, $superglobals ) as $bad_variable ) {
				$this->phpcsFile->addError( 'Detected usage of a non-sanitized, non-validated input variable %s: %s', $stackPtr, 'InputNotValidatedNotSanitized', array( $bad_variable, $this->tokens[ $stackPtr ]['content'] ) );
			}

			return;
		}

		// Check if this is a superglobal.
		if ( ! \in_array( $this->tokens[ $stackPtr ]['content'], $superglobals, true ) ) {
			return;
		}

		// If we're overriding a superglobal with an assignment, no need to test.
		if ( $this->is_assignment( $stackPtr ) ) {
			return;
		}

		// This superglobal is being validated.
		if ( $this->is_in_isset_or_empty( $stackPtr ) ) {
			return;
		}

		$array_keys = $this->get_array_access_keys( $stackPtr );

		if ( empty( $array_keys ) ) {
			return;
		}

		$error_data = array( $this->tokens[ $stackPtr ]['content'] . '[' . implode( '][', $array_keys ) . ']' );

		/*
		 * Check for validation first.
		 */
		$validated = false;

		for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) {
			if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
				continue;
			}

			if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $i ]['code']
				&& isset( $this->tokens[ $i ]['bracket_closer'] )
			) {
				// Skip over array keys.
				$i = $this->tokens[ $i ]['bracket_closer'];
				continue;
			}

			if ( \T_COALESCE === $this->tokens[ $i ]['code'] ) {
				$validated = true;
			}

			// Anything else means this is not a validation coalesce.
			break;
		}

		if ( false === $validated ) {
			$validated = $this->is_validated( $stackPtr, $array_keys, $this->check_validation_in_scope_only );
		}

		if ( false === $validated ) {
			$this->phpcsFile->addError(
				'Detected usage of a possibly undefined superglobal array index: %s. Use isset() or empty() to check the index exists before using it',
				$stackPtr,
				'InputNotValidated',
				$error_data
			);
		}

		if ( $this->has_whitelist_comment( 'sanitization', $stackPtr ) ) {
			return;
		}

		// If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed.
		if ( $this->is_in_type_test( $stackPtr ) ) {
			return;
		}

		// If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed.
		if ( $this->is_comparison( $stackPtr, false ) ) {
			return;
		}

		// If this is a comparison using the array comparison functions, sanitization isn't needed.
		if ( $this->is_in_array_comparison( $stackPtr ) ) {
			return;
		}

		$this->mergeFunctionLists();

		// Now look for sanitizing functions.
		if ( ! $this->is_sanitized( $stackPtr, true ) ) {
			$this->phpcsFile->addError(
				'Detected usage of a non-sanitized input variable: %s',
				$stackPtr,
				'InputNotSanitized',
				$error_data
			);
		}
	}

	/**
	 * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already.
	 *
	 * @since 0.11.0 Split out from the `process()` method.
	 *
	 * @return void
	 */
	protected function mergeFunctionLists() {
		if ( $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) {
			$this->sanitizingFunctions = $this->merge_custom_array(
				$this->customSanitizingFunctions,
				$this->sanitizingFunctions
			);

			$this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions;
		}

		if ( $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) {
			$this->unslashingSanitizingFunctions = $this->merge_custom_array(
				$this->customUnslashingSanitizingFunctions,
				$this->unslashingSanitizingFunctions
			);

			$this->addedCustomFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions;
		}
	}

}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\Utils;

use WordPressCS\WordPress\AbstractFunctionParameterSniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Comprehensive I18n text domain fixer tool.
 *
 * This sniff can:
 * - Add missing text domains.
 * - Replace text domains based on an array of `old` values to a `new` value.
 *
 * Note: Without a user-defined configuration in a custom ruleset, this sniff will be ignored.
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   1.2.0
 */
class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff {

	/**
	 * A list of tokenizers this sniff supports.
	 *
	 * @since 1.2.0
	 *
	 * @var array
	 */
	public $supportedTokenizers = array(
		'PHP',
		'CSS',
	);

	/**
	 * Old text domain(s) to replace.
	 *
	 * @since 1.2.0
	 *
	 * @var string[]|string
	 */
	public $old_text_domain;

	/**
	 * New text domain.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	public $new_text_domain = '';

	/**
	 * The group name for this group of functions.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	protected $group_name = 'i18nfixer';

	/**
	 * The WP Internationalization related functions to target for the replacements.
	 *
	 * @since 1.2.0
	 *
	 * @var array <string function name> => <int parameter position>
	 */
	protected $target_functions = array(
		'load_textdomain'                        => 1,
		'load_plugin_textdomain'                 => 1,
		'load_muplugin_textdomain'               => 1,
		'load_theme_textdomain'                  => 1,
		'load_child_theme_textdomain'            => 1,
		'unload_textdomain'                      => 1,

		'__'                                     => 2,
		'_e'                                     => 2,
		'_x'                                     => 3,
		'_ex'                                    => 3,
		'_n'                                     => 4,
		'_nx'                                    => 5,
		'_n_noop'                                => 3,
		'_nx_noop'                               => 4,
		'translate_nooped_plural'                => 3,
		'_c'                                     => 2, // Deprecated.
		'_nc'                                    => 4, // Deprecated.
		'__ngettext'                             => 4, // Deprecated.
		'__ngettext_noop'                        => 3, // Deprecated.
		'translate_with_context'                 => 2, // Deprecated.

		'esc_html__'                             => 2,
		'esc_html_e'                             => 2,
		'esc_html_x'                             => 3,
		'esc_attr__'                             => 2,
		'esc_attr_e'                             => 2,
		'esc_attr_x'                             => 3,

		'is_textdomain_loaded'                   => 1,
		'get_translations_for_domain'            => 1,

		// Shouldn't be used by plugins/themes.
		'translate'                              => 2,
		'translate_with_gettext_context'         => 3,

		// WP private functions. Shouldn't be used by plugins/themes.
		'_load_textdomain_just_in_time'          => 1,
		'_get_path_to_translation_from_lang_dir' => 1,
		'_get_path_to_translation'               => 1,
	);

	/**
	 * Whether a valid new text domain was found.
	 *
	 * @since 1.2.0
	 *
	 * @var bool
	 */
	private $is_valid = false;

	/**
	 * The new text domain as validated.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $validated_textdomain = '';

	/**
	 * Whether the plugin/theme header has been seen and fixed yet.
	 *
	 * @since 1.2.0
	 *
	 * @var bool
	 */
	private $header_found = false;

	/**
	 * Possible headers for a theme.
	 *
	 * @link https://developer.wordpress.org/themes/basics/main-stylesheet-style-css/
	 *
	 * @since 1.2.0
	 *
	 * @var array Array key is the header name, the value indicated whether it is a
	 *            required (true) or optional (false) header.
	 */
	private $theme_headers = array(
		'Theme Name'  => true,
		'Theme URI'   => false,
		'Author'      => true,
		'Author URI'  => false,
		'Description' => true,
		'Version'     => true,
		'License'     => true,
		'License URI' => true,
		'Tags'        => false,
		'Text Domain' => true,
		'Domain Path' => false,
	);

	/**
	 * Possible headers for a plugin.
	 *
	 * @link https://developer.wordpress.org/plugins/the-basics/header-requirements/
	 *
	 * @since 1.2.0
	 *
	 * @var array Array key is the header name, the value indicated whether it is a
	 *            required (true) or optional (false) header.
	 */
	private $plugin_headers = array(
		'Plugin Name' => true,
		'Plugin URI'  => false,
		'Description' => false,
		'Version'     => false,
		'Author'      => false,
		'Author URI'  => false,
		'License'     => false,
		'License URI' => false,
		'Text Domain' => false,
		'Domain Path' => false,
		'Network'     => false,
	);

	/**
	 * Regex template to match theme/plugin headers.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $header_regex_template = '`^(?:\s*(?:(?:\*|//)\s*)?)?(%s)\s*:\s*([^\r\n]+)`';

	/**
	 * Regex to match theme headers.
	 *
	 * Set from within the register() method.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $theme_header_regex;

	/**
	 * Regex to match plugin headers.
	 *
	 * Set from within the register() method.
	 *
	 * @since 1.2.0
	 *
	 * @var string
	 */
	private $plugin_header_regex;

	/**
	 * The --tab-width CLI value that is being used.
	 *
	 * @since 1.2.0
	 *
	 * @var integer
	 */
	private $tab_width = null;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @since 1.2.0
	 *
	 * @return array
	 */
	public function register() {
		$headers                  = array_map(
			'preg_quote',
			array_keys( $this->theme_headers ),
			array_fill( 0, \count( $this->theme_headers ), '`' )
		);
		$this->theme_header_regex = sprintf( $this->header_regex_template, implode( '|', $headers ) );

		$headers                   = array_map(
			'preg_quote',
			array_keys( $this->plugin_headers ),
			array_fill( 0, \count( $this->plugin_headers ), '`' )
		);
		$this->plugin_header_regex = sprintf( $this->header_regex_template, implode( '|', $headers ) );

		$targets = parent::register();

		$targets[] = \T_DOC_COMMENT_OPEN_TAG;
		$targets[] = \T_COMMENT;

		return $targets;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @since 1.2.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {
		// Check if the old/new properties are correctly set. If not, bow out.
		if ( ! is_string( $this->new_text_domain )
			|| '' === $this->new_text_domain
		) {
			return ( $this->phpcsFile->numTokens + 1 );
		}

		if ( isset( $this->old_text_domain ) ) {
			$this->old_text_domain = $this->merge_custom_array( $this->old_text_domain, array(), false );

			if ( ! is_array( $this->old_text_domain )
				|| array() === $this->old_text_domain
			) {
				return ( $this->phpcsFile->numTokens + 1 );
			}
		}

		// Only validate and throw warning about the text domain once.
		if ( $this->new_text_domain !== $this->validated_textdomain ) {
			$this->is_valid             = false;
			$this->validated_textdomain = $this->new_text_domain;
			$this->header_found         = false;

			if ( 'default' === $this->new_text_domain ) {
				$this->phpcsFile->addWarning(
					'The "default" text domain is reserved for WordPress core use and should not be used by plugins or themes',
					0,
					'ReservedNewDomain',
					array( $this->new_text_domain )
				);

				return ( $this->phpcsFile->numTokens + 1 );
			}

			if ( preg_match( '`^[a-z0-9-]+$`', $this->new_text_domain ) !== 1 ) {
				$this->phpcsFile->addWarning(
					'The text domain should be a simple lowercase text string with words separated by dashes. "%s" appears invalid',
					0,
					'InvalidNewDomain',
					array( $this->new_text_domain )
				);

				return ( $this->phpcsFile->numTokens + 1 );
			}

			// If the text domain passed both validations, it should be considered valid.
			$this->is_valid = true;

		} elseif ( false === $this->is_valid ) {
			return ( $this->phpcsFile->numTokens + 1 );
		}

		if ( isset( $this->tab_width ) === false ) {
			if ( isset( $this->phpcsFile->config->tabWidth ) === false
				|| 0 === $this->phpcsFile->config->tabWidth
			) {
				// We have no idea how wide tabs are, so assume 4 spaces for fixing.
				$this->tab_width = 4;
			} else {
				$this->tab_width = $this->phpcsFile->config->tabWidth;
			}
		}

		if ( \T_DOC_COMMENT_OPEN_TAG === $this->tokens[ $stackPtr ]['code']
			|| \T_COMMENT === $this->tokens[ $stackPtr ]['code']
		) {
			// Examine for plugin/theme file header.
			return $this->process_comments( $stackPtr );

		} elseif ( 'CSS' !== $this->phpcsFile->tokenizerType ) {
			// Examine a T_STRING token in a PHP file as a function call.
			return parent::process_token( $stackPtr );
		}
	}

	/**
	 * Process the parameters of a matched function.
	 *
	 * @since 1.2.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched.
	 * @param string $matched_content The token content (function name) which was matched.
	 * @param array  $parameters      Array with information about the parameters.
	 *
	 * @return void
	 */
	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
		$target_param = $this->target_functions[ $matched_content ];

		if ( isset( $parameters[ $target_param ] ) === false && 1 !== $target_param ) {
			$error_msg  = 'Missing $domain arg';
			$error_code = 'MissingArgDomain';

			if ( isset( $parameters[ ( $target_param - 1 ) ] ) ) {
				$fix = $this->phpcsFile->addFixableError( $error_msg, $stackPtr, $error_code );

				if ( true === $fix ) {
					$start_previous = $parameters[ ( $target_param - 1 ) ]['start'];
					$end_previous   = $parameters[ ( $target_param - 1 ) ]['end'];
					if ( \T_WHITESPACE === $this->tokens[ $start_previous ]['code']
						&& $this->tokens[ $start_previous ]['content'] === $this->phpcsFile->eolChar
					) {
						// Replicate the new line + indentation of the previous item.
						$replacement = ',';
						for ( $i = $start_previous; $i <= $end_previous; $i++ ) {
							if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) {
								break;
							}

							if ( isset( $this->tokens[ $i ]['orig_content'] ) ) {
								$replacement .= $this->tokens[ $i ]['orig_content'];
							} else {
								$replacement .= $this->tokens[ $i ]['content'];
							}
						}

						$replacement .= "'{$this->new_text_domain}'";
					} else {
						$replacement = ", '{$this->new_text_domain}'";
					}

					if ( \T_WHITESPACE === $this->tokens[ $end_previous ]['code'] ) {
						$this->phpcsFile->fixer->addContentBefore( $end_previous, $replacement );
					} else {
						$this->phpcsFile->fixer->addContent( $end_previous, $replacement );
					}
				}
			} else {
				$error_msg .= ' and preceding argument(s)';
				$error_code = 'MissingArgs';

				// Expected preceeding param also missing, just throw the warning.
				$this->phpcsFile->addWarning( $error_msg, $stackPtr, $error_code );
			}

			return;
		}

		// Target parameter found. Let's examine it.
		$domain_param_start = $parameters[ $target_param ]['start'];
		$domain_param_end   = $parameters[ $target_param ]['end'];
		$domain_token       = null;

		for ( $i = $domain_param_start; $i <= $domain_param_end; $i++ ) {
			if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) {
				continue;
			}

			if ( \T_CONSTANT_ENCAPSED_STRING !== $this->tokens[ $i ]['code'] ) {
				// Unexpected token found, not our concern. This is handled by the I18n sniff.
				return;
			}

			if ( isset( $domain_token ) ) {
				// More than one T_CONSTANT_ENCAPSED_STRING found, not our concern. This is handled by the I18n sniff.
				return;
			}

			$domain_token = $i;
		}

		// If we're still here, this means only one T_CONSTANT_ENCAPSED_STRING was found.
		$old_domain = $this->strip_quotes( $this->tokens[ $domain_token ]['content'] );

		if ( ! \in_array( $old_domain, $this->old_text_domain, true ) ) {
			// Not a text domain targetted for replacement, ignore.
			return;
		}

		$fix = $this->phpcsFile->addFixableError(
			'Mismatched text domain. Expected \'%s\' but found \'%s\'',
			$domain_token,
			'TextDomainMismatch',
			array( $this->new_text_domain, $old_domain )
		);

		if ( true === $fix ) {
			$replacement = str_replace( $old_domain, $this->new_text_domain, $this->tokens[ $domain_token ]['content'] );
			$this->phpcsFile->fixer->replaceToken( $domain_token, $replacement );
		}
	}

	/**
	 * Process the function if no parameters were found.
	 *
	 * @since 1.2.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param array  $group_name      The name of the group which was matched.
	 * @param string $matched_content The token content (function name) which was matched.
	 *
	 * @return void
	 */
	public function process_no_parameters( $stackPtr, $group_name, $matched_content ) {

		$target_param = $this->target_functions[ $matched_content ];

		if ( 1 !== $target_param ) {
			// Only process the no param case as fixable if the text domain is expected to be the first parameter.
			$this->phpcsFile->addWarning( 'Missing $domain arg and preceding argument(s)', $stackPtr, 'MissingArgs' );
			return;
		}

		$opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
		if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $opener ]['code']
			|| isset( $this->tokens[ $opener ]['parenthesis_closer'] ) === false
		) {
			// Parse error or live coding.
			return;
		}

		$fix = $this->phpcsFile->addFixableError( 'Missing $domain arg', $stackPtr, 'MissingArgDomain' );
		if ( true === $fix ) {
			$closer      = $this->tokens[ $opener ]['parenthesis_closer'];
			$replacement = " '{$this->new_text_domain}' ";

			if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) {
				$replacement = trim( $replacement );
				$addBefore   = ( $closer - 1 );
				if ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code']
					&& $this->tokens[ $closer - 1 ]['line'] === $this->tokens[ $closer ]['line']
				) {
					if ( isset( $this->tokens[ ( $closer - 1 ) ]['orig_content'] ) ) {
						$replacement = $this->tokens[ ( $closer - 1 ) ]['orig_content']
							. "\t"
							. $replacement;
					} else {
						$replacement = $this->tokens[ ( $closer - 1 ) ]['content']
							. str_repeat( ' ', $this->tab_width )
							. $replacement;
					}

					--$addBefore;
				} else {
					// We don't know whether the code uses tabs or spaces, so presume WPCS, i.e. tabs.
					$replacement = "\t" . $replacement;
				}

				$replacement = $this->phpcsFile->eolChar . $replacement;

				$this->phpcsFile->fixer->addContentBefore( $addBefore, $replacement );

			} elseif ( \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code'] ) {
				$this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), $replacement );
			} else {
				$this->phpcsFile->fixer->addContentBefore( $closer, $replacement );
			}
		}
	}

	/**
	 * Process comments to find the plugin/theme headers.
	 *
	 * @since 1.2.0
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_comments( $stackPtr ) {
		if ( true === $this->header_found && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
			return;
		}

		$regex   = $this->plugin_header_regex;
		$headers = $this->plugin_headers;
		$type    = 'plugin';
		$skip_to = $stackPtr;

		$file = $this->strip_quotes( $this->phpcsFile->getFileName() );
		if ( 'STDIN' === $file ) {
			return;
		}

		$file_name = basename( $file );
		if ( 'CSS' === $this->phpcsFile->tokenizerType ) {
			if ( 'style.css' !== $file_name && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
				// CSS files only need to be examined for the file header.
				return ( $this->phpcsFile->numTokens + 1 );
			}

			$regex   = $this->theme_header_regex;
			$headers = $this->theme_headers;
			$type    = 'theme';
		}

		$comment_details = array(
			'required_header_found' => false,
			'headers_found'         => 0,
			'text_domain_ptr'       => false,
			'text_domain_found'     => '',
			'last_header_ptr'       => false,
			'last_header_matches'   => array(),
		);

		if ( \T_COMMENT === $this->tokens[ $stackPtr ]['code'] ) {
			$block_comment = false;
			if ( substr( $this->tokens[ $stackPtr ]['content'], 0, 2 ) === '/*' ) {
				$block_comment = true;
			}

			$current = $stackPtr;
			do {
				if ( false === $comment_details['text_domain_ptr']
					|| false === $comment_details['required_header_found']
					|| $comment_details['headers_found'] < 3
				) {
					$comment_details = $this->examine_comment_line( $current, $regex, $headers, $comment_details );
				}

				if ( true === $block_comment && substr( $this->tokens[ $current ]['content'], -2 ) === '*/' ) {
					++$current;
					break;
				}

				++$current;
			} while ( isset( $this->tokens[ $current ] ) && \T_COMMENT === $this->tokens[ $current ]['code'] );

			$skip_to = $current;

		} else {
			if ( ! isset( $this->tokens[ $stackPtr ]['comment_closer'] ) ) {
				return;
			}

			$closer  = $this->tokens[ $stackPtr ]['comment_closer'];
			$current = $stackPtr;

			while ( ( $current = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $current + 1 ), $closer ) ) !== false ) {
				$comment_details = $this->examine_comment_line( $current, $regex, $headers, $comment_details );

				if ( false !== $comment_details['text_domain_ptr']
					&& true === $comment_details['required_header_found']
					&& $comment_details['headers_found'] >= 3
				) {
					// No need to look at the rest of the docblock.
					break;
				}
			}

			$skip_to = $closer;
		}

		// So, was this the plugin/theme header ?
		if ( true === $comment_details['required_header_found']
			&& $comment_details['headers_found'] >= 3
		) {
			$this->header_found = true;

			$text_domain_ptr   = $comment_details['text_domain_ptr'];
			$text_domain_found = $comment_details['text_domain_found'];

			if ( false !== $text_domain_ptr ) {
				if ( $this->new_text_domain !== $text_domain_found
					&& ( \in_array( $text_domain_found, $this->old_text_domain, true ) )
				) {
					$fix = $this->phpcsFile->addFixableError(
						'Mismatched text domain in %s header. Expected \'%s\' but found \'%s\'',
						$text_domain_ptr,
						'TextDomainHeaderMismatch',
						array(
							$type,
							$this->new_text_domain,
							$text_domain_found,
						)
					);

					if ( true === $fix ) {
						if ( isset( $this->tokens[ $text_domain_ptr ]['orig_content'] ) ) {
							$replacement = $this->tokens[ $text_domain_ptr ]['orig_content'];
						} else {
							$replacement = $this->tokens[ $text_domain_ptr ]['content'];
						}

						$replacement = str_replace( $text_domain_found, $this->new_text_domain, $replacement );

						$this->phpcsFile->fixer->replaceToken( $text_domain_ptr, $replacement );
					}
				}
			} else {
				$last_header_ptr     = $comment_details['last_header_ptr'];
				$last_header_matches = $comment_details['last_header_matches'];

				$fix = $this->phpcsFile->addFixableError(
					'Missing "Text Domain" in %s header',
					$last_header_ptr,
					'MissingTextDomainHeader',
					array( $type )
				);

				if ( true === $fix ) {
					if ( isset( $this->tokens[ $last_header_ptr ]['orig_content'] ) ) {
						$replacement = $this->tokens[ $last_header_ptr ]['orig_content'];
					} else {
						$replacement = $this->tokens[ $last_header_ptr ]['content'];
					}

					$replacement = str_replace( $last_header_matches[1], 'Text Domain', $replacement );
					$replacement = str_replace( $last_header_matches[2], $this->new_text_domain, $replacement );

					if ( \T_DOC_COMMENT_OPEN_TAG === $this->tokens[ $stackPtr ]['code'] ) {
						for ( $i = ( $last_header_ptr - 1 ); ; $i-- ) {
							if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $last_header_ptr ]['line'] ) {
								++$i;
								break;
							}
						}

						$replacement = $this->phpcsFile->eolChar
							. $this->phpcsFile->getTokensAsString( $i, ( $last_header_ptr - $i ), true )
							. $replacement;
					}

					$this->phpcsFile->fixer->addContent( $comment_details['last_header_ptr'], $replacement );
				}
			}
		}

		return $skip_to;
	}

	/**
	 * Examine an individual token in a larger comment for plugin/theme headers.
	 *
	 * @since 1.2.0
	 *
	 * @param int    $stackPtr        The position of the current token in the stack.
	 * @param string $regex           The regex to use to examine the comment line.
	 * @param array  $headers         Valid headers for a plugin or theme.
	 * @param array  $comment_details The information collected so far.
	 *
	 * @return array Adjusted $comment_details array
	 */
	protected function examine_comment_line( $stackPtr, $regex, $headers, $comment_details ) {
		if ( preg_match( $regex, $this->tokens[ $stackPtr ]['content'], $matches ) === 1 ) {
			++$comment_details['headers_found'];

			if ( true === $headers[ $matches[1] ] ) {
				$comment_details['required_header_found'] = true;
			}

			if ( 'Text Domain' === $matches[1] ) {
				$comment_details['text_domain_ptr']   = $stackPtr;
				$comment_details['text_domain_found'] = trim( $matches[2] );
			}

			$comment_details['last_header_ptr']     = $stackPtr;
			$comment_details['last_header_matches'] = $matches;
		}

		return $comment_details;
	}
}


<?php
/**
 * WordPress Coding Standard.
 *
 * @package WPCS\WordPressCodingStandards
 * @link    https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WordPressCS\WordPress\Sniffs\WP;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Flag cron schedules less than 15 minutes.
 *
 * @link    https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cron-schedules-less-than-15-minutes-or-expensive-events
 *
 * @package WPCS\WordPressCodingStandards
 *
 * @since   0.3.0
 * @since   0.11.0 - Extends the WordPressCS native `Sniff` class.
 *                 - Now deals correctly with WP time constants.
 * @since   0.13.0 Class name changed: this class is now namespaced.
 * @since   0.14.0 The minimum cron interval tested against is now configurable.
 * @since   1.0.0  This sniff has been moved from the `VIP` category to the `WP` category.
 */
class CronIntervalSniff extends Sniff {

	/**
	 * Minimum allowed cron interval in seconds.
	 *
	 * Defaults to 900 (= 15 minutes), which is the requirement for the VIP platform.
	 *
	 * @since 0.14.0
	 *
	 * @var int
	 */
	public $min_interval = 900;

	/**
	 * Known WP Time constant names and their value.
	 *
	 * @since 0.11.0
	 *
	 * @var array
	 */
	protected $wp_time_constants = array(
		'MINUTE_IN_SECONDS' => 60,
		'HOUR_IN_SECONDS'   => 3600,
		'DAY_IN_SECONDS'    => 86400,
		'WEEK_IN_SECONDS'   => 604800,
		'MONTH_IN_SECONDS'  => 2592000,
		'YEAR_IN_SECONDS'   => 31536000,
	);

	/**
	 * Function within which the hook should be found.
	 *
	 * @var array
	 */
	protected $valid_functions = array(
		'add_filter' => true,
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_CONSTANT_ENCAPSED_STRING,
			\T_DOUBLE_QUOTED_STRING,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return void
	 */
	public function process_token( $stackPtr ) {
		$token = $this->tokens[ $stackPtr ];

		if ( 'cron_schedules' !== $this->strip_quotes( $token['content'] ) ) {
			return;
		}

		// If within add_filter.
		$functionPtr = $this->is_in_function_call( $stackPtr, $this->valid_functions );
		if ( false === $functionPtr ) {
			return;
		}

		$callback = $this->get_function_call_parameter( $functionPtr, 2 );
		if ( false === $callback ) {
			return;
		}

		if ( $stackPtr >= $callback['start'] ) {
			// "cron_schedules" found in the second parameter, not the first.
			return;
		}

		// Detect callback function name.
		$callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true );

		// If callback is array, get second element.
		if ( false !== $callbackArrayPtr
			&& ( \T_ARRAY === $this->tokens[ $callbackArrayPtr ]['code']
				|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $callbackArrayPtr ]['code'] )
		) {
			$callback = $this->get_function_call_parameter( $callbackArrayPtr, 2 );

			if ( false === $callback ) {
				$this->confused( $stackPtr );
				return;
			}
		}

		unset( $functionPtr );

		// Search for the function in tokens.
		$callbackFunctionPtr = $this->phpcsFile->findNext( array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING, \T_CLOSURE ), $callback['start'], ( $callback['end'] + 1 ) );

		if ( false === $callbackFunctionPtr ) {
			$this->confused( $stackPtr );
			return;
		}

		if ( \T_CLOSURE === $this->tokens[ $callbackFunctionPtr ]['code'] ) {
			$functionPtr = $callbackFunctionPtr;
		} else {
			$functionName = $this->strip_quotes( $this->tokens[ $callbackFunctionPtr ]['content'] );

			for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
				if ( \T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
					$foundName = $this->phpcsFile->getDeclarationName( $ptr );
					if ( $foundName === $functionName ) {
						$functionPtr = $ptr;
						break;
					} elseif ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
						// Skip to the end of the function definition.
						$ptr = $this->tokens[ $ptr ]['scope_closer'];
					}
				}
			}
		}

		if ( ! isset( $functionPtr ) ) {
			$this->confused( $stackPtr );
			return;
		}

		if ( ! isset( $this->tokens[ $functionPtr ]['scope_opener'], $this->tokens[ $functionPtr ]['scope_closer'] ) ) {
			return;
		}

		$opening = $this->tokens[ $functionPtr ]['scope_opener'];
		$closing = $this->tokens[ $functionPtr ]['scope_closer'];
		for ( $i = $opening; $i <= $closing; $i++ ) {

			if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) {
				if ( 'interval' === $this->strip_quotes( $this->tokens[ $i ]['content'] ) ) {
					$operator = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $i, null, false, null, true );
					if ( false === $operator ) {
						$this->confused( $stackPtr );
						return;
					}

					$valueStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true, null, true );
					$valueEnd   = $this->phpcsFile->findNext( array( \T_COMMA, \T_CLOSE_PARENTHESIS ), ( $valueStart + 1 ) );
					$value      = '';
					for ( $j = $valueStart; $j < $valueEnd; $j++ ) {
						if ( isset( Tokens::$emptyTokens[ $this->tokens[ $j ]['code'] ] ) ) {
							continue;
						}
						$value .= $this->tokens[ $j ]['content'];
					}

					if ( is_numeric( $value ) ) {
						$interval = $value;
						break;
					}

					// Deal correctly with WP time constants.
					$value = str_replace( array_keys( $this->wp_time_constants ), array_values( $this->wp_time_constants ), $value );

					// If all digits and operators, eval!
					if ( preg_match( '#^[\s\d+*/-]+$#', $value ) > 0 ) {
						$interval = eval( "return ( $value );" ); // phpcs:ignore Squiz.PHP.Eval -- No harm here.
						break;
					}

					$this->confused( $stackPtr );
					return;
				}
			}
		}

		$this->min_interval = (int) $this->min_interval;

		if ( isset( $interval ) && $interval < $this->min_interval ) {
			$minutes = round( ( $this->min_interval / 60 ), 1 );
			$this->phpcsFile->addWarning(
				'Scheduling crons at %s sec ( less than %s minutes ) is discouraged.',
				$stackPtr,
				'CronSchedulesInterval',
				array(
					$interval,
					$minutes,
				)
			);
			return;
		}
	}

	/**
	 * Add warning about unclear cron schedule change.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 */
	public function confused( $stackPtr ) {
		$this->phpcsFile->addWarning(
			'Detected changing of cron_schedules, but could not detect the interval value.',
			$stackPtr,
			'ChangeDetected'
		);
	}

}