diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..3b5a67dd --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,12 @@ +parameters: + level: 9 + paths: + - src + - profile-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php + + treatPhpDocTypesAsCertain: false diff --git a/src/Command.php b/src/Command.php index 8727938e..b27e082a 100644 --- a/src/Command.php +++ b/src/Command.php @@ -129,14 +129,22 @@ class Command { * * @skipglobalargcheck * @when before_wp_load + * + * @param array $args + * @param array $assoc_args + * @return void */ public function stage( $args, $assoc_args ) { global $wpdb; - $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $focus = Utils\get_flag_value( $typed_assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; $valid_stages = array( 'bootstrap', 'main_query', 'template' ); if ( $focus && ( true !== $focus && ! in_array( $focus, $valid_stages, true ) ) ) { @@ -179,9 +187,10 @@ public function stage( $args, $assoc_args ) { ); } $fields = array_merge( $base, $metrics ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $loggers = $profiler->get_loggers(); - if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) { + /** @var array $typed_assoc_args */ + if ( Utils\get_flag_value( $typed_assoc_args, 'spotlight' ) ) { $loggers = self::shine_spotlight( $loggers, $metrics ); } @@ -257,13 +266,21 @@ public function stage( $args, $assoc_args ) { * * @skipglobalargcheck * @when before_wp_load + * + * @param array $args + * @param array $assoc_args + * @return void */ public function hook( $args, $assoc_args ) { - $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $focus = Utils\get_flag_value( $typed_assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; $profiler = new Profiler( 'hook', $focus ); $profiler->run(); @@ -291,13 +308,16 @@ public function hook( $args, $assoc_args ) { 'request_count', ); $fields = array_merge( $base, $metrics ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $loggers = $profiler->get_loggers(); - if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) { + /** @var array $typed_assoc_args */ + if ( Utils\get_flag_value( $typed_assoc_args, 'spotlight' ) ) { $loggers = self::shine_spotlight( $loggers, $metrics ); } - $search = Utils\get_flag_value( $assoc_args, 'search', false ); - if ( false !== $search && '' !== $search ) { + /** @var array $typed_assoc_args */ + $search_val = Utils\get_flag_value( $typed_assoc_args, 'search', '' ); + $search = is_string( $search_val ) ? $search_val : ''; + if ( '' !== $search ) { if ( ! $focus ) { WP_CLI::error( '--search requires --all or a specific hook.' ); } @@ -357,16 +377,24 @@ public function hook( $args, $assoc_args ) { * | 0.1009s | 100% | 1 | * +---------+-------------+---------------+ * + * @param array $args + * @param array $assoc_args + * @return void + * * @subcommand eval */ public function eval_( $args, $assoc_args ) { $statement = $args[0]; - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; self::profile_eval_ish( - $assoc_args, + $typed_assoc_args, function () use ( $statement ) { eval( $statement ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- no other way around here }, @@ -426,21 +454,30 @@ function () use ( $statement ) { * | 0.1009s | 100% | 1 | * +---------+-------------+---------------+ * + * @param array $args + * @param array $assoc_args + * @return void + * * @subcommand eval-file */ public function eval_file( $args, $assoc_args ) { $file = $args[0]; - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + /** @var array $typed_assoc_args */ + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; if ( ! file_exists( $file ) ) { WP_CLI::error( "'$file' does not exist." ); } self::profile_eval_ish( - $assoc_args, + $typed_assoc_args, function () use ( $file ) { self::include_file( $file ); }, @@ -451,12 +488,20 @@ function () use ( $file ) { /** * Profile an eval or eval-file statement. + * + * @param array $assoc_args + * @param callable $profile_callback + * @param string $order + * @param string|null $orderby + * @return void */ private static function profile_eval_ish( $assoc_args, $profile_callback, $order = 'ASC', $orderby = null ) { - $hook = Utils\get_flag_value( $assoc_args, 'hook' ); - $focus = false; - $type = false; - $fields = array(); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $hook = Utils\get_flag_value( $typed_assoc_args, 'hook' ); + $focus = false; + $type = false; + $fields = array(); if ( $hook ) { $type = 'hook'; if ( true !== $hook ) { @@ -492,7 +537,7 @@ private static function profile_eval_ish( $assoc_args, $profile_callback, $order 'request_count', ) ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $formatter->display_items( $loggers, false, $order, $orderby ); } @@ -500,6 +545,7 @@ private static function profile_eval_ish( $assoc_args, $profile_callback, $order * Include a file without exposing it to current scope * * @param string $file + * @return void */ private static function include_file( $file ) { include $file; @@ -508,9 +554,9 @@ private static function include_file( $file ) { /** * Filter loggers with zero-ish values. * - * @param array $loggers - * @param array $metrics - * @return array + * @param array<\WP_CLI\Profile\Logger> $loggers + * @param array $metrics + * @return array<\WP_CLI\Profile\Logger> */ private static function shine_spotlight( $loggers, $metrics ) { @@ -550,9 +596,9 @@ private static function shine_spotlight( $loggers, $metrics ) { /** * Filter loggers to only those whose callback name matches a pattern. * - * @param array $loggers - * @param string $pattern - * @return array + * @param array<\WP_CLI\Profile\Logger> $loggers + * @param string $pattern + * @return array<\WP_CLI\Profile\Logger> */ private static function filter_by_callback( $loggers, $pattern ) { return array_filter( @@ -562,4 +608,20 @@ function ( $logger ) use ( $pattern ) { } ); } + + /** + * Get typed assoc args for get_flag_value. + * + * @param array $assoc_args + * @return array + */ + private static function get_typed_assoc_args( $assoc_args ) { + $typed = array(); + foreach ( $assoc_args as $k => $v ) { + if ( is_bool( $v ) || is_string( $v ) ) { + $typed[ $k ] = $v; + } + } + return $typed; + } } diff --git a/src/Formatter.php b/src/Formatter.php index 775db154..7c290c74 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -4,13 +4,32 @@ class Formatter { + /** + * @var \WP_CLI\Formatter + */ private $formatter; + /** + * @var array + */ private $args; + /** + * @var int|null + */ private $total_cell_index; + /** + * Formatter constructor. + * + * @param array $assoc_args + * @param array|null $fields + * @param string|false $prefix + */ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { + if ( null === $fields ) { + $fields = []; + } $format_args = array( 'format' => 'table', 'fields' => $fields, @@ -24,10 +43,19 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { } if ( ! is_array( $format_args['fields'] ) ) { - $format_args['fields'] = explode( ',', $format_args['fields'] ); + $fields_val = $format_args['fields']; + $fields_str = is_scalar( $fields_val ) ? (string) $fields_val : ''; + $format_args['fields'] = explode( ',', $fields_str ); } - $format_args['fields'] = array_filter( array_map( 'trim', $format_args['fields'] ) ); + $format_args['fields'] = array_filter( + array_map( + function ( $val ) { + return trim( is_scalar( $val ) ? (string) $val : '' ); + }, + $format_args['fields'] + ) + ); if ( isset( $assoc_args['fields'] ) ) { if ( empty( $format_args['fields'] ) ) { @@ -39,9 +67,9 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { } } - if ( 'time' !== $fields[0] ) { + if ( ! empty( $fields ) && 'time' !== $fields[0] ) { $index = array_search( $fields[0], $format_args['fields'], true ); - $this->total_cell_index = ( false !== $index ) ? $index : null; + $this->total_cell_index = ( false !== $index ) ? (int) $index : null; } $this->args = $format_args; @@ -51,11 +79,17 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { /** * Display multiple items according to the output arguments. * - * @param array $items + * @param array<\WP_CLI\Profile\Logger> $items + * @param bool $include_total + * @param string $order + * @param string|null $orderby + * @return void */ public function display_items( $items, $include_total, $order, $orderby ) { if ( 'table' === $this->args['format'] && empty( $this->args['field'] ) ) { - $this->show_table( $order, $orderby, $items, $this->args['fields'], $include_total ); + /** @var array $fields */ + $fields = $this->args['fields']; + $this->show_table( $order, $orderby, $items, $fields, $include_total ); } else { $this->formatter->display_items( $items ); } @@ -64,13 +98,14 @@ public function display_items( $items, $include_total, $order, $orderby ) { /** * Function to compare floats. * - * @param double $a Floating number. - * @param double $b Floating number. + * @param float $a Floating number. + * @param float $b Floating number. + * @return int */ private function compare_float( $a, $b ) { - $a = number_format( $a, 4 ); - $b = number_format( $b, 4 ); - if ( 0 === $a - $b ) { + $a = round( $a, 4 ); + $b = round( $b, 4 ); + if ( 0.0 === $a - $b ) { return 0; } elseif ( $a - $b < 0 ) { return -1; @@ -82,8 +117,12 @@ private function compare_float( $a, $b ) { /** * Show items in a \cli\Table. * - * @param array $items - * @param array $fields + * @param string $order + * @param string|null $orderby + * @param array<\WP_CLI\Profile\Logger> $items + * @param array $fields + * @param bool $include_total + * @return void */ private function show_table( $order, $orderby, $items, $fields, $include_total ) { $table = new \cli\Table(); @@ -109,7 +148,7 @@ function ( $a, $b ) use ( $order, $orderby ) { list( $first, $second ) = $orderby_array; if ( is_numeric( $first->$orderby ) && is_numeric( $second->$orderby ) ) { - return $this->compare_float( $first->$orderby, $second->$orderby ); + return $this->compare_float( (float) $first->$orderby, (float) $second->$orderby ); } return strcmp( $first->$orderby, $second->$orderby ); @@ -139,13 +178,17 @@ function ( $a, $b ) use ( $order, $orderby ) { } if ( stripos( $fields[ $i ], '_ratio' ) ) { if ( ! is_null( $value ) ) { + assert( is_array( $totals[ $i ] ) ); $totals[ $i ][] = $value; } } else { - $totals[ $i ] += $value; + $current_total = is_numeric( $totals[ $i ] ) ? $totals[ $i ] : 0; + $add_value = is_numeric( $value ) ? $value : 0; + $totals[ $i ] = $current_total + $add_value; } if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) { - $values[ $i ] = round( $value, 4 ) . 's'; + $value_num = is_numeric( $value ) ? (float) $value : 0.0; + $values[ $i ] = round( $value_num, 4 ) . 's'; } } $table->addRow( $values ); @@ -156,11 +199,18 @@ function ( $a, $b ) use ( $order, $orderby ) { continue; } if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) { - $totals[ $i ] = round( $value, 4 ) . 's'; + assert( is_numeric( $value ) ); + $totals[ $i ] = round( (float) $value, 4 ) . 's'; } if ( is_array( $value ) ) { if ( ! empty( $value ) ) { - $totals[ $i ] = round( ( array_sum( array_map( 'floatval', $value ) ) / count( $value ) ), 2 ) . '%'; + $float_values = array_map( + function ( $val ) { + return floatval( is_scalar( $val ) ? $val : 0 ); + }, + $value + ); + $totals[ $i ] = round( ( array_sum( $float_values ) / count( $value ) ), 2 ) . '%'; } else { $totals[ $i ] = null; } diff --git a/src/Logger.php b/src/Logger.php index 0c3ab116..f6e53ecb 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -2,36 +2,72 @@ namespace WP_CLI\Profile; +/** + * Logger class. + * + * @property string $callback + * @property string $location + */ class Logger { - public $time = 0; - public $query_count = 0; - public $query_time = 0; - public $cache_hits = 0; - public $cache_misses = 0; - public $cache_ratio = null; - public $hook_count = 0; - public $hook_time = 0; - public $request_count = 0; - public $request_time = 0; - private $start_time = null; - private $query_offset = null; - private $cache_hit_offset = null; - private $cache_miss_offset = null; - private $hook_start_time = null; - private $hook_depth = 0; + /** @var float */ + public $time = 0; + /** @var int */ + public $query_count = 0; + /** @var float */ + public $query_time = 0; + /** @var int */ + public $cache_hits = 0; + /** @var int */ + public $cache_misses = 0; + /** @var string|null */ + public $cache_ratio = null; + /** @var int */ + public $hook_count = 0; + /** @var float */ + public $hook_time = 0; + /** @var int */ + public $request_count = 0; + /** @var float */ + public $request_time = 0; + /** @var float|null */ + private $start_time = null; + /** @var int|null */ + private $query_offset = null; + /** @var int|null */ + private $cache_hit_offset = null; + /** @var int|null */ + private $cache_miss_offset = null; + /** @var float|null */ + private $hook_start_time = null; + /** @var int */ + private $hook_depth = 0; + /** @var float|null */ private $request_start_time = null; + /** @var array */ private $definitions = array(); + /** @var array<\WP_CLI\Profile\Logger> */ public static $active_loggers = array(); + /** + * Logger constructor. + * + * @param array $definition + */ public function __construct( $definition = array() ) { foreach ( $definition as $k => $v ) { $this->definitions[ $k ] = $v; } } + /** + * Magic getter for definitions. + * + * @param string $key + * @return mixed + */ public function __get( $key ) { if ( isset( $this->definitions[ $key ] ) ) { return $this->definitions[ $key ]; @@ -40,16 +76,31 @@ public function __get( $key ) { return null; } + /** + * Magic setter for definitions. + * + * @param string $key + * @param mixed $value + * @return void + */ public function __set( $key, $value ) { $this->definitions[ $key ] = $value; } + /** + * Magic isset for definitions. + * + * @param string $key + * @return bool + */ public function __isset( $key ) { return isset( $this->definitions[ $key ] ); } /** * Start this logger + * + * @return void */ public function start() { global $wpdb, $wp_object_cache; @@ -66,6 +117,8 @@ public function start() { /** * Whether or not the logger is running + * + * @return bool */ public function running() { return ! is_null( $this->start_time ); @@ -73,6 +126,8 @@ public function running() { /** * Stop this logger + * + * @return void */ public function stop() { global $wpdb, $wp_object_cache; @@ -115,6 +170,8 @@ public function stop() { /** * Start this logger's hook timer + * + * @return void */ public function start_hook_timer() { ++$this->hook_count; @@ -128,6 +185,8 @@ public function start_hook_timer() { /** * Stop this logger's hook timer + * + * @return void */ public function stop_hook_timer() { if ( $this->hook_depth ) { @@ -142,6 +201,8 @@ public function stop_hook_timer() { /** * Start this logger's request timer + * + * @return void */ public function start_request_timer() { ++$this->request_count; @@ -150,6 +211,8 @@ public function start_request_timer() { /** * Stop this logger's request timer + * + * @return void */ public function stop_request_timer() { if ( ! is_null( $this->request_start_time ) ) { diff --git a/src/Profiler.php b/src/Profiler.php index 2658441f..b224bcd9 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -6,9 +6,13 @@ class Profiler { + /** @var string|false */ private $type; + /** @var string|bool|null */ private $focus; - private $loggers = array(); + /** @var array> */ + private $loggers = array(); + /** @var array> */ private $stage_hooks = array( 'bootstrap' => array( 'muplugins_loaded', @@ -35,26 +39,49 @@ class Profiler { ), ); - private $current_stage_hooks = array(); - private $running_hook = null; - private $previous_filter = null; + /** @var array */ + private $current_stage_hooks = array(); + /** @var string|null */ + private $running_hook = null; + /** @var string|null */ + private $previous_filter = null; + /** @var array|null */ private $previous_filter_callbacks = null; - private $filter_depth = 0; - - private $tick_callback = null; - private $tick_location = null; - private $tick_start_time = null; - private $tick_query_offset = null; - private $tick_cache_hit_offset = null; + /** @var int */ + private $filter_depth = 0; + + /** @var string|null */ + private $tick_callback = null; + /** @var string|null */ + private $tick_location = null; + /** @var float|null */ + private $tick_start_time = null; + /** @var int|null */ + private $tick_query_offset = null; + /** @var int|null */ + private $tick_cache_hit_offset = null; + /** @var int|null */ private $tick_cache_miss_offset = null; + /** @var bool */ private $is_admin_request = false; + /** + * Profiler constructor. + * + * @param string|false $type + * @param string|bool|null $focus + */ public function __construct( $type, $focus ) { $this->type = $type; $this->focus = $focus; } + /** + * Get the loggers. + * + * @return array<\WP_CLI\Profile\Logger> + */ public function get_loggers() { foreach ( $this->loggers as $i => $logger ) { if ( is_array( $logger ) ) { @@ -72,11 +99,15 @@ public function get_loggers() { $logger->location = self::get_short_location( $logger->location ); $this->loggers[ $i ] = $logger; } - return $this->loggers; + /** @var array<\WP_CLI\Profile\Logger> $loggers */ + $loggers = $this->loggers; + return $loggers; } /** * Run the profiler against WordPress + * + * @return void */ public function run() { $url = WP_CLI::get_runner()->config['url']; @@ -120,7 +151,7 @@ function () { ); if ( 'hook' === $this->type && - $this->focus && + is_string( $this->focus ) && ':before' === substr( $this->focus, -7, 7 ) ) { $stage_hooks = array(); @@ -129,7 +160,7 @@ function () { } $end_hook = substr( $this->focus, 0, -7 ); $key = array_search( $end_hook, $stage_hooks, true ); - if ( isset( $stage_hooks[ $key - 1 ] ) ) { + if ( is_int( $key ) && isset( $stage_hooks[ $key - 1 ] ) ) { $start_hook = $stage_hooks[ $key - 1 ]; WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 ); } else { @@ -138,7 +169,7 @@ function () { WP_CLI::add_wp_hook( $end_hook, array( $this, 'wp_tick_profile_end' ), -9999 ); } elseif ( 'hook' === $this->type && - $this->focus && + is_string( $this->focus ) && ':after' === substr( $this->focus, -6, 6 ) ) { $start_hook = substr( $this->focus, 0, -6 ); @@ -153,6 +184,9 @@ function () { /** * Start profiling function calls on the end of this filter + * + * @param mixed $value + * @return mixed */ public function wp_tick_profile_begin( $value = null ) { @@ -189,6 +223,9 @@ public function wp_tick_profile_begin( $value = null ) { /** * Stop profiling function calls at the beginning of this filter + * + * @param mixed $value + * @return mixed */ public function wp_tick_profile_end( $value = null ) { unregister_tick_function( array( $this, 'handle_function_tick' ) ); @@ -198,6 +235,8 @@ public function wp_tick_profile_end( $value = null ) { /** * Profiling verbosity at the beginning of every action and filter + * + * @return void */ public function wp_hook_begin() { @@ -206,17 +245,23 @@ public function wp_hook_begin() { } $current_filter = current_filter(); + if ( ! is_string( $current_filter ) ) { + return; + } if ( ( 'stage' === $this->type && in_array( $current_filter, $this->current_stage_hooks, true ) ) || ( 'hook' === $this->type && ! $this->focus ) ) { $pseudo_hook = "{$current_filter}:before"; if ( isset( $this->loggers[ $pseudo_hook ] ) ) { + assert( $this->loggers[ $pseudo_hook ] instanceof Logger ); $this->loggers[ $pseudo_hook ]->stop(); } $callback_count = 0; $callbacks = self::get_filter_callbacks( $current_filter ); if ( false !== $callbacks ) { foreach ( $callbacks as $priority => $cbs ) { - $callback_count += count( $cbs ); + if ( is_array( $cbs ) ) { + $callback_count += count( $cbs ); + } } } $this->loggers[ $current_filter ] = new Logger( @@ -229,7 +274,8 @@ public function wp_hook_begin() { } if ( 0 === $this->filter_depth - && ! is_null( $this->previous_filter_callbacks ) ) { + && ! is_null( $this->previous_filter_callbacks ) + && ! is_null( $this->previous_filter ) ) { self::set_filter_callbacks( $this->previous_filter, $this->previous_filter_callbacks ); $this->previous_filter_callbacks = null; } @@ -247,6 +293,9 @@ public function wp_hook_begin() { /** * Wrap current filter callbacks with a timer + * + * @param string $current_filter + * @return void */ private function wrap_current_filter_callbacks( $current_filter ) { @@ -258,23 +307,38 @@ private function wrap_current_filter_callbacks( $current_filter ) { $this->previous_filter_callbacks = $callbacks; foreach ( $callbacks as $priority => $priority_callbacks ) { - foreach ( $priority_callbacks as $i => $the_ ) { - $callbacks[ $priority ][ $i ] = array( - 'function' => function () use ( $the_, $i ) { - if ( ! isset( $this->loggers[ $i ] ) ) { - $this->loggers[ $i ] = new Logger( - array( - 'callback' => $the_['function'], - ) - ); - } - $this->loggers[ $i ]->start(); - $value = call_user_func_array( $the_['function'], func_get_args() ); - $this->loggers[ $i ]->stop(); - return $value; - }, - 'accepted_args' => $the_['accepted_args'], - ); + if ( is_array( $priority_callbacks ) ) { + $new_priority_callbacks = $priority_callbacks; + foreach ( $priority_callbacks as $i => $the_ ) { + if ( is_array( $the_ ) && isset( $the_['function'] ) && isset( $the_['accepted_args'] ) ) { + $func = $the_['function']; + $new_priority_callbacks[ $i ] = array( + 'function' => function () use ( $func, $i ) { + if ( ! isset( $this->loggers[ $i ] ) ) { + $this->loggers[ $i ] = new Logger( + array( + 'callback' => $func, + ) + ); + } + assert( $this->loggers[ $i ] instanceof Logger ); + $this->loggers[ $i ]->start(); + + $args = func_get_args(); + if ( is_callable( $func ) ) { + $value = call_user_func_array( $func, $args ); + } else { + $value = null; + } + + $this->loggers[ $i ]->stop(); + return $value; + }, + 'accepted_args' => $the_['accepted_args'], + ); + } + } + $callbacks[ $priority ] = $new_priority_callbacks; } } self::set_filter_callbacks( $current_filter, $callbacks ); @@ -282,6 +346,9 @@ private function wrap_current_filter_callbacks( $current_filter ) { /** * Profiling verbosity at the end of every action and filter + * + * @param mixed $filter_value + * @return mixed */ public function wp_hook_end( $filter_value = null ) { @@ -292,10 +359,11 @@ public function wp_hook_end( $filter_value = null ) { $current_filter = current_filter(); if ( ( 'stage' === $this->type && in_array( $current_filter, $this->current_stage_hooks, true ) ) || ( 'hook' === $this->type && ! $this->focus ) ) { + assert( $this->loggers[ $current_filter ] instanceof Logger ); $this->loggers[ $current_filter ]->stop(); if ( 'stage' === $this->type ) { $key = array_search( $current_filter, $this->current_stage_hooks, true ); - if ( false !== $key && isset( $this->current_stage_hooks[ $key + 1 ] ) ) { + if ( is_int( $key ) && isset( $this->current_stage_hooks[ $key + 1 ] ) ) { $pseudo_hook = "{$this->current_stage_hooks[$key+1]}:before"; } else { $pseudo_hook = "{$this->current_stage_hooks[$key]}:after"; @@ -313,6 +381,8 @@ public function wp_hook_end( $filter_value = null ) { /** * Handle the tick of a function + * + * @return void */ public function handle_function_tick() { global $wpdb, $wp_object_cache; @@ -334,26 +404,41 @@ public function handle_function_tick() { ); } - $this->loggers[ $callback_hash ]['time'] += $time; - - if ( isset( $wpdb ) ) { - $total_queries = count( $wpdb->queries ); - for ( $i = $this->tick_query_offset; $i < $total_queries; $i++ ) { - $this->loggers[ $callback_hash ]['query_time'] += $wpdb->queries[ $i ][1]; - ++$this->loggers[ $callback_hash ]['query_count']; + $logger_data = $this->loggers[ $callback_hash ]; + if ( is_array( $logger_data ) ) { + $current_time = isset( $logger_data['time'] ) && is_numeric( $logger_data['time'] ) ? $logger_data['time'] : 0.0; + $logger_data['time'] = (float) $current_time + $time; + + if ( isset( $wpdb ) ) { + $total_queries = count( $wpdb->queries ); + for ( $i = $this->tick_query_offset; $i < $total_queries; $i++ ) { + $q_time = isset( $wpdb->queries[ $i ][1] ) ? $wpdb->queries[ $i ][1] : 0.0; + $current_q_time = isset( $logger_data['query_time'] ) && is_numeric( $logger_data['query_time'] ) ? $logger_data['query_time'] : 0.0; + $q_time_val = is_numeric( $q_time ) ? $q_time : 0.0; + $logger_data['query_time'] = (float) $current_q_time + (float) $q_time_val; + + $current_q_count = isset( $logger_data['query_count'] ) && is_numeric( $logger_data['query_count'] ) ? $logger_data['query_count'] : 0; + $logger_data['query_count'] = (int) $current_q_count + 1; + } } - } - if ( isset( $wp_object_cache ) ) { - $hits = ! empty( $wp_object_cache->cache_hits ) ? $wp_object_cache->cache_hits : 0; - $misses = ! empty( $wp_object_cache->cache_misses ) ? $wp_object_cache->cache_misses : 0; - $this->loggers[ $callback_hash ]['cache_hits'] = ( $hits - $this->tick_cache_hit_offset ) + $this->loggers[ $callback_hash ]['cache_hits']; - $this->loggers[ $callback_hash ]['cache_misses'] = ( $misses - $this->tick_cache_miss_offset ) + $this->loggers[ $callback_hash ]['cache_misses']; - $total = $this->loggers[ $callback_hash ]['cache_hits'] + $this->loggers[ $callback_hash ]['cache_misses']; - if ( $total ) { - $ratio = ( $this->loggers[ $callback_hash ]['cache_hits'] / $total ) * 100; - $this->loggers[ $callback_hash ]['cache_ratio'] = round( $ratio, 2 ) . '%'; + if ( isset( $wp_object_cache ) ) { + $hits = ! empty( $wp_object_cache->cache_hits ) ? $wp_object_cache->cache_hits : 0; + $misses = ! empty( $wp_object_cache->cache_misses ) ? $wp_object_cache->cache_misses : 0; + + $current_hits = isset( $logger_data['cache_hits'] ) && is_numeric( $logger_data['cache_hits'] ) ? $logger_data['cache_hits'] : 0; + $logger_data['cache_hits'] = ( $hits - $this->tick_cache_hit_offset ) + (int) $current_hits; + + $current_misses = isset( $logger_data['cache_misses'] ) && is_numeric( $logger_data['cache_misses'] ) ? $logger_data['cache_misses'] : 0; + $logger_data['cache_misses'] = ( $misses - $this->tick_cache_miss_offset ) + (int) $current_misses; + + $total = $logger_data['cache_hits'] + $logger_data['cache_misses']; + if ( $total ) { + $ratio = ( $logger_data['cache_hits'] / $total ) * 100; + $logger_data['cache_ratio'] = round( $ratio, 2 ) . '%'; + } } + $this->loggers[ $callback_hash ] = $logger_data; } } @@ -366,7 +451,10 @@ public function handle_function_tick() { $location = ''; $callback = ''; if ( in_array( strtolower( $frame['function'] ), array( 'include', 'require', 'include_once', 'require_once' ), true ) ) { - $callback = $frame['function'] . " '" . $frame['args'][0] . "'"; + $callback = $frame['function']; + if ( isset( $frame['args'] ) && is_array( $frame['args'] ) && isset( $frame['args'][0] ) && is_scalar( $frame['args'][0] ) ) { + $callback .= " '" . (string) $frame['args'][0] . "'"; + } } elseif ( isset( $frame['object'] ) && method_exists( $frame['object'], $frame['function'] ) ) { $callback = get_class( $frame['object'] ) . '->' . $frame['function'] . '()'; } elseif ( isset( $frame['class'] ) && method_exists( $frame['class'], $frame['function'] ) ) { @@ -399,6 +487,9 @@ public function handle_function_tick() { /** * Profiling request time for any active Loggers + * + * @param mixed $filter_value + * @return mixed */ public function wp_request_begin( $filter_value = null ) { foreach ( Logger::$active_loggers as $logger ) { @@ -409,6 +500,9 @@ public function wp_request_begin( $filter_value = null ) { /** * Profiling request time for any active Loggers + * + * @param mixed $filter_value + * @return mixed */ public function wp_request_end( $filter_value = null ) { foreach ( Logger::$active_loggers as $logger ) { @@ -419,6 +513,8 @@ public function wp_request_end( $filter_value = null ) { /** * Runs through the entirety of the WP bootstrap process + * + * @return void */ private function load_wordpress_with_template() { @@ -439,21 +535,22 @@ private function load_wordpress_with_template() { if ( 'bootstrap' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['bootstrap'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'bootstrap' ) ); - $logger->start(); + $bootstrap_logger = new Logger( array( 'stage' => 'bootstrap' ) ); + $bootstrap_logger->start(); } } WP_CLI::get_runner()->load_wordpress(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } if ( 'hook' === $this->type && 'wp_loaded:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $bootstrap_logger ) ) { + $bootstrap_logger->stop(); + $this->loggers[] = $bootstrap_logger; } // Skip main_query and template stages for admin requests. @@ -466,21 +563,22 @@ private function load_wordpress_with_template() { if ( 'main_query' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['main_query'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'main_query' ) ); - $logger->start(); + $main_query_logger = new Logger( array( 'stage' => 'main_query' ) ); + $main_query_logger->start(); } } wp(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } if ( 'hook' === $this->type && 'wp:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $main_query_logger ) ) { + $main_query_logger->stop(); + $this->loggers[] = $main_query_logger; } define( 'WP_USE_THEMES', true ); @@ -496,28 +594,32 @@ private function load_wordpress_with_template() { if ( 'template' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['template'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'template' ) ); - $logger->start(); + $template_logger = new Logger( array( 'stage' => 'template' ) ); + $template_logger->start(); } } ob_start(); require_once ABSPATH . WPINC . '/template-loader.php'; ob_get_clean(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } if ( 'hook' === $this->type && 'wp_footer:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $template_logger ) ) { + $template_logger->stop(); + $this->loggers[] = $template_logger; } } /** * Get a human-readable name from a callback + * + * @param mixed $callback + * @return array{0: string, 1: string} */ private static function get_name_location_from_callback( $callback ) { $location = ''; @@ -526,9 +628,11 @@ private static function get_name_location_from_callback( $callback ) { if ( is_array( $callback ) && is_object( $callback[0] ) ) { $reflection = new \ReflectionMethod( $callback[0], $callback[1] ); $name = get_class( $callback[0] ) . '->' . $callback[1] . '()'; - } elseif ( is_array( $callback ) && method_exists( $callback[0], $callback[1] ) ) { + } elseif ( is_array( $callback ) && isset( $callback[0] ) && isset( $callback[1] ) && ( is_object( $callback[0] ) || is_string( $callback[0] ) ) && is_string( $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) { $reflection = new \ReflectionMethod( $callback[0], $callback[1] ); - $name = $callback[0] . '::' . $callback[1] . '()'; + /** @var string $class_name */ + $class_name = $callback[0]; + $name = $class_name . '::' . $callback[1] . '()'; } elseif ( is_object( $callback ) && is_a( $callback, 'Closure' ) ) { $reflection = new \ReflectionFunction( $callback ); $name = 'function(){}'; @@ -549,7 +653,8 @@ private static function get_name_location_from_callback( $callback ) { * @return string */ private static function get_short_location( $location ) { - $abspath = rtrim( realpath( ABSPATH ), '/' ) . '/'; + $real_abspath = realpath( ABSPATH ); + $abspath = rtrim( false !== $real_abspath ? $real_abspath : ABSPATH, '/' ) . '/'; if ( defined( 'WP_PLUGIN_DIR' ) && 0 === stripos( $location, WP_PLUGIN_DIR ) ) { $location = str_replace( trailingslashit( WP_PLUGIN_DIR ), '', $location ); } elseif ( defined( 'WPMU_PLUGIN_DIR' ) && 0 === stripos( $location, WPMU_PLUGIN_DIR ) ) { @@ -566,6 +671,9 @@ private static function get_short_location( $location ) { /** * Set the hooks for the current stage + * + * @param array $hooks + * @return void */ private function set_stage_hooks( $hooks ) { $this->current_stage_hooks = $hooks; @@ -577,8 +685,8 @@ private function set_stage_hooks( $hooks ) { /** * Get the callbacks for a given filter * - * @param string - * @return array|false + * @param string $filter + * @return array|false */ private static function get_filter_callbacks( $filter ) { global $wp_filter; @@ -602,7 +710,8 @@ private static function get_filter_callbacks( $filter ) { * Set the callbacks for a given filter * * @param string $filter - * @param mixed $callbacks + * @param mixed $callbacks + * @return void */ private static function set_filter_callbacks( $filter, $callbacks ) { global $wp_filter; @@ -612,6 +721,7 @@ private static function set_filter_callbacks( $filter, $callbacks ) { } if ( is_a( $wp_filter[ $filter ], 'WP_Hook' ) ) { + /** @var array $callbacks */ $wp_filter[ $filter ]->callbacks = $callbacks; } else { $wp_filter[ $filter ] = $callbacks; // phpcs:ignore diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 00000000..81f3d91b --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,6 @@ +