Skip to content

Commit 76d749f

Browse files
authored
Merge pull request #201 from clue-labs/phpstan-max
Improve type definitions and update to PHPStan level `max`
2 parents 03e0af6 + 8d34918 commit 76d749f

25 files changed

+631
-467
lines changed

examples/index.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE | ENT_DISALLOWED, 'utf-8'));
2222
};
2323

24+
$name = $request->getAttribute('name');
25+
assert(is_string($name));
26+
2427
return React\Http\Message\Response::plaintext(
25-
"Hello " . $escape($request->getAttribute('name')) . "!\n"
28+
"Hello " . $escape($name) . "!\n"
2629
);
2730
});
2831

@@ -63,6 +66,7 @@
6366
ob_start();
6467
var_dump($request);
6568
$info = ob_get_clean();
69+
assert(is_string($info));
6670

6771
if (PHP_SAPI !== 'cli' && (!function_exists('xdebug_is_enabled') || !xdebug_is_enabled())) {
6872
$info = htmlspecialchars($info, 0, 'utf-8');
@@ -201,9 +205,10 @@
201205
});
202206

203207
$app->get('/location/{status:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) {
204-
$statusCode = (int) $request->getAttribute('status');
208+
$statusCode = $request->getAttribute('status');
209+
assert(is_string($statusCode) && is_numeric($statusCode));
205210

206-
return new React\Http\Message\Response($statusCode, ['Location' => '/foobar']);
211+
return new React\Http\Message\Response((int) $statusCode, ['Location' => '/foobar']);
207212
});
208213

209214
$app->run();

phpstan.neon.dist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
parameters:
2-
level: 5
2+
level: max
33

44
paths:
55
- examples/
@@ -14,4 +14,4 @@ parameters:
1414
- '/^Instantiated class Fiber not found\.$/'
1515
- '/^Call to method (start|isTerminated|getReturn)\(\) on an unknown class Fiber\.$/'
1616
# ignore incomplete type information for mocks in legacy PHPUnit 7.5
17-
- '/^Parameter #\d+ \$.+ of class .+ constructor expects .+, PHPUnit\\Framework\\MockObject\\MockObject given\.$/'
17+
- '/^Parameter #\d+ .+ of .+ expects .+, PHPUnit\\Framework\\MockObject\\MockObject given\.$/'

src/AccessLogHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private function log(ServerRequestInterface $request, ResponseInterface $respons
9494

9595
private function escape(string $s): string
9696
{
97-
return preg_replace_callback('/[\x00-\x1F\x7F-\xFF"\\\\]+/', function (array $m) {
97+
return (string) preg_replace_callback('/[\x00-\x1F\x7F-\xFF"\\\\]+/', function (array $m) {
9898
return str_replace('%', '\x', rawurlencode($m[0]));
9999
}, $s);
100100
}

src/App.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public function redirect(string $route, string $target, int $code = Response::ST
220220
$this->any($route, new RedirectHandler($target, $code));
221221
}
222222

223-
public function run()
223+
public function run(): void
224224
{
225225
if (\PHP_SAPI === 'cli') {
226226
$this->runLoop();
@@ -229,7 +229,7 @@ public function run()
229229
}
230230
}
231231

232-
private function runLoop()
232+
private function runLoop(): void
233233
{
234234
$http = new HttpServer(function (ServerRequestInterface $request) {
235235
return $this->handleRequest($request);
@@ -240,7 +240,7 @@ private function runLoop()
240240
$socket = new SocketServer($listen);
241241
$http->listen($socket);
242242

243-
$this->sapi->log('Listening on ' . \str_replace('tcp:', 'http:', $socket->getAddress()));
243+
$this->sapi->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress()));
244244

245245
$http->on('error', function (\Exception $e) {
246246
$orig = $e;
@@ -290,7 +290,7 @@ private function runLoop()
290290
Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf');
291291
}
292292

293-
private function runOnce()
293+
private function runOnce(): void
294294
{
295295
$request = $this->sapi->requestFromGlobals();
296296

@@ -319,11 +319,14 @@ private function runOnce()
319319
private function handleRequest(ServerRequestInterface $request)
320320
{
321321
$response = ($this->handler)($request);
322+
assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator);
323+
322324
if ($response instanceof \Generator) {
323325
if ($response->valid()) {
324326
$response = $this->coroutine($response);
325327
} else {
326328
$response = $response->getReturn();
329+
assert($response instanceof ResponseInterface);
327330
}
328331
}
329332

@@ -341,6 +344,8 @@ private function coroutine(\Generator $generator): PromiseInterface
341344
}
342345

343346
$promise = $generator->current();
347+
assert($promise instanceof PromiseInterface);
348+
344349
$promise->then(function ($value) use ($generator, $next) {
345350
$generator->send($value);
346351
$next();

src/Container.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function __construct($loader = [])
3434
$this->container = $loader;
3535
}
3636

37+
/** @return mixed */
3738
public function __invoke(ServerRequestInterface $request, callable $next = null)
3839
{
3940
if ($next === null) {
@@ -117,6 +118,7 @@ public function getAccessLogHandler(): AccessLogHandler
117118
{
118119
if ($this->container instanceof ContainerInterface) {
119120
if ($this->container->has(AccessLogHandler::class)) {
121+
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
120122
return $this->container->get(AccessLogHandler::class);
121123
} else {
122124
return new AccessLogHandler();
@@ -130,6 +132,7 @@ public function getErrorHandler(): ErrorHandler
130132
{
131133
if ($this->container instanceof ContainerInterface) {
132134
if ($this->container->has(ErrorHandler::class)) {
135+
// @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError`
133136
return $this->container->get(ErrorHandler::class);
134137
} else {
135138
return new ErrorHandler();
@@ -139,22 +142,25 @@ public function getErrorHandler(): ErrorHandler
139142
}
140143

141144
/**
142-
* @template T
145+
* @template T of object
143146
* @param class-string<T> $name
144147
* @return T
145148
* @throws \BadMethodCallException if object of type $name can not be loaded
146149
*/
147150
private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */
148151
{
152+
assert(\is_array($this->container));
153+
149154
if (\array_key_exists($name, $this->container)) {
150155
if (\is_string($this->container[$name])) {
151156
if ($depth < 1) {
152157
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
153158
}
154159

160+
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
155161
$value = $this->loadObject($this->container[$name], $depth - 1);
156162
if (!$value instanceof $name) {
157-
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
163+
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . \get_class($value));
158164
}
159165

160166
$this->container[$name] = $value;
@@ -171,6 +177,7 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+)
171177
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
172178
}
173179

180+
// @phpstan-ignore-next-line because type of container value is explicitly checked after getting here
174181
$value = $this->loadObject($value, $depth - 1);
175182
}
176183
if (!$value instanceof $name) {
@@ -210,10 +217,14 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+)
210217
$params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false);
211218

212219
// instantiate with list of parameters
220+
// @phpstan-ignore-next-line because `$class->newInstance()` is known to return `T`
213221
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
214222
}
215223

216-
/** @throws \BadMethodCallException if either parameter can not be loaded */
224+
/**
225+
* @return list<mixed>
226+
* @throws \BadMethodCallException if either parameter can not be loaded
227+
*/
217228
private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array
218229
{
219230
$params = [];
@@ -230,6 +241,8 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
230241
*/
231242
private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */
232243
{
244+
assert(\is_array($this->container));
245+
233246
$type = $parameter->getType();
234247
$hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull());
235248

@@ -277,6 +290,7 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
277290
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
278291
}
279292

293+
// @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition
280294
return $this->loadObject($type->getName(), $depth - 1);
281295
}
282296

@@ -286,19 +300,21 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
286300
*/
287301
private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */
288302
{
289-
assert(\array_key_exists($name, $this->container) || isset($_SERVER[$name]));
303+
assert(\is_array($this->container) && (\array_key_exists($name, $this->container) || isset($_SERVER[$name])));
290304

291305
if (($this->container[$name] ?? null) instanceof \Closure) {
292306
if ($depth < 1) {
293307
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
294308
}
295309

296310
// build list of factory parameters based on parameter types
297-
$closure = new \ReflectionFunction($this->container[$name]);
311+
$factory = $this->container[$name];
312+
assert($factory instanceof \Closure);
313+
$closure = new \ReflectionFunction($factory);
298314
$params = $this->loadFunctionParams($closure, $depth - 1, true);
299315

300316
// invoke factory with list of parameters
301-
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);
317+
$value = $params === [] ? $factory() : $factory(...$params);
302318

303319
if (!\is_object($value) && !\is_scalar($value) && $value !== null) {
304320
throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . \gettype($value));

src/ErrorHandler.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ public function requestNotFound(): ResponseInterface
120120
);
121121
}
122122

123-
/** @internal */
123+
/**
124+
* @internal
125+
* @param list<string> $allowedMethods
126+
*/
124127
public function requestMethodNotAllowed(array $allowedMethods): ResponseInterface
125128
{
126129
$methods = \implode('/', \array_map(function (string $method) { return '<code>' . $method . '</code>'; }, $allowedMethods));
@@ -155,6 +158,7 @@ private function errorInvalidException(\Throwable $e): ResponseInterface
155158
);
156159
}
157160

161+
/** @param mixed $value */
158162
private function errorInvalidResponse($value): ResponseInterface
159163
{
160164
return $this->htmlResponse(
@@ -165,6 +169,7 @@ private function errorInvalidResponse($value): ResponseInterface
165169
);
166170
}
167171

172+
/** @param mixed $value */
168173
private function errorInvalidCoroutine($value, string $file, int $line): ResponseInterface
169174
{
170175
$where = ' near or before '. $this->where($file, $line) . '.';
@@ -192,6 +197,7 @@ private function htmlResponse(int $statusCode, string $title, string ...$info):
192197
);
193198
}
194199

200+
/** @param mixed $value */
195201
private function describeType($value): string
196202
{
197203
if ($value === null) {

src/FilesystemHandler.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
use FrameworkX\Io\HtmlHandler;
66
use FrameworkX\Io\RedirectHandler;
7+
use Psr\Http\Message\ResponseInterface;
78
use Psr\Http\Message\ServerRequestInterface;
89
use React\Http\Message\Response;
910

1011
class FilesystemHandler
1112
{
13+
/** @var string */
1214
private $root;
1315

1416
/**
@@ -59,9 +61,10 @@ public function __construct(string $root)
5961
$this->html = new HtmlHandler();
6062
}
6163

62-
public function __invoke(ServerRequestInterface $request)
64+
public function __invoke(ServerRequestInterface $request): ResponseInterface
6365
{
6466
$local = $request->getAttribute('path', '');
67+
assert(\is_string($local));
6568
$path = \rtrim($this->root . '/' . $local, '/');
6669

6770
// local path should not contain "./", "../", "//" or null bytes or start with slash
@@ -80,6 +83,7 @@ public function __invoke(ServerRequestInterface $request)
8083
}
8184

8285
$files = \scandir($path);
86+
// @phpstan-ignore-next-line TODO handle error if directory can not be accessed
8387
foreach ($files as $file) {
8488
if ($file === '.' || $file === '..') {
8589
continue;
@@ -117,7 +121,7 @@ public function __invoke(ServerRequestInterface $request)
117121
return new Response(
118122
Response::STATUS_OK,
119123
$headers,
120-
\file_get_contents($path)
124+
\file_get_contents($path) // @phpstan-ignore-line TODO handle error if file can not be accessed
121125
);
122126
} else {
123127
return $this->errorHandler->requestNotFound();

src/Io/FiberHandler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public function __invoke(ServerRequestInterface $request, callable $next): mixed
5656
$fiber->start();
5757
if ($fiber->isTerminated()) {
5858
/** @throws void because fiber is known to have terminated successfully */
59+
/** @var ResponseInterface|PromiseInterface|\Generator */
5960
return $fiber->getReturn();
6061
}
6162

src/Io/HtmlHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ public function statusResponse(int $statusCode, string $title, string $subtitle,
5050

5151
public function escape(string $s): string
5252
{
53-
return \preg_replace_callback(
53+
return (string) \preg_replace_callback(
5454
'/[\x00-\x1F]+/',
5555
function (array $match): string {
5656
return '<span>' . \addcslashes($match[0], "\x00..\xff") . '</span>';
5757
},
58-
\preg_replace(
58+
(string) \preg_replace(
5959
'/(^| ) |(?: $)/',
6060
'$1&nbsp;',
6161
\htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8')

src/Io/MiddlewareHandler.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,24 @@
99
*/
1010
class MiddlewareHandler
1111
{
12+
/** @var list<callable> $handlers */
1213
private $handlers;
1314

15+
/** @param list<callable> $handlers */
1416
public function __construct(array $handlers)
1517
{
1618
assert(count($handlers) >= 2);
1719

1820
$this->handlers = $handlers;
1921
}
2022

23+
/** @return mixed */
2124
public function __invoke(ServerRequestInterface $request)
2225
{
2326
return $this->call($request, 0);
2427
}
2528

29+
/** @return mixed */
2630
private function call(ServerRequestInterface $request, int $position)
2731
{
2832
if (!isset($this->handlers[$position + 2])) {

0 commit comments

Comments
 (0)