<?php
declare(strict_types=1);
/**
* Hoa
*
*
* @license
*
* New BSD License
*
* Copyright © 2007-2017, Hoa community. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Hoa nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace Kitab\Compiler\Target\DocTest\CodeBlockHandler;
use Kitab\Compiler\Parser;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\PrettyPrinter;
/**
* A code block handler for the `php` type.
*
* This handler defines the `php` type with the following options:
*
* * `php,ignore` to avoid compiling the code block into a test case. The code
* block is just displayed in the documentation, but not tested,
* * `php,must_throw` to indicate that the code block must throw an exception,
* so catching an exception is expected,
* * `php,must_throw(E)` to indicate that the code block must throw an
* exception of kind `E`.
*/
class Php implements Definition
{
/**
* A traverser, for PHP-Parser, is a set of visitors visiting the Abstract
* Syntax Tree produced by the parser. This traverser will be used to
* pretty print the PHP code, and to make it embeddable inside a test
* case.
*
* The traverser is allocated once, hence the static declaration.
*/
protected static $_phpTraverser = null;
/**
* The handler name is `php`.
*
* # Examples
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* assert('php' === $handler->getDefinitionName());
* ```
*/
public function getDefinitionName(): string
{
return 'php';
}
/**
* Check whether a code block type contains `php` or is empty. The
* consequence is that the `php` type will be assumed if a code block has
* no type.
*
* # Examples
*
* All the following type syntaxes are handled:
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* assert(true === $handler->mightHandleCodeblock('php'));
* assert(true === $handler->mightHandleCodeblock('php,ignore'));
* assert(true === $handler->mightHandleCodeblock('php,must_throw'));
* assert(false === $handler->mightHandleCodeblock('foobar'));
* ```
*
* A code block with no type is assumed to be of type `php`:
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* assert($handler->mightHandleCodeblock(''));
* ```
*/
public function mightHandleCodeblock(string $codeBlockType): bool
{
return empty($codeBlockType) || 0 !== preg_match('/\bphp\b/', $codeBlockType);
}
/**
* Unfold the code block content, and compile it into a test case.
*
* # Examples
*
* A regular code block content:
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* $codeBlockType = 'php';
* $codeBlockContent = 'assert(true);';
* $output =
* '$this' . "\n" .
* ' ->assert(function () {' . "\n" .
* ' \assert(\true);' . "\n" .
* ' });';
*
* assert($output === $handler->compileToTestCaseBody($codeBlockType, $codeBlockContent));
* ```
*
* A code block that must not be tested:
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* $codeBlockType = 'php,ignore';
* $codeBlockContent = 'assert(true);';
* $output = '$this->skip(\'Skipped because the code block type contains `ignore`: `php,ignore`.\');';
*
* assert($output === $handler->compileToTestCaseBody($codeBlockType, $codeBlockContent));
* ```
*
* A code block that must throw an exception of kind `E`:
*
* ```php
* $handler = new Kitab\Compiler\Target\DocTest\CodeBlockHandler\Php();
*
* $codeBlockType = 'php,must_throw(E)';
* $codeBlockContent = 'assert(true);';
* $output =
* '$this' . "\n" .
* ' ->exception(function () {' . "\n" .
* ' \assert(\true);' . "\n" .
* ' })' . "\n" .
* ' ->isInstanceOf(\E::class);';
*
* assert($output === $handler->compileToTestCaseBody($codeBlockType, $codeBlockContent));
* ```
*/
public function compileToTestCaseBody(string $codeBlockType, string $codeBlockContent): string
{
$codeBlockContent = $this->unfoldCode($codeBlockContent);
if (0 !== preg_match('/\bignore\b/', $codeBlockType)) {
return
sprintf(
'$this->skip(\'Skipped because ' .
'the code block type contains `ignore`: `%s`.\');',
$codeBlockType
);
}
if (0 !== preg_match('/\bmust_throw(?:\(([^\)]+)\)|\b)/', $codeBlockType, $matches)) {
return
sprintf(
'$this' . "\n" .
' ->exception(function () {' . "\n" .
' %s' . "\n" .
' })' . "\n" .
' ->isInstanceOf(\\%s::class);',
preg_replace(
'/^\h+$/m',
'',
str_replace("\n", "\n" . ' ', $codeBlockContent)
),
isset($matches[1]) ? $matches[1] : 'Exception'
);
}
return
sprintf(
'$this' . "\n" .
' ->assert(function () {' . "\n" .
' %s' . "\n" .
' });',
preg_replace(
'/^\h+$/m',
'',
str_replace("\n", "\n" . ' ', $codeBlockContent)
)
);
}
/**
* Prepare the code to be embeddable inside a test case.
*/
protected function unfoldCode(string $phpCode): string
{
$ast = Parser::getPhpParser()->parse('<?php ' . $phpCode);
$ast = self::getPhpTraverser()->traverse($ast);
return Parser::getPhpPrettyPrinter()->prettyPrint($ast);
}
/**
* Get the statically allocated traverser instance.
*/
protected static function getPhpTraverser(): NodeTraverser
{
if (null === self::$_phpTraverser) {
self::$_phpTraverser = new NodeTraverser();
self::$_phpTraverser->addVisitor(new NodeVisitor\NameResolver());
self::$_phpTraverser->addVisitor(new IntoPHPTestCaseBody());
}
return self::$_phpTraverser;
}
}