Skip to content

Commit 2c321dc

Browse files
committed
added TypeAssert helper for type inference testing with Nette Tester
1 parent 3aa338a commit 2c321dc

4 files changed

Lines changed: 182 additions & 0 deletions

File tree

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@ parameters:
55
- src
66
- tests
77

8+
excludePaths:
9+
analyse:
10+
- tests/data/*
11+
812
includes:
913
- extension.neon

src/Testing/TypeAssert.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Nette\PHPStan\Testing;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Name;
9+
use PHPStan\Analyser\NodeScopeResolver;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Analyser\ScopeContext;
12+
use PHPStan\Analyser\ScopeFactory;
13+
use PHPStan\DependencyInjection\Container;
14+
use PHPStan\DependencyInjection\ContainerFactory;
15+
use PHPStan\File\FileHelper;
16+
use PHPStan\Type\VerbosityLevel;
17+
use Tester\Assert;
18+
use function array_merge, count, hash, implode, in_array, sprintf, strtolower, sys_get_temp_dir;
19+
20+
21+
/**
22+
* Verifies assertType() calls in a PHP file against PHPStan's type inference using Nette Tester.
23+
*/
24+
class TypeAssert
25+
{
26+
/** @var array<string, Container> */
27+
private static array $containers = [];
28+
29+
30+
/**
31+
* Gathers assertType() calls from a PHP file and verifies them against PHPStan's type inference.
32+
* @param list<string> $configFiles
33+
*/
34+
public static function assertTypes(string $file, array $configFiles = []): void
35+
{
36+
$container = self::createContainer($configFiles);
37+
38+
$fileHelper = $container->getByType(FileHelper::class);
39+
$file = $fileHelper->normalizePath($file);
40+
41+
$parser = $container->getService('defaultAnalysisParser');
42+
$nodeScopeResolver = $container->getByType(NodeScopeResolver::class);
43+
$scopeFactory = $container->getByType(ScopeFactory::class);
44+
45+
$pathRoutingParser = $container->getService('pathRoutingParser');
46+
$pathRoutingParser->setAnalysedFiles([$file]);
47+
$nodeScopeResolver->setAnalysedFiles([$file]);
48+
$scope = $scopeFactory->create(ScopeContext::create($file));
49+
50+
$asserts = [];
51+
$nodeScopeResolver->processNodes(
52+
$parser->parseFile($file),
53+
$scope,
54+
static function (Node $node, Scope $scope) use (&$asserts): void {
55+
$assert = self::processAssertTypeCall($node, $scope);
56+
if ($assert !== null) {
57+
$asserts[] = $assert;
58+
}
59+
},
60+
);
61+
62+
if (count($asserts) === 0) {
63+
Assert::fail(sprintf('File %s does not contain any assertType() calls.', $file));
64+
}
65+
66+
foreach ($asserts as $assert) {
67+
Assert::same(
68+
$assert['expected'],
69+
$assert['actual'],
70+
sprintf('on line %d', $assert['line']),
71+
);
72+
}
73+
}
74+
75+
76+
/**
77+
* @return array{expected: string, actual: string, line: int}|null
78+
*/
79+
private static function processAssertTypeCall(Node $node, Scope $scope): ?array
80+
{
81+
if (!$node instanceof Node\Expr\FuncCall) {
82+
return null;
83+
}
84+
85+
$nameNode = $node->name;
86+
if (!$nameNode instanceof Name) {
87+
return null;
88+
}
89+
90+
$functionName = $nameNode->toString();
91+
92+
if (in_array(strtolower($functionName), ['asserttype', 'assertnativetype'], true)) {
93+
Assert::fail(sprintf('Missing use statement for %s() on line %d.', $functionName, $node->getStartLine()));
94+
}
95+
96+
if ($functionName !== 'PHPStan\Testing\assertType') {
97+
return null;
98+
}
99+
100+
if (count($node->getArgs()) !== 2) {
101+
Assert::fail(sprintf('Wrong assertType() call on line %d.', $node->getStartLine()));
102+
}
103+
104+
$expectedType = $scope->getType($node->getArgs()[0]->value);
105+
$constantStrings = $expectedType->getConstantStrings();
106+
if (count($constantStrings) !== 1) {
107+
Assert::fail(sprintf(
108+
'Expected type must be a literal string, %s given on line %d.',
109+
$expectedType->describe(VerbosityLevel::precise()),
110+
$node->getStartLine(),
111+
));
112+
return null;
113+
}
114+
115+
$actualType = $scope->getType($node->getArgs()[1]->value);
116+
return [
117+
'expected' => $constantStrings[0]->getValue(),
118+
'actual' => $actualType->describe(VerbosityLevel::precise()),
119+
'line' => $node->getStartLine(),
120+
];
121+
}
122+
123+
124+
/**
125+
* @param list<string> $configFiles
126+
*/
127+
private static function createContainer(array $configFiles): Container
128+
{
129+
$cacheKey = hash('sha256', implode("\n", $configFiles));
130+
131+
if (isset(self::$containers[$cacheKey])) {
132+
ContainerFactory::postInitializeContainer(self::$containers[$cacheKey]);
133+
return self::$containers[$cacheKey];
134+
}
135+
136+
$tmpDir = sys_get_temp_dir() . '/phpstan-tests';
137+
@mkdir($tmpDir, 0o777, true); // @ - directory may already exist
138+
139+
$containerFactory = new ContainerFactory((string) getcwd());
140+
141+
$allConfigFiles = array_merge(
142+
[$containerFactory->getConfigDirectory() . '/config.level8.neon'],
143+
$configFiles,
144+
);
145+
146+
$container = $containerFactory->create($tmpDir, $allConfigFiles, []);
147+
self::$containers[$cacheKey] = $container;
148+
149+
foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) {
150+
(static function (string $file): void {
151+
require_once $file;
152+
})($bootstrapFile);
153+
}
154+
155+
return $container;
156+
}
157+
}

tests/TypeAssertTest.phpt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require __DIR__ . '/bootstrap.php';
6+
7+
use Nette\PHPStan\Testing\TypeAssert;
8+
9+
// assertType() inside a function body — verifies PathRoutingParser fix
10+
TypeAssert::assertTypes(__DIR__ . '/data/assert-in-function.php');

tests/data/assert-in-function.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
8+
function testAssertTypeInsideFunction(): void
9+
{
10+
assertType('1', 1);
11+
}

0 commit comments

Comments
 (0)