Fix code sniffer errors in CodeHacker and related classes.

This commit is contained in:
Nestor Soriano 2020-04-16 15:03:15 +02:00
parent 57845ef8b8
commit f0b0822c1c
6 changed files with 147 additions and 43 deletions

View File

@ -1,7 +1,19 @@
<?php <?php
/**
* CodeHacker class file.
*
* @package WooCommerce/Testing
*/
//phpcs:disable Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing
//phpcs:disable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged
namespace Automattic\WooCommerce\Testing\CodeHacking; namespace Automattic\WooCommerce\Testing\CodeHacking;
use \ReflectionObject;
use \ReflectionFunction;
use \ReflectionException;
/** /**
* CodeHacker - allows to hack (alter on the fly) the content of PHP code files. * CodeHacker - allows to hack (alter on the fly) the content of PHP code files.
* *
@ -24,19 +36,19 @@ class CodeHacker {
const PROTOCOL = 'file'; const PROTOCOL = 'file';
/** @var resource|null */
public $context; public $context;
/** @var resource|null */
private $handle; private $handle;
/** @var array|null */ private static $path_white_list = array();
private static $pathWhitelist = array();
private static $hacks = array(); private static $hacks = array();
private static $enabled = false; private static $enabled = false;
/**
* Enable the code hacker.
*/
public static function enable() { public static function enable() {
if ( ! self::$enabled ) { if ( ! self::$enabled ) {
stream_wrapper_unregister( self::PROTOCOL ); stream_wrapper_unregister( self::PROTOCOL );
@ -45,6 +57,9 @@ class CodeHacker {
} }
} }
/**
* Disable the code hacker.
*/
public static function restore() { public static function restore() {
if ( self::$enabled ) { if ( self::$enabled ) {
stream_wrapper_restore( self::PROTOCOL ); stream_wrapper_restore( self::PROTOCOL );
@ -52,26 +67,65 @@ class CodeHacker {
} }
} }
/**
* Unregister all the registered hacks.
*/
public static function clear_hacks() { public static function clear_hacks() {
self::$hacks = array(); self::$hacks = array();
} }
/**
* Check if the code hacker is enabled.
*
* @return bool True if the code hacker is enabled.
*/
public static function is_enabled() { public static function is_enabled() {
return self::$enabled; return self::$enabled;
} }
/**
* Register a new hack.
*
* @param mixed $hack A function with signature "hack($code, $path)" or an object containing a method with that signature.
* @throws \Exception Invalid input.
*/
public static function add_hack( $hack ) { public static function add_hack( $hack ) {
if ( ! is_callable( $hack ) && ! is_object( $hack ) ) { if ( ! is_callable( $hack ) && ! is_object( $hack ) ) {
throw new Exception( "Hacks must be either functions, or objects having a 'process(\$text, \$path)' method." ); throw new \Exception( "Hacks must be either functions, or objects having a 'process(\$text, \$path)' method." );
} }
// TODO: Check that callbacks have at least two parameters (if they have more, they must be optional); and that objects have a "process" method with the same condition. if ( ! self::is_valid_hack_callback( $hack ) && ! self::is_valid_hack_object( $hack ) ) {
throw new \Exception( "CodeHacker::addhack: hacks must be either a function with a 'hack(\$code,\$path)' signature, or an object containing a public method 'hack' with that signature. " );
}
self::$hacks[] = $hack; self::$hacks[] = $hack;
} }
public static function setWhitelist( array $pathWhitelist ) { private static function is_valid_hack_callback( $callback ) {
self::$pathWhitelist = $pathWhitelist; return is_callable( $callback ) && 2 === ( new ReflectionFunction( $callback ) )->getNumberOfRequiredParameters();
}
private static function is_valid_hack_object( $callback ) {
if ( ! is_object( $callback ) ) {
return false;
}
$ro = new ReflectionObject( ( $callback ) );
try {
$rm = $ro->getMethod( 'hack' );
return $rm->isPublic() && ! $rm->isStatic() && 2 === $rm->getNumberOfRequiredParameters();
} catch ( ReflectionException $exception ) {
return false;
}
}
/**
* Set the white list of files to hack. If note set, all the PHP files will be hacked.
*
* @param array $path_white_list Paths of the files to hack, can be relative paths.
*/
public static function set_white_list( array $path_white_list ) {
self::$path_white_list = $path_white_list;
} }
@ -89,7 +143,7 @@ class CodeHacker {
public function dir_readdir() { public function dir_readdir() {
return readdir( $this->handle ); return readdir( $this->handle );
} }
@ -104,8 +158,8 @@ class CodeHacker {
} }
public function rename( $pathFrom, $pathTo ) { public function rename( $path_from, $path_to ) {
return $this->native( 'rename', $pathFrom, $pathTo, $this->context ); return $this->native( 'rename', $path_from, $path_to, $this->context );
} }
@ -114,13 +168,11 @@ class CodeHacker {
} }
public function stream_cast( $castAs ) { public function stream_cast( $cast_as ) {
return $this->handle; return $this->handle;
} }
public function stream_close() { public function stream_close() {
// echo "***** CLOSE HANDLE: " . $this->handle . " \n";
fclose( $this->handle ); fclose( $this->handle );
} }
@ -159,11 +211,11 @@ class CodeHacker {
} }
public function stream_open( $path, $mode, $options, &$openedPath ) { public function stream_open( $path, $mode, $options, &$opened_path ) {
$usePath = (bool) ( $options & STREAM_USE_PATH ); $use_path = (bool) ( $options & STREAM_USE_PATH );
if ( $mode === 'rb' && self::pathInWhitelist( $path ) && pathinfo( $path, PATHINFO_EXTENSION ) === 'php' ) { if ( 'rb' === $mode && self::path_in_white_list( $path ) && 'php' === pathinfo( $path, PATHINFO_EXTENSION ) ) {
$content = $this->native( 'file_get_contents', $path, $usePath, $this->context ); $content = $this->native( 'file_get_contents', $path, $use_path, $this->context );
if ( $content === false ) { if ( false === $content ) {
return false; return false;
} }
$modified = self::hack( $content, $path ); $modified = self::hack( $content, $path );
@ -175,8 +227,8 @@ class CodeHacker {
} }
} }
$this->handle = $this->context $this->handle = $this->context
? $this->native( 'fopen', $path, $mode, $usePath, $this->context ) ? $this->native( 'fopen', $path, $mode, $use_path, $this->context )
: $this->native( 'fopen', $path, $mode, $usePath ); : $this->native( 'fopen', $path, $mode, $use_path );
return (bool) $this->handle; return (bool) $this->handle;
} }
@ -196,17 +248,17 @@ class CodeHacker {
public function stream_stat() { public function stream_stat() {
return fstat( $this->handle ); return fstat( $this->handle );
} }
public function stream_tell() { public function stream_tell() {
return ftell( $this->handle ); return ftell( $this->handle );
} }
public function stream_truncate( $newSize ) { public function stream_truncate( $new_size ) {
return ftruncate( $this->handle, $newSize ); return ftruncate( $this->handle, $new_size );
} }
@ -250,15 +302,19 @@ class CodeHacker {
} }
private static function pathInWhitelist( $path ) { private static function path_in_white_list( $path ) {
if ( empty( self::$pathWhitelist ) ) { if ( empty( self::$path_white_list ) ) {
return true; return true;
} }
foreach ( self::$pathWhitelist as $whitelistItem ) { foreach ( self::$path_white_list as $white_list_item ) {
if ( substr( $path, -strlen( $whitelistItem ) ) === $whitelistItem ) { if ( substr( $path, -strlen( $white_list_item ) ) === $white_list_item ) {
return true; return true;
} }
} }
return false; return false;
} }
} }
//phpcs:enable Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing
//phpcs:enable WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged

View File

@ -1,4 +1,11 @@
<?php <?php
/**
* CodeHackerTestHook class file.
*
* @package WooCommerce/Testing
*/
// phpcs:disable Squiz.Commenting.FunctionComment.Missing, PHPCompatibility.FunctionDeclarations
namespace Automattic\WooCommerce\Testing\CodeHacking; namespace Automattic\WooCommerce\Testing\CodeHacking;
@ -91,14 +98,14 @@ final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
* *
* @param object $reflection_object The class or method reflection object whose doc comment will be parsed. * @param object $reflection_object The class or method reflection object whose doc comment will be parsed.
* @return bool True if at least one valid @hack annotation was found. * @return bool True if at least one valid @hack annotation was found.
* @throws Exception * @throws Exception Class specified in @hack directive doesn't exist.
*/ */
private function add_hacks_from_annotations( $reflection_object ) { private function add_hacks_from_annotations( $reflection_object ) {
$annotations = Test::parseAnnotations( $reflection_object->getDocComment() ); $annotations = Test::parseAnnotations( $reflection_object->getDocComment() );
$hacks_added = false; $hacks_added = false;
foreach ( $annotations as $id => $annotation_instances ) { foreach ( $annotations as $id => $annotation_instances ) {
if ( $id !== 'hack' ) { if ( 'hack' !== $id ) {
continue; continue;
} }
@ -107,7 +114,7 @@ final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
$params = $matches[0]; $params = $matches[0];
$hack_class = array_shift( $params ); $hack_class = array_shift( $params );
if(false === strpos( $hack_class, '\\' )) { if ( false === strpos( $hack_class, '\\' ) ) {
$hack_class = __NAMESPACE__ . '\\Hacks\\' . $hack_class; $hack_class = __NAMESPACE__ . '\\Hacks\\' . $hack_class;
} }
@ -132,7 +139,7 @@ final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
* *
* @param string $class_name Test class name. * @param string $class_name Test class name.
* @param string $method_name Test method name. * @param string $method_name Test method name.
* @throws ReflectionException * @throws ReflectionException Error when instatiating a ReflectionClass.
*/ */
private function execute_before_methods( $class_name, $method_name ) { private function execute_before_methods( $class_name, $method_name ) {
$methods = array( 'before_all', "before_{$method_name}" ); $methods = array( 'before_all', "before_{$method_name}" );
@ -154,3 +161,5 @@ final class CodeHackerTestHook implements BeforeTestHook, AfterTestHook {
} }
} }
} }
// phpcs:enable Squiz.Commenting.FunctionComment.Missing, PHPCompatibility.FunctionDeclarations

View File

@ -1,4 +1,11 @@
<?php <?php
/**
* BypassFinalsHack class file.
*
* @package WooCommerce/Testing
*/
// phpcs:disable Squiz.Commenting.FunctionComment.Missing
namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks; namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks;
@ -21,3 +28,5 @@ final class BypassFinalsHack extends CodeHack {
return $code; return $code;
} }
} }
// phpcs:enable Squiz.Commenting.FunctionComment.Missing

View File

@ -1,4 +1,9 @@
<?php <?php
/**
* CodeHack class file.
*
* @package WooCommerce/Testing
*/
namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks; namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks;
@ -26,6 +31,7 @@ abstract class CodeHack {
* @return array Tokenized code. * @return array Tokenized code.
*/ */
protected function tokenize( $code ) { protected function tokenize( $code ) {
//phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters, PHPCompatibility.Constants.NewConstants
return PHP_VERSION_ID >= 70000 ? token_get_all( $code, TOKEN_PARSE ) : token_get_all( $code ); return PHP_VERSION_ID >= 70000 ? token_get_all( $code, TOKEN_PARSE ) : token_get_all( $code );
} }
@ -33,7 +39,7 @@ abstract class CodeHack {
* Check if a token is of a given type. * Check if a token is of a given type.
* *
* @param mixed $token Token to check. * @param mixed $token Token to check.
* @param int $type Type of token to check (see https://www.php.net/manual/en/tokens.php) * @param int $type Type of token to check (see https://www.php.net/manual/en/tokens.php).
* @return bool True if it's a token of the given type, false otherwise. * @return bool True if it's a token of the given type, false otherwise.
*/ */
protected function is_token_of_type( $token, $type ) { protected function is_token_of_type( $token, $type ) {
@ -53,7 +59,7 @@ abstract class CodeHack {
/** /**
* Converts a token to its string representation. * Converts a token to its string representation.
* *
* @param $token Token to convert. * @param mixed $token Token to convert.
* @return mixed String representation of the token. * @return mixed String representation of the token.
*/ */
protected function token_to_string( $token ) { protected function token_to_string( $token ) {
@ -70,7 +76,7 @@ abstract class CodeHack {
*/ */
protected function string_ends_with( $haystack, $needle ) { protected function string_ends_with( $haystack, $needle ) {
$length = strlen( $needle ); $length = strlen( $needle );
if ( $length == 0 ) { if ( 0 === $length ) {
return true; return true;
} }

View File

@ -1,4 +1,11 @@
<?php <?php
/**
* FunctionsMockerHack class file.
*
* @package WooCommerce/Testing
*/
// phpcs:disable Squiz.Commenting.FunctionComment.Missing
namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks; namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks;
@ -17,6 +24,11 @@ use ReflectionClass;
*/ */
final class FunctionsMockerHack extends CodeHack { final class FunctionsMockerHack extends CodeHack {
/**
* Tokens that precede a non-standalone-function identifier.
*
* @var array
*/
private static $non_global_function_tokens = array( private static $non_global_function_tokens = array(
T_PAAMAYIM_NEKUDOTAYIM, T_PAAMAYIM_NEKUDOTAYIM,
T_DOUBLE_COLON, T_DOUBLE_COLON,
@ -28,9 +40,9 @@ final class FunctionsMockerHack extends CodeHack {
* FunctionsMockerHack constructor. * FunctionsMockerHack constructor.
* *
* @param string $mock_class Name of the class containing function mocks as public static methods. * @param string $mock_class Name of the class containing function mocks as public static methods.
* @throws ReflectionException * @throws ReflectionException Error when instantiating ReflectionClass.
*/ */
public function __construct( string $mock_class ) { public function __construct( $mock_class ) {
$this->mock_class = $mock_class; $this->mock_class = $mock_class;
$rc = new ReflectionClass( $mock_class ); $rc = new ReflectionClass( $mock_class );
@ -53,14 +65,16 @@ final class FunctionsMockerHack extends CodeHack {
$token_type = $this->token_type_of( $token ); $token_type = $this->token_type_of( $token );
if ( T_WHITESPACE === $token_type ) { if ( T_WHITESPACE === $token_type ) {
$code .= $this->token_to_string( $token ); $code .= $this->token_to_string( $token );
} elseif ( T_STRING === $token_type && ! $previous_token_is_non_global_function_qualifier && in_array( $token[1], $this->mocked_methods ) ) { } elseif ( T_STRING === $token_type && ! $previous_token_is_non_global_function_qualifier && in_array( $token[1], $this->mocked_methods, true ) ) {
$code .= "{$this->mock_class}::{$token[1]}"; $code .= "{$this->mock_class}::{$token[1]}";
} else { } else {
$code .= $this->token_to_string( $token ); $code .= $this->token_to_string( $token );
$previous_token_is_non_global_function_qualifier = in_array( $token_type, self::$non_global_function_tokens ); $previous_token_is_non_global_function_qualifier = in_array( $token_type, self::$non_global_function_tokens, true );
} }
} }
return $code; return $code;
} }
} }
// phpcs:enable Squiz.Commenting.FunctionComment.Missing

View File

@ -1,4 +1,11 @@
<?php <?php
/**
* StaticMockerHack class file.
*
* @package WooCommerce/Testing
*/
// phpcs:disable Squiz.Commenting.FunctionComment.Missing
namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks; namespace Automattic\WooCommerce\Testing\CodeHacking\Hacks;
@ -26,9 +33,9 @@ final class StaticMockerHack extends CodeHack {
* *
* @param string $source_class Name of the original class (the one having the members to be mocked). * @param string $source_class Name of the original class (the one having the members to be mocked).
* @param string $mock_class Name of the mock class (the one having the replacement mock members). * @param string $mock_class Name of the mock class (the one having the replacement mock members).
* @throws ReflectionException * @throws ReflectionException Error when instantiating ReflectionClass.
*/ */
public function __construct( string $source_class, string $mock_class ) { public function __construct( $source_class, $mock_class ) {
$this->source_class = $source_class; $this->source_class = $source_class;
$this->target_class = $mock_class; $this->target_class = $mock_class;
@ -61,12 +68,13 @@ final class StaticMockerHack extends CodeHack {
$code = ''; $code = '';
$current_token = null; $current_token = null;
// phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( $current_token = current( $tokens ) ) { while ( $current_token = current( $tokens ) ) {
if ( $this->is_token_of_type( $current_token, T_STRING ) && $this->source_class === $current_token[1] ) { if ( $this->is_token_of_type( $current_token, T_STRING ) && $this->source_class === $current_token[1] ) {
$next_token = next( $tokens ); $next_token = next( $tokens );
if ( $this->is_token_of_type( $next_token, T_DOUBLE_COLON ) ) { if ( $this->is_token_of_type( $next_token, T_DOUBLE_COLON ) ) {
$called_member = next( $tokens )[1]; $called_member = next( $tokens )[1];
if ( in_array( $called_member, $this->members_implemented_in_mock ) ) { if ( in_array( $called_member, $this->members_implemented_in_mock, true ) ) {
// Reference to source class member that exists in mock class, replace. // Reference to source class member that exists in mock class, replace.
$code .= "{$this->target_class}::{$called_member}"; $code .= "{$this->target_class}::{$called_member}";
} else { } else {
@ -88,3 +96,5 @@ final class StaticMockerHack extends CodeHack {
return $code; return $code;
} }
} }
// phpcs:enable Squiz.Commenting.FunctionComment.Missing