allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s

This commit is contained in:
2026-03-30 14:54:57 +07:00
parent 66aed7c4e8
commit b5e3a778ce
21316 changed files with 2777892 additions and 13 deletions

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function str_contains;
use function str_starts_with;
/**
* @implements Rule<InClassNode>
*/
final class ApiAndInternalAnnotationCheck implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}
public function processNode(Node $node, Scope $scope): array
{
$reflection = $scope->getClassReflection();
if (! $reflection) {
return [];
}
if ($reflection->isAnonymous()) {
return [];
}
if (str_contains($reflection->getFileName() ?? '', '/tests/')) {
return [];
}
if (str_starts_with($reflection->getName(), 'CuyZ\Valinor\QA')) {
return [];
}
if (! preg_match('/@(api|internal)\s+/', $reflection->getResolvedPhpDoc()?->getPhpDocString() ?? '')) {
return [
RuleErrorBuilder::message(
'Missing annotation `@api` or `@internal`.'
)->identifier('valinor.apiOrInternalAnnotation')->build(),
];
}
return [];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use function count;
final class ArgumentsMapperPHPStanExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return ArgumentsMapper::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'mapArguments';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if (! $type->isCallable()->yes()) {
return new MixedType();
}
$acceptors = $type->getCallableParametersAcceptors($scope);
if (count($acceptors) !== 1) {
return new MixedType();
}
$parameters = $acceptors[0]->getParameters();
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($parameters as $parameter) {
$builder->setOffsetValueType(new ConstantStringType($parameter->getName()), $parameter->getType(), $parameter->isOptional());
}
return $builder->getArray();
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\NormalizerBuilder;
use CuyZ\Valinor\Utility\Polyfill;
use PhpParser\Node;
use PHPStan\Analyser\Error;
use PHPStan\Analyser\IgnoreErrorExtension;
use PHPStan\Analyser\Scope;
use function in_array;
/**
* This extension should not be used by default, as it suppresses errors that
* are typically indicative of purity issues in the codebase. It is advised to
* first address the root causes of these errors before enabling this extension.
*
* When enabled, it will ignore all purity-related errors of method calls in the
* context of `MapperBuilder` and `NormalizerBuilder`.
*/
final class SuppressPureErrors implements IgnoreErrorExtension
{
public function shouldIgnore(Error $error, Node $node, Scope $scope): bool
{
if ($error->getIdentifier() !== 'argument.type') {
return false;
}
if (! $node instanceof Node\Expr\MethodCall) {
return false;
}
$type = $scope->getType($node->var);
if (! $type->isObject()->yes()) {
return false;
}
if (! Polyfill::array_find($type->getObjectClassNames(), fn (string $className) => $className === MapperBuilder::class || $className === NormalizerBuilder::class)) {
return false;
}
if (! $node->name instanceof Node\Identifier) {
return false;
}
$methodName = $node->name->toString();
if (! in_array($methodName, [
'infer',
'registerConstructor',
'registerConverter',
'registerTransformer',
], true)) {
return false;
}
if ($node->args === []) {
return false;
}
$callableArgumentIndex = [
'infer' => 1,
'registerConstructor' => 0,
'registerConverter' => 0,
'registerTransformer' => 0,
][$methodName];
$argument = $node->args[$callableArgumentIndex];
if (! $argument instanceof Node\Arg) {
return false;
}
$argumentType = $scope->getType($argument->value);
if ($argumentType->isCallable()->yes()) {
return true;
}
if ($argumentType->isArray()->yes() && $argumentType->getIterableValueType()->isCallable()->yes()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\PHPStan\Extension;
use CuyZ\Valinor\Mapper\TreeMapper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\PhpDocParser\Parser\ParserException;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function implode;
use function method_exists;
final class TreeMapperPHPStanExtension implements DynamicMethodReturnTypeExtension
{
public function __construct(private TypeStringResolver $resolver) {}
public function getClass(): string
{
return TreeMapper::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'map';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
$arguments = $methodCall->getArgs();
if (count($arguments) === 0) {
return new MixedType();
}
$type = $scope->getType($arguments[0]->value);
if ($type instanceof UnionType) {
return $type->traverse(fn (Type $type) => $this->type($type));
}
try {
return $this->type($type);
} catch (ParserException) {
// Fallback to `mixed` type if the type cannot be resolved. This can
// occur with a type that is not understood/supported by PHPStan. If
// that happens, returning a mixed type is the safest option, as it
// will not make the analysis fail.
return new MixedType();
}
}
private function type(Type $type): Type
{
if ($type->isConstantValue()->yes()) {
$value = implode('', $type->getConstantScalarValues());
return $this->resolver->resolve($value);
}
// @phpstan-ignore function.alreadyNarrowedType (support for PHPStan v1)
if (method_exists($type, 'isClassString') && $type->isClassString()->yes()) {
return $type->getClassStringObjectType();
}
// @phpstan-ignore method.nonObject (support for PHPStan v1)
if (method_exists($type, 'isClassStringType') && $type->isClassStringType()->yes()) {
return $type->getClassStringObjectType();
}
return new MixedType();
}
}

View File

@@ -0,0 +1,19 @@
<?php
use CuyZ\Valinor\QA\PHPStan\Extension\ArgumentsMapperPHPStanExtension;
use CuyZ\Valinor\QA\PHPStan\Extension\TreeMapperPHPStanExtension;
require_once 'Extension/ArgumentsMapperPHPStanExtension.php';
require_once 'Extension/TreeMapperPHPStanExtension.php';
return [
'services' => [
[
'class' => TreeMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
], [
'class' => ArgumentsMapperPHPStanExtension::class,
'tags' => ['phpstan.broker.dynamicMethodReturnTypeExtension']
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
use CuyZ\Valinor\QA\PHPStan\Extension\SuppressPureErrors;
require_once 'Extension/SuppressPureErrors.php';
return [
'services' => [
[
'class' => SuppressPureErrors::class,
'tags' => ['phpstan.ignoreErrorExtension'],
],
],
];

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\Psalm\Plugin;
use CuyZ\Valinor\Mapper\ArgumentsMapper;
use Psalm\Internal\Type\Comparator\CallableTypeComparator;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Union;
use function count;
use function reset;
final class ArgumentsMapperPsalmPlugin implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return [ArgumentsMapper::class];
}
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
if ($event->getMethodNameLowercase() !== 'maparguments') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$types = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if ($types === null) {
return null;
}
$types = $types->getAtomicTypes();
if (count($types) !== 1) {
return null;
}
$type = reset($types);
if ($type instanceof TKeyedArray) {
// Internal class usage, see https://github.com/vimeo/psalm/issues/8726
$type = CallableTypeComparator::getCallableFromAtomic($event->getSource()->getCodebase(), $type);
}
if (! $type instanceof TClosure) {
return null;
}
$typeParams = $type->params ?? [];
if ($typeParams === []) {
return null;
}
$params = [];
foreach ($typeParams as $param) {
$params[$param->name] = $param->type ?? new Union([new TMixed()]);
}
return new Union([new TKeyedArray($params)]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\QA\Psalm\Plugin;
use CuyZ\Valinor\Mapper\TreeMapper;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TDependentGetClass;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Union;
final class TreeMapperPsalmPlugin implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return [TreeMapper::class];
}
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{
if ($event->getMethodNameLowercase() !== 'map') {
return null;
}
$arguments = $event->getCallArgs();
if (count($arguments) === 0) {
return null;
}
$type = $event->getSource()->getNodeTypeProvider()->getType($arguments[0]->value);
if (! $type) {
return null;
}
$types = [];
foreach ($type->getAtomicTypes() as $node) {
$inferred = self::type($node);
if ($inferred === null) {
return null;
}
$types[] = $inferred;
}
return Type::combineUnionTypeArray($types, $event->getSource()->getCodebase());
}
private static function type(Atomic $node): ?Union
{
return match (true) {
$node instanceof TLiteralString => Type::parseString($node->value),
$node instanceof TDependentGetClass => $node->as_type,
$node instanceof TClassString && $node->as_type => new Union([$node->as_type]),
default => null,
};
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace CuyZ\Valinor\QA\Psalm;
use CuyZ\Valinor\QA\Psalm\Plugin\ArgumentsMapperPsalmPlugin;
use CuyZ\Valinor\QA\Psalm\Plugin\TreeMapperPsalmPlugin;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use SimpleXMLElement;
class ValinorPsalmPlugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $api, ?SimpleXMLElement $config = null): void
{
require_once __DIR__ . '/Plugin/TreeMapperPsalmPlugin.php';
require_once __DIR__ . '/Plugin/ArgumentsMapperPsalmPlugin.php';
$api->registerHooksFromClass(TreeMapperPsalmPlugin::class);
$api->registerHooksFromClass(ArgumentsMapperPsalmPlugin::class);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<issueHandlers xmlns="https://getpsalm.org/schema/config">
<InvalidArgument>
<errorLevel type="suppress">
<referencedFunction name="CuyZ\Valinor\NormalizerBuilder::registerTransformer"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::infer"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConstructor"/>
<referencedFunction name="\CuyZ\Valinor\MapperBuilder::registerConverter"/>
</errorLevel>
</InvalidArgument>
</issueHandlers>