diff --git a/.phan/config.php b/.phan/config.php index 55705fd..3f7f75b 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -192,7 +192,6 @@ // A list of files to include in analysis 'file_list' => [ - // 'vendor/phpunit/phpunit/src/Framework/TestCase.php', ], // A file list that defines files that will be excluded @@ -216,7 +215,6 @@ // your application should be included in this list. 'directory_list' => [ 'src', - 'vendor', ], // List of case-insensitive file extensions supported by Phan. @@ -235,9 +233,52 @@ // should be added to the `directory_list` as // to `exclude_analysis_directory_list`. "exclude_analysis_directory_list" => [ - 'vendor', ], // A list of plugin files to execute - 'plugins' => [ ], + 'plugins' => [ + 'AlwaysReturnPlugin', + 'DollarDollarPlugin', + 'UnreachableCodePlugin', + 'DuplicateArrayKeyPlugin', + 'PregRegexCheckerPlugin', + 'PrintfCheckerPlugin', + 'UseReturnValuePlugin', + + // UnknownElementTypePlugin warns about unknown types in element signatures. + 'UnknownElementTypePlugin', + 'DuplicateExpressionPlugin', + // warns about carriage returns("\r"), trailing whitespace, and tabs in PHP files. + 'WhitespacePlugin', + // Warn about inline HTML anywhere in the files. + 'InlineHTMLPlugin', + //////////////////////////////////////////////////////////////////////// + // Plugins for Phan's self-analysis + //////////////////////////////////////////////////////////////////////// + + 'NoAssertPlugin', + 'PossiblyStaticMethodPlugin', + + // 'HasPHPDocPlugin', + 'PHPDocToRealTypesPlugin', // suggests replacing (at)return void with `: void` in the declaration, etc. + 'PHPDocRedundantPlugin', + 'PreferNamespaceUsePlugin', + 'EmptyStatementListPlugin', + + // Report empty (not overridden or overriding) methods and functions + // 'EmptyMethodAndFunctionPlugin', + + // This should only be enabled if the code being analyzed contains Phan plugins. + 'PhanSelfCheckPlugin', + // Warn about using the same loop variable name as a loop variable of an outer loop. + 'LoopVariableReusePlugin', + // Warn about assigning the value the variable already had to that variable. + 'RedundantAssignmentPlugin', + // These are specific to Phan's coding style + 'StrictComparisonPlugin', + // Warn about `$var == SOME_INT_OR_STRING_CONST` due to unintuitive behavior such as `0 == 'a'` + 'StrictLiteralComparisonPlugin', + 'ShortArrayPlugin', + 'SimplifyExpressionPlugin', + ], ]; diff --git a/.travis.yml b/.travis.yml index 9f5aaa1..ab533a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: php php: - - 7.1 - - 7.0 + - 7.4 + - 7.3 + - 7.2 before_script: - ci/install_runkit_for_php_version.sh - composer install script: - - php vendor/bin/phpunit tests + - vendor/bin/phan + - php -d opcache.enable=0 vendor/bin/phpunit tests diff --git a/README.md b/README.md index 16136da..0e17169 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,17 @@ This requires [runkit7 (fork of the runkit PECL)](https://github.com/runkit7/run Logging for parameters, return types, backtraces, and exceptions is enabled by default. Code using this utility can disable or replace the default implementation to log those details. +**NOTE: When using this in recent php versions, disable opcache or opcache optimizations before running php.** + ## Authors Tyson Andre ## Requirements -- PHP version 7.0 or greater -- [runkit7 (fork of runkit)](https://github.com/runkit7/runkit7) must be installed and enabled. -- runkit must be enabled in your php.ini settings (`extension=runkit.so`) +- PHP version 7.2 or greater +- [runkit7](https://github.com/runkit7/runkit7) 3.1.0a1 or greater must be installed and enabled. +- runkit7 must be enabled in your php.ini settings (`extension=runkit7.so`) ## License @@ -107,7 +109,7 @@ To attempt this, `runkit.internal_override=On` must be temporarily added to php. ----- -README.md: Copyright 2017 Ifwe Inc. +README.md: Copyright 2020 Ifwe Inc. README.md is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. diff --git a/composer.json b/composer.json index 9e47df3..e9d9b0c 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,12 @@ "keywords": ["mocks", "debugging", "traceon", "runkit", "runkit7"], "type": "library", "require": { - "php": ">= 7.0", - "ext-runkit": ">=1.0.5a5" + "php": ">= 7.2", + "ext-runkit7": ">=3.1.0a1" }, "require-dev": { - "phpunit/phpunit": "~6.2" + "phpunit/phpunit": "^7.0", + "phan/phan": "^3.1.1" }, "autoload": { "psr-4": {"TraceOn\\": "src/TraceOn"} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a093c5b..ad2715e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,7 +10,6 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" forceCoversAnnotation="false" - mapTestClassNameToCoveredClassName="false" processIsolation="false" stopOnError="false" stopOnFailure="false" diff --git a/src/TraceOn/TraceOn.php b/src/TraceOn/TraceOn.php index cba1a48..3387e43 100644 --- a/src/TraceOn/TraceOn.php +++ b/src/TraceOn/TraceOn.php @@ -2,14 +2,18 @@ namespace TraceOn; +use InvalidArgumentException; +use RuntimeException; +use Throwable; + /** * This class is recommended only for debugging issues involving deeply nested calls, and not for use during normal operations. * - * It uses runkit to print out function arguments, stack traces, and return values, as well as exceptions. - * Calls to this can be added to an entry point, e.g. to figure out if a deeply nested function is being called, + * This is a class which uses runkit7 to print out function calls, stack traces, and return values. + * This can be used from the test box, e.g. to figure out if a deeply nested function is being called, * what a function is being called with, what it is returning, etc. * - * This library is compatible with php 7.0 and 7.1. + * This library is compatible with php 7.1-7.4. * * This library works with static and instance methods. It has a similar interface to \SimpleStaticMock\SimpleStaticMock. * TODO: allow operating on functions by passing in null as a class name. @@ -42,7 +46,7 @@ * * ---------------------------------------------------------------------- * - * Copyright 2017 Ifwe Inc. + * Copyright 2020 Ifwe Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,44 +99,44 @@ class TraceOn { /** This can be passed in to disable a specific logging type. False can be passed in, to change loggers to noop. */ const NOOP = '\TraceOn\TraceOn::noop'; - /** @var bool */ + /** @var bool is the function/method instrumented */ private $_traced = true; /** @var ?string - If null, this is a mock of a function instead of a method. */ private $_className; - /** @var string */ + /** @var string the name of the method/function where the original implementation was moved */ private $_methodName; - /** @var string */ + /** @var string method/function name that was mocked */ private $_originalMethodName; - /** @var TraceOn[] - Maps full name to path */ + /** @var array maps method name to the only instance for this method. */ private static $_registry = []; /** * Start logging backtraces, parameters, and return values of the instance method or static method $className::$methodName, whenever it is called. - * @param string $className class name with method to mock + * @param ?string $className class name with method to mock (null to mock global functions) * @param string $methodName method name to mock - * @param array $options + * @param array $options */ - public function __construct($className, $methodName, array $options = []) { + public function __construct(?string $className, string $methodName, array $options = []) { if ($className === null) { $methodName = ltrim($methodName, "\\"); } $key = self::get_key($className, $methodName); if (isset(self::$_registry[$key])) { - throw new \RuntimeException("Logger for $key already exists"); + throw new RuntimeException("Logger for $key already exists"); } - if (!extension_loaded('runkit')) { - throw new \RuntimeException("Runkit is not installed"); + if (!extension_loaded('runkit7')) { + throw new RuntimeException("Runkit7 is not installed"); } if ($className !== null) { if (!class_exists($className)) { - throw new \RuntimeException("Failed to load class '$className'"); + throw new RuntimeException("Failed to load class '$className'"); } } else if (!function_exists($methodName)) { - throw new \RuntimeException("Failed to load function '$methodName'"); + throw new RuntimeException("Failed to load function '$methodName'"); } - $runkitFlags = self::compute_runkit_flags($className, $methodName); - $methodSeparator = ($runkitFlags & RUNKIT_ACC_STATIC) ? '::' : '->'; + $runkitFlags = self::compute_runkit7_flags($className, $methodName); + $methodSeparator = ($runkitFlags & RUNKIT7_ACC_STATIC) ? '::' : '->'; if ($className !== null) { $fullMethodName = $className . $methodSeparator . $methodName; } else { @@ -141,12 +145,12 @@ public function __construct($className, $methodName, array $options = []) { $originalMethodName = $methodName . '_original'; if ($className !== null) { - if (!runkit_method_copy($className, $originalMethodName, $className, $methodName)) { - throw new \RuntimeException('Failed to copy method'); + if (!runkit7_method_copy($className, $originalMethodName, $className, $methodName)) { + throw new RuntimeException('Failed to copy method'); } } else { - if (!runkit_function_copy(ltrim($methodName, "\\"), $originalMethodName)) { - throw new \RuntimeException('Failed to copy function'); + if (!runkit7_function_copy(ltrim($methodName, "\\"), $originalMethodName)) { + throw new RuntimeException('Failed to copy function'); } } $args = ''; // no args @@ -156,8 +160,9 @@ public function __construct($className, $methodName, array $options = []) { * @param string $default * @return string callback to invoke for this stage of the method tracing (Before call, after call returns, after call has an exception) * Treat passing no value as the default, treat passing falsey value as a no-op, and otherwise, expect a string. + * @suppress PhanPluginCanUseReturnType */ - $getOption = function(string $key, $default) use ($options) { + $getOption = static function (string $key, string $default) use ($options) { if (!array_key_exists($key, $options)) { return $default; } else if ($options[$key]) { @@ -169,19 +174,19 @@ public function __construct($className, $methodName, array $options = []) { $log_return = $getOption(self::PARAM_RETURN_LOGGER, self::DEFAULT_RETURN_LOGGER); $log_exception = $getOption(self::PARAM_EXCEPTION_LOGGER, self::DEFAULT_EXCEPTION_LOGGER); if ($log_args && (!is_string($log_args) || !is_callable($log_args))) { - throw new \InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_ARGS_LOGGER, got " . gettype($log_args)); + throw new InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_ARGS_LOGGER, got " . gettype($log_args)); } if ($log_return && (!is_string($log_return) || !is_callable($log_return))) { - throw new \InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_RETURN_LOGGER, got " . gettype($log_return)); + throw new InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_RETURN_LOGGER, got " . gettype($log_return)); } if ($log_exception && (!is_string($log_exception) || !is_callable($log_exception))) { - throw new \InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_EXCEPTION_LOGGER, got " . gettype($log_exception)); + throw new InvalidArgumentException("Expected a callable string (method/function) for TraceOn::PARAM_EXCEPTION_LOGGER, got " . gettype($log_exception)); } $print_backtrace_repr = ($options[self::PARAM_SHOULD_PRINT_BACKTRACE] ?? true) ? 'true' : 'false'; if ($className === null) { $fullOriginalMethodRepr = $originalMethodName; - } else if ($runkitFlags & RUNKIT_ACC_STATIC) { + } else if ($runkitFlags & RUNKIT7_ACC_STATIC) { $fullOriginalMethodRepr = 'self::' . $originalMethodName; } else { $fullOriginalMethodRepr = '$this->' . $originalMethodName; @@ -206,12 +211,12 @@ public function __construct($className, $methodName, array $options = []) { EOT; if ($className !== null) { - if (!runkit_method_redefine($className, $methodName, $args, $implementation, $runkitFlags)) { - throw new \RuntimeException('Failed to redefine method ' . $fullMethodName); + if (!runkit7_method_redefine($className, $methodName, $args, $implementation, $runkitFlags)) { + throw new RuntimeException('Failed to redefine method ' . $fullMethodName); } } else { - if (!runkit_function_redefine($methodName, $args, $implementation)) { - throw new \RuntimeException('Failed to redefine method ' . $fullMethodName); + if (!runkit7_function_redefine($methodName, $args, $implementation)) { + throw new RuntimeException('Failed to redefine method ' . $fullMethodName); } } $this->_className = $className; @@ -220,100 +225,116 @@ public function __construct($className, $methodName, array $options = []) { self::$_registry[$key] = $this; } - // Callback to do nothing - public static function noop(...$arguments) { } + /** + * Callback to do nothing + * @param ...$arguments @unused-param + * @suppress PhanPluginUseReturnValueNoopVoid + */ + public static function noop(...$arguments): void { + } - public static function log_arguments($fullMethodName, array $args) { + /** + * @param list $args + */ + public static function log_arguments(string $fullMethodName, array $args): void { echo "in $fullMethodName : Params : \n"; var_export($args); echo "\\n"; } - public static function log_return(string $fullMethodName, $value) { + /** + * @param mixed $value + */ + public static function log_return(string $fullMethodName, $value): void { echo "\nreturn value of $fullMethodName is :\n"; var_export($value); echo "\n\n"; } - public static function log_exception(string $fullMethodName, \Throwable $e) { + public static function log_exception(string $fullMethodName, Throwable $e): void { printf("\ngot exception of class %s for %s : %s\n%s\n\n", get_class($e), $fullMethodName, $e->getMessage(), $e->getTraceAsString()); } - public static function log_arguments_json(string $fullMethodName, array $args) { + /** + * @param list $args + */ + public static function log_arguments_json(string $fullMethodName, array $args): void { printf("Calling %s: args=%s\n", $fullMethodName, json_encode($args)); } /** * @param ?string $className (if null, this returns 0) * @param string $methodName - * @return int $flags to pass to runkit for the signature of the new implementation of the method + * @return int $flags to pass to runkit7 for the signature of the new implementation of the method */ - public static function compute_runkit_flags($className, string $methodName) : int { + public static function compute_runkit7_flags(?string $className, string $methodName) : int { if ($className === null) { return 0; } $method = new \ReflectionMethod($className, $methodName); $flags = 0; if ($method->isStatic()) { - $flags |= RUNKIT_ACC_STATIC; + $flags |= RUNKIT7_ACC_STATIC; } if ($method->isPrivate()) { - $flags |= RUNKIT_ACC_PRIVATE; + $flags |= RUNKIT7_ACC_PRIVATE; } else if ($method->isProtected()) { - $flags |= RUNKIT_ACC_PROTECTED; + $flags |= RUNKIT7_ACC_PROTECTED; } else { - $flags |= RUNKIT_ACC_PUBLIC; + $flags |= RUNKIT7_ACC_PUBLIC; } return $flags; } /** - * @param ?string $className - * @param string $methodName - * @return string + * @param ?string $className if non-null, the name of the class + * @param string $methodName the name of the function/method */ - public static function get_key($className, string $methodName) { + public static function get_key(?string $className, string $methodName): string { if ($className === null) { - return ltrim("\\", $methodName); + return ltrim($methodName, "\\"); } return sprintf('%s::%s', ltrim(strtolower($className), "\\"), strtolower($methodName)); } /** * Stop logging calls to this function. - * @return void */ - public function cleanup() { - if (!$this->_traced) { return; } + public function cleanup(): void { + if (!$this->_traced) { + return; + } $key = self::get_key($this->_className, $this->_methodName); if ($this->_className !== null) { - if (!runkit_method_remove($this->_className, $this->_methodName)) { - throw new \RuntimeException('Unmock failed to remove method ' . $key); + if (!runkit7_method_remove($this->_className, $this->_methodName)) { + throw new RuntimeException('Unmock failed to remove method ' . $key); } - if (!runkit_method_rename($this->_className, $this->_originalMethodName, $this->_methodName)) { - throw new \RuntimeException('Unmock failed rename to restore original method for ' . $key); + if (!runkit7_method_rename($this->_className, $this->_originalMethodName, $this->_methodName)) { + throw new RuntimeException('Unmock failed rename to restore original method for ' . $key); } } else { - if (!runkit_function_remove($this->_methodName)) { - throw new \RuntimeException('Unmock failed to remove function ' . $this->_methodName); + if (!runkit7_function_remove($this->_methodName)) { + throw new RuntimeException('Unmock failed to remove function ' . $this->_methodName); } - if (!runkit_function_rename($this->_originalMethodName, $this->_methodName)) { - throw new \RuntimeException('Unmock failed rename to restore original function for ' . $this->_methodName); + if (!runkit7_function_rename($this->_originalMethodName, $this->_methodName)) { + throw new RuntimeException('Unmock failed rename to restore original function for ' . $this->_methodName); } } $this->_traced = false; unset(self::$_registry[$key]); } + public function __destruct() { + $this->cleanup(); + } + /** * Cleans up all of the TraceOn instances. - * @return void */ - public static function cleanup_all() { + public static function cleanup_all(): void { $entries = self::$_registry; foreach ($entries as $entry) { $entry->cleanup(); } } } - diff --git a/tests/TraceOnTest.php b/tests/TraceOnTest.php index 32b69a1..e186d66 100644 --- a/tests/TraceOnTest.php +++ b/tests/TraceOnTest.php @@ -1,8 +1,9 @@