Skip to content

Commit 3ecb761

Browse files
committed
feature: tighten return types of config helper by using dynamic analysis
1 parent c82b7c7 commit 3ecb761

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace Psalm\LaravelPlugin\Handlers\Helpers;
4+
5+
use Illuminate\Config\Repository;
6+
use Psalm\LaravelPlugin\Providers\ApplicationProvider;
7+
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
8+
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
9+
use Psalm\Type\Atomic\TArray;
10+
use Psalm\Type\Atomic\TArrayKey;
11+
use Psalm\Type\Atomic\TBool;
12+
use Psalm\Type\Atomic\TClosedResource;
13+
use Psalm\Type\Atomic\TFloat;
14+
use Psalm\Type\Atomic\TLiteralFloat;
15+
use Psalm\Type\Atomic\TLiteralInt;
16+
use Psalm\Type\Atomic\TLiteralString;
17+
use Psalm\Type\Atomic\TMixed;
18+
use Psalm\Type\Atomic\TNamedObject;
19+
use Psalm\Type\Atomic\TNull;
20+
use Psalm\Type\Atomic\TResource;
21+
use Psalm\Type\Union;
22+
23+
use function gettype;
24+
use function get_class;
25+
26+
class ConfigHandler implements FunctionReturnTypeProviderInterface
27+
{
28+
public static function getFunctionIds(): array
29+
{
30+
return ['config'];
31+
}
32+
33+
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
34+
{
35+
// we're going to attempt some dynamic analysis to tighten the actual return type here.
36+
// this could be done statically, but it's quicker + easier to do this dynamically.
37+
// PRs to make this static in the future more than welcome!
38+
$call_args = $event->getCallArgs();
39+
if (!isset($call_args[0])) {
40+
return new Union([
41+
new TNamedObject(Repository::class),
42+
]);
43+
}
44+
45+
$argumentType = $call_args[0]->value;
46+
47+
if (!$argumentType->value) {
48+
return null;
49+
}
50+
51+
$argumentValue = $argumentType->value;
52+
53+
try {
54+
// dynamic analysis
55+
$returnValue = ApplicationProvider::getApp()->make('config')->get($argumentValue);
56+
} catch (\Throwable $t) {
57+
return null;
58+
}
59+
60+
// turn actual return value into a psalm type. there's probably a helper in psalm to do this, but i couldn't find one
61+
switch (gettype($returnValue)) {
62+
case 'boolean':
63+
$type = new TBool();
64+
break;
65+
case 'integer':
66+
$type = new TLiteralInt($returnValue);
67+
break;
68+
case 'double':
69+
$type = new TLiteralFloat($returnValue);
70+
break;
71+
case 'string':
72+
$type = new TLiteralString($returnValue);
73+
break;
74+
case 'array':
75+
$type = new TArray([
76+
new Union([new TArrayKey()]),
77+
new Union([new TMixed()]),
78+
]);
79+
break;
80+
case 'object':
81+
$type = new TNamedObject(get_class($returnValue));
82+
break;
83+
case 'resource':
84+
$type = new TResource();
85+
break;
86+
case 'resource (closed)':
87+
$type = new TClosedResource();
88+
break;
89+
case 'NULL':
90+
if (isset($call_args[1])) {
91+
return $event->getStatementsSource()->getNodeTypeProvider()->getType($call_args[1]->value);
92+
}
93+
$type = new TNull();
94+
break;
95+
case 'unknown type':
96+
default:
97+
$type = new TMixed();
98+
break;
99+
}
100+
101+
return new Union([
102+
$type,
103+
]);
104+
}
105+
}

src/Plugin.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Psalm\LaravelPlugin\Handlers\Eloquent\ModelPropertyAccessorHandler;
1010
use Psalm\LaravelPlugin\Handlers\Eloquent\ModelRelationshipPropertyHandler;
1111
use Psalm\LaravelPlugin\Handlers\Eloquent\RelationsMethodHandler;
12+
use Psalm\LaravelPlugin\Handlers\Helpers\ConfigHandler;
1213
use Psalm\LaravelPlugin\Handlers\Helpers\PathHandler;
1314
use Psalm\LaravelPlugin\Handlers\Helpers\RedirectHandler;
1415
use Psalm\LaravelPlugin\Handlers\Helpers\TransHandler;
@@ -99,6 +100,8 @@ private function registerHandlers(RegistrationInterface $registration): void
99100
$registration->registerHooksFromClass(RedirectHandler::class);
100101
require_once 'Handlers/SuppressHandler.php';
101102
$registration->registerHooksFromClass(SuppressHandler::class);
103+
require_once 'Handlers/Helpers/ConfigHandler.php';
104+
$registration->registerHooksFromClass(ConfigHandler::class);
102105
}
103106

104107
private function generateStubFiles(): void
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Feature: Config helper
2+
The global config helper will return a strict type
3+
4+
Background:
5+
Given I have the following config
6+
"""
7+
<?xml version="1.0"?>
8+
<psalm errorLevel="1">
9+
<projectFiles>
10+
<directory name="."/>
11+
<ignoreFiles> <directory name="../../vendor"/> </ignoreFiles>
12+
</projectFiles>
13+
<plugins>
14+
<pluginClass class="Psalm\LaravelPlugin\Plugin"/>
15+
</plugins>
16+
</psalm>
17+
"""
18+
And I have the following code preamble
19+
"""
20+
<?php declare(strict_types=1);
21+
22+
"""
23+
24+
Scenario: config with no arguments returns a repository instance
25+
Given I have the following code
26+
"""
27+
function test(): \Illuminate\Config\Repository {
28+
return config();
29+
}
30+
"""
31+
When I run Psalm
32+
Then I see no errors
33+
34+
Scenario: config with one argument
35+
Given I have the following code
36+
"""
37+
function test(): string
38+
{
39+
return config('app.name');
40+
}
41+
"""
42+
When I run Psalm
43+
Then I see no errors
44+
45+
Scenario: config with first null argument and second argument provided
46+
Given I have the following code
47+
"""
48+
function test(): bool
49+
{
50+
return config('app.non-existent', false);
51+
}
52+
"""
53+
When I run Psalm
54+
Then I see no errors

0 commit comments

Comments
 (0)