Skip to content

Commit

Permalink
Pipe example
Browse files Browse the repository at this point in the history
  • Loading branch information
klimick committed Jan 20, 2022
1 parent 0a81f8c commit 1d988ad
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 0 deletions.
84 changes: 84 additions & 0 deletions pipe-example.php
@@ -0,0 +1,84 @@
<?php

use Psalm\Tests\Config\Plugin\Hook\PipeFunctionPlugin;

/**
* @template A
* @template B
*
* @param callable(A): B $_ab
* @return callable(list<A>): list<B>
*/
function map(callable $_ab): callable
{
throw new RuntimeException('???');
}

/**
* @template A
*
* @param callable(A): bool $_predicate
* @return callable(list<A>): list<A>
*/
function filter(callable $_predicate): callable
{
throw new RuntimeException('???');
}

/**
* @return list<string>
*/
function getList(): array
{
return [];
}

function stringToInt(string $str): int
{
return (int) $str;
}

/**
* @psalm-immutable
*/
final class Greater10
{
public int $val;

public function __construct(int $val)
{
$this->val = $val;
}
}

/**
* @template T1
* @template T2
* @template T3
* @template T4
* @template T5
* @template T6
* @template T7
* @template T8
* @template T9
* @template T10
* @template T11
* @template T12
*
* @return mixed
* @no-named-arguments
*
* @see PipeFunctionPlugin
*/
function pipe(callable ...$_functions)
{
throw new RuntimeException('???');
}

/** @psalm-trace $_ */
$_ = pipe(
getList(),
map(fn($a) => stringToInt($a)),
filter(fn($a) => $a > 10),
map(fn($a) => new Greater10($a))
);
169 changes: 169 additions & 0 deletions psalm.xml
@@ -0,0 +1,169 @@
<?xml version="1.0"?>
<psalm
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
name="Psalm for Psalm"
useDocblockTypes="true"
errorLevel="1"
strictBinaryOperands="false"
rememberPropertyAssignmentsAfterCall="true"
checkForThrowsDocblock="false"
throwExceptionOnError="0"
findUnusedCode="true"
ensureArrayStringOffsetsExist="true"
ensureArrayIntOffsetsExist="true"
resolveFromConfigFile="true"
xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"
limitMethodComplexity="true"
errorBaseline="psalm-baseline.xml"
findUnusedPsalmSuppress="true"
>
<stubs>
<file name="stubs/phpparser.phpstub"/>
</stubs>
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
<directory name="examples"/>
<file name="psalm"/>
<file name="psalm-language-server"/>
<file name="psalm-plugin"/>
<file name="psalm-refactor"/>
<file name="psalter"/>
<ignoreFiles>
<file name="src/Psalm/Internal/PhpTraverser/CustomTraverser.php"/>
<file name="tests/ErrorBaselineTest.php"/>
<file name="vendor/symfony/console/Command/Command.php"/>
<directory name="tests/fixtures"/>
<file name="vendor/felixfbecker/advanced-json-rpc/lib/Dispatcher.php" />
<directory name="vendor/netresearch/jsonmapper" />
<directory name="vendor/phpunit" />
</ignoreFiles>
</projectFiles>

<ignoreExceptions>
<class name="UnexpectedValueException"/>
<class name="InvalidArgumentException"/>
<class name="LogicException"/>
</ignoreExceptions>

<plugins>
<plugin filename="examples/plugins/FunctionCasingChecker.php"/>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
<plugin filename="examples/plugins/InternalChecker.php"/>
<pluginClass class="Psalm\Tests\Config\Plugin\Hook\PipeFunctionPlugin"/>
</plugins>

<issueHandlers>
<PossiblyNullOperand errorLevel="suppress"/>

<DeprecatedMethod>
<errorLevel type="suppress">
<directory name="tests" />
</errorLevel>
</DeprecatedMethod>

<DeprecatedClass>
<errorLevel type="suppress">
<referencedClass name="PackageVersions\Versions"/>
</errorLevel>
</DeprecatedClass>

<UnusedParam>
<errorLevel type="suppress">
<directory name="examples"/>
</errorLevel>
</UnusedParam>

<PossiblyUnusedParam>
<errorLevel type="suppress">
<directory name="examples"/>
</errorLevel>
</PossiblyUnusedParam>

<UnusedClass>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="src/Psalm/Internal/Fork" />
<directory name="src/Psalm/Node" />
<file name="src/Psalm/Plugin/Shepherd.php" />
</errorLevel>
</UnusedClass>

<MissingConstructor>
<errorLevel type="suppress">
<directory name="tests"/>
</errorLevel>
</MissingConstructor>

<PossiblyUndefinedIntArrayOffset>
<errorLevel type="suppress">
<directory name="src/Psalm/Internal/ExecutionEnvironment" />
<directory name="tests"/>
</errorLevel>
</PossiblyUndefinedIntArrayOffset>

<MissingThrowsDocblock errorLevel="info"/>

<PossiblyUnusedProperty>
<errorLevel type="suppress">
<file name="src/Psalm/Report.php"/>
</errorLevel>
</PossiblyUnusedProperty>

<PossiblyUnusedMethod>
<errorLevel type="suppress">
<directory name="src/Psalm/Plugin"/>
<directory name="src/Psalm/SourceControl/Git/"/>
<file name="src/Psalm/Internal/LanguageServer/Client/TextDocument.php"/>
<file name="src/Psalm/Internal/LanguageServer/Server/TextDocument.php"/>
<referencedMethod name="Psalm\Codebase::getParentInterfaces"/>
<referencedMethod name="Psalm\Codebase::getMethodParams"/>
<referencedMethod name="Psalm\Codebase::getMethodReturnType"/>
<referencedMethod name="Psalm\Codebase::getMethodReturnTypeLocation"/>
<referencedMethod name="Psalm\Codebase::getDeclaringMethodId"/>
<referencedMethod name="Psalm\Codebase::getAppearingMethodId"/>
<referencedMethod name="Psalm\Codebase::getOverriddenMethodIds"/>
<referencedMethod name="Psalm\Codebase::getCasedMethodId"/>
<referencedMethod name="Psalm\Codebase::createClassLikeStorage"/>
<referencedMethod name="Psalm\Codebase::isVariadic"/>
<referencedMethod name="Psalm\Codebase::getMethodReturnsByRef"/>
</errorLevel>
</PossiblyUnusedMethod>

<InternalMethod>
<errorLevel type="suppress">
<directory name="tests"/>
</errorLevel>
</InternalMethod>

<PossiblyUndefinedStringArrayOffset>
<errorLevel type="suppress">
<directory name="src/Psalm/Internal/Provider/ReturnTypeProvider" />
<file name="src/Psalm/Internal/Type/AssertionReconciler.php" />
<file name="src/Psalm/Internal/Type/NegatedAssertionReconciler.php" />
<file name="src/Psalm/Internal/Type/SimpleAssertionReconciler.php" />
<file name="src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php" />
<directory name="tests"/>
</errorLevel>
</PossiblyUndefinedStringArrayOffset>

<MixedPropertyTypeCoercion>
<errorLevel type="suppress">
<directory name="vendor/nikic/php-parser" />
</errorLevel>
</MixedPropertyTypeCoercion>

<PropertyTypeCoercion>
<errorLevel type="suppress">
<directory name="vendor/nikic/php-parser" />
</errorLevel>
</PropertyTypeCoercion>

<MixedAssignment>
<errorLevel type="suppress">
<directory name="vendor/nikic/php-parser" />
</errorLevel>
</MixedAssignment>
</issueHandlers>
</psalm>
Expand Up @@ -571,6 +571,11 @@ private static function handleNamedFunction(

$function_call_info->function_params = $function_callable->params;
}
} else {
$function_call_info->function_storage = $codebase_functions->getStorage(
$statements_analyzer,
strtolower($function_call_info->function_id)
);
}

if ($codebase->store_node_types
Expand Down
111 changes: 111 additions & 0 deletions tests/Config/Plugin/Hook/PipeFunctionPlugin.php
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace Psalm\Tests\Config\Plugin\Hook;

use PhpParser\Node\Arg;
use Psalm\Plugin\EventHandler\Event\FunctionParamsProviderEvent;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use SimpleXMLElement;

use function array_map;
use function count;
use function range;

final class PipeFunctionPlugin implements FunctionParamsProviderInterface, FunctionReturnTypeProviderInterface, PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
{
$registration->registerHooksFromClass(self::class);
}

/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['pipe'];
}

/**
* @return ?array<int, FunctionLikeParameter>
*/
public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array
{
$args_count = count($event->getCallArgs());

if ($args_count < 2 || $args_count > 12) {
return null;
}

return [
// First input arg to pipe.
new FunctionLikeParameter('in', false, self::createTemplate()),
// Rest callable args.
// A -> B
// B -> C
// C -> D and etc...
...array_map(
fn(int $arg_offset) => new FunctionLikeParameter(
'fn' . $arg_offset,
false,
new Union([
new Type\Atomic\TCallable(
'callable',
[new FunctionLikeParameter('a', false, self::createTemplate($arg_offset - 1))],
self::createTemplate($arg_offset),
),
])
),
range(2, $args_count),
),
];
}

private static function createTemplate(int $offset = 1): Union
{
return new Union([
new TTemplateParam('T' . $offset, Type::getMixed(), 'fn-pipe')
]);
}

public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
$source = $event->getStatementsSource();
$type_provider = $source->getNodeTypeProvider();

$args = $event->getStmt()->args;
$args_count = count($args);

if ($args_count < 2 || $args_count > 12) {
return null;
}

// Try to fetch return type from last callable arg.
$last_callable_arg = $args[$args_count - 1];

if (!($last_callable_arg instanceof Arg) ||
!($type = $type_provider->getType($last_callable_arg->value)) ||
!($type->isSingle())
) {
return null;
}

$atomic = $type->getSingleAtomic();

if (!$atomic instanceof Type\Atomic\TCallable && !$atomic instanceof Type\Atomic\TClosure) {
return null;
}

return $atomic->return_type;
}
}

0 comments on commit 1d988ad

Please sign in to comment.