.phan/plugins/PHPUnitAssertionPlugin.php
<?php
declare(strict_types=1);
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Language\Context;
use Phan\Language\Element\Comment\Assertion;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\UnionType;
use Phan\PluginV3;
use Phan\PluginV3\AnalyzeFunctionCallCapability;
/**
* Mark PHPUnit helper assertions as having side effects.
*
* - assertTrue
* - assertNull
* - assertNotNull
* - assertFalse
* - assertSame($expected, $actual)
* - assertInstanceof
*
* NOTE: This will probably be rewritten
*/
class PHPUnitAssertionPlugin extends PluginV3 implements AnalyzeFunctionCallCapability
{
/**
* @override
*/
public function getAnalyzeFunctionCallClosures(CodeBase $code_base): array
{
// @phan-suppress-next-line PhanThrowTypeAbsentForCall
$assert_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString('PHPUnit\Framework\Assert');
if (!$code_base->hasClassWithFQSEN($assert_class_fqsen)) {
if (!getenv('PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET')) {
// @phan-suppress-next-line PhanPluginRemoveDebugCall
fwrite(STDERR, "PHPUnitAssertionPlugin failed to find class PHPUnit\Framework\Assert, giving up (set environment variable PHAN_PHPUNIT_ASSERTION_PLUGIN_QUIET=1 to ignore this)\n");
}
return [];
}
$result = [];
foreach ($code_base->getClassByFQSEN($assert_class_fqsen)->getMethodMap($code_base) as $method) {
$closure = $this->createClosureForMethod($code_base, $method, $method->getName());
if (!$closure) {
continue;
}
$result[(string)$method->getFQSEN()] = $closure;
}
return $result;
}
/**
* @return ?Closure(CodeBase, Context, FunctionInterface, array, ?Node):void
* @suppress PhanAccessClassConstantInternal, PhanAccessMethodInternal
*/
private function createClosureForMethod(CodeBase $code_base, Method $method, string $name): ?Closure
{
// TODO: Add a helper method which will convert a doc comment and a stub php function source code to a closure for a param index (or indices)
switch (\strtolower($name)) {
case 'asserttrue':
case 'assertnotfalse':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_TRUE),
0
);
case 'assertfalse':
case 'assertnottrue':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::empty(), 'unusedParamName', Assertion::IS_FALSE),
0
);
// TODO: Rest of https://github.com/sebastianbergmann/phpunit/issues/3368
case 'assertisstring':
// TODO: Could convert to real types?
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('string'), 'unusedParamName', Assertion::IS_OF_TYPE),
0
);
case 'assertnull':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_OF_TYPE),
0
);
case 'assertnotnull':
return $method->createClosureForAssertion(
$code_base,
new Assertion(UnionType::fromFullyQualifiedPHPDocString('null'), 'unusedParamName', Assertion::IS_NOT_OF_TYPE),
0
);
case 'assertsame':
// Sets the type of $actual to $expected
//
// This is equivalent to the side effects of the below doc comment.
// Note that the doc comment would make phan emit warnings about invalid classes, etc.
// TODO: Reuse the code for templates here
//
// (at)template T
// (at)param T $expected
// (at)param mixed $actual
// (at)phan-assert T $actual
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
static function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
return UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]);
},
Assertion::IS_OF_TYPE,
1
);
case 'assertinternaltype':
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
$string = $args[0];
if ($string instanceof ast\Node) {
$string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $string))->asSingleScalarValueOrNull();
}
if (!is_string($string)) {
return UnionType::empty();
}
$original_type = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[1]));
switch ($string) {
case 'numeric':
return UnionType::fromFullyQualifiedPHPDocString('int|float|string');
case 'integer':
case 'int':
return UnionType::fromFullyQualifiedPHPDocString('int');
case 'double':
case 'float':
case 'real':
return UnionType::fromFullyQualifiedPHPDocString('float');
case 'string':
return UnionType::fromFullyQualifiedPHPDocString('string');
case 'boolean':
case 'bool':
return UnionType::fromFullyQualifiedPHPDocString('bool');
case 'null':
return UnionType::fromFullyQualifiedPHPDocString('null');
case 'array':
$result = $original_type->arrayTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('array');
}
return $result;
case 'object':
$result = $original_type->objectTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('object');
}
return $result;
case 'resource':
return UnionType::fromFullyQualifiedPHPDocString('resource');
case 'scalar':
$result = $original_type->scalarTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('int|string|float|bool');
}
return $result;
case 'callable':
$result = $original_type->callableTypes();
if ($result->isEmpty()) {
return UnionType::fromFullyQualifiedPHPDocString('callable');
}
return $result;
}
// Warn about possibly invalid assertion
// NOTE: This is only emitted for variables
$this->emitPluginIssue(
$code_base,
$context,
'PhanPluginPHPUnitAssertionInvalidInternalType',
'Unknown type {STRING_LITERAL} in call to assertInternalType',
[$string]
);
return UnionType::empty();
},
Assertion::IS_OF_TYPE,
1
);
case 'assertinstanceof':
// This is equivalent to the side effects of the below doc comment.
// Note that the doc comment would make phan emit warnings about invalid classes, etc.
// TODO: Reuse the code for class-string<T> here.
//
// (at)template T
// (at)param class-string<T> $expected
// (at)param mixed $actual
// (at)phan-assert T $actual
return $method->createClosureForUnionTypeExtractorAndAssertionType(
/**
* @param list<Node|string|int|float> $args
*/
static function (CodeBase $code_base, Context $context, array $args): UnionType {
if (\count($args) < 2) {
return UnionType::empty();
}
$string = (UnionTypeVisitor::unionTypeFromNode($code_base, $context, $args[0]))->asSingleScalarValueOrNull();
if (!is_string($string)) {
return UnionType::empty();
}
try {
return FullyQualifiedClassName::fromFullyQualifiedString($string)->asType()->asPHPDocUnionType();
} catch (\Exception $_) {
return UnionType::empty();
}
},
Assertion::IS_OF_TYPE,
1
);
}
return null;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.
return new PHPUnitAssertionPlugin();