[TOOLS] Raise PHPStan level to 5 and fix associated error, fixing some bugs in the process

This commit is contained in:
Hugo Sales 2022-10-19 22:38:49 +01:00
parent edeee49af9
commit fed2242a56
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
16 changed files with 139 additions and 108 deletions

View File

@ -104,7 +104,6 @@ class EditFeeds extends Controller
$feed->setUrl($fd[$md5 . '-url']); $feed->setUrl($fd[$md5 . '-url']);
$feed->setOrdering($fd[$md5 . '-order']); $feed->setOrdering($fd[$md5 . '-order']);
$feed->setTitle($fd[$md5 . '-title']); $feed->setTitle($fd[$md5 . '-title']);
DB::merge($feed);
} }
DB::flush(); DB::flush();
Cache::delete($key); Cache::delete($key);

View File

@ -1,5 +1,5 @@
parameters: parameters:
level: 4 level: 5
bootstrapFiles: bootstrapFiles:
- config/bootstrap.php - config/bootstrap.php
paths: paths:

View File

@ -112,7 +112,9 @@ class Cover
DB::flush(); DB::flush();
// Only delete files if the commit went through // Only delete files if the commit went through
if ($old_file != null) { if ($old_file != null) {
@unlink($old_file); foreach ($old_file as $f) {
@unlink($f);
}
} }
throw new RedirectException(); throw new RedirectException();
} }
@ -128,7 +130,9 @@ class Cover
$old_file = $cover->delete(); $old_file = $cover->delete();
DB::remove($cover); DB::remove($cover);
DB::flush(); DB::flush();
@unlink($old_file); foreach ($old_file as $f) {
@unlink($f);
}
throw new RedirectException(); throw new RedirectException();
} }
$removeForm = $form2->createView(); $removeForm = $form2->createView();

View File

@ -170,7 +170,7 @@ class Oomox
if ($reset_button->isClicked()) { if ($reset_button->isClicked()) {
DB::remove(EntityOomox::getByPK($actor_id)); DB::remove(EntityOomox::getByPK($actor_id));
} else { } else {
DB::merge($current_oomox_settings); DB::persist($current_oomox_settings);
} }
DB::flush(); DB::flush();
} }
@ -219,7 +219,7 @@ class Oomox
if ($reset_button->isClicked()) { if ($reset_button->isClicked()) {
DB::remove(EntityOomox::getByPK($actor_id)); DB::remove(EntityOomox::getByPK($actor_id));
} else { } else {
DB::merge($current_oomox_settings); DB::persist($current_oomox_settings);
} }
DB::flush(); DB::flush();
} }

View File

@ -73,7 +73,7 @@ class APIv1 extends Controller
// dd($tag_names, $keys, $result, $xml, (string) $xml); // dd($tag_names, $keys, $result, $xml, (string) $xml);
// $xml->addChild(); // $xml->addChild();
// dd($xml); // dd($xml);
return new Response(content: $xml, status: $status); return new Response(content: (string) $xml, status: $status);
} else { } else {
throw new InvalidRequestException; throw new InvalidRequestException;
} }

View File

@ -78,6 +78,7 @@ class EditBlocked extends Controller
Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]);
switch ($type) { switch ($type) {
case 'toggle-canon': case 'toggle-canon':
/** @var NoteTagBlock $ntb */
$ntb = DB::getReference($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]); $ntb = DB::getReference($block_class, ['blocker' => $user->getId(), 'canonical' => $canon]);
$ntb->setUseCanonical(!$ntb->getUseCanonical()); $ntb->setUseCanonical(!$ntb->getUseCanonical());
DB::flush(); DB::flush();

View File

@ -23,7 +23,6 @@ declare(strict_types = 1);
namespace App\Core; namespace App\Core;
use App\Core\DB;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Entity\Note; use App\Entity\Note;
@ -32,6 +31,7 @@ use App\Util\Exception\ConfigurationException;
use App\Util\Exception\NotImplementedException; use App\Util\Exception\NotImplementedException;
use Functional as F; use Functional as F;
use InvalidArgumentException; use InvalidArgumentException;
use Memcached;
use Redis; use Redis;
use RedisCluster; use RedisCluster;
use Symfony\Component\Cache\Adapter; use Symfony\Component\Cache\Adapter;
@ -77,12 +77,13 @@ abstract class Cache
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
// This requires extra server configuration, but the code was tested // This requires extra server configuration, but the code was tested
// manually and works, so it'll be excluded from automatic tests, for now, at least // manually and works, so it'll be excluded from automatic tests, for now, at least
if (F\Every($dsns, function ($str) { [$scheme, $rest] = explode('://', $str); return str_contains($rest, ':'); }) == false) { if (F\Every($dsns, function ($str) { [$scheme, $rest] = explode('://', $str);
return str_contains($rest, ':'); }) == false) {
throw new ConfigurationException('The configuration of a redis cluster requires specifying the ports to use'); throw new ConfigurationException('The configuration of a redis cluster requires specifying the ports to use');
} }
$class = RedisCluster::class; // true for persistent connection $class = RedisCluster::class; // true for persistent connection
$seeds = F\Map($dsns, fn ($str) => explode('://', $str)[1]); $seeds = F\Map($dsns, fn ($str) => explode('://', $str)[1]);
$r = new RedisCluster(name: null, seeds: $seeds, timeout: null, readTimeout: null, persistent: true); $r = new RedisCluster(name: null, seeds: $seeds, timeout: 0.0, readTimeout: 0.0, persistent: true);
// Distribute reads randomly // Distribute reads randomly
$r->setOption($class::OPT_SLAVE_FAILOVER, $class::FAILOVER_DISTRIBUTE); $r->setOption($class::OPT_SLAVE_FAILOVER, $class::FAILOVER_DISTRIBUTE);
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
@ -101,8 +102,12 @@ abstract class Cache
// These all are excluded from automatic testing, as they require an unreasonable amount // These all are excluded from automatic testing, as they require an unreasonable amount
// of configuration in the testing environment. The code is really simple, so it should work // of configuration in the testing environment. The code is really simple, so it should work
// memcached can also have multiple servers // memcached can also have multiple servers
// host:port:weight?
$dsns = explode(';', $dsn); $dsns = explode(';', $dsn);
$adapters[$pool][] = new Adapter\MemcachedAdapter($dsns); $servers = F\map($dsns, fn ($dsn) => explode(':', $dsn));
$memcached = new Memcached();
$memcached->addServers($servers);
$adapters[$pool][] = new Adapter\MemcachedAdapter($memcached);
break; break;
case 'filesystem': case 'filesystem':
$adapters[$pool][] = new Adapter\FilesystemAdapter($rest); $adapters[$pool][] = new Adapter\FilesystemAdapter($rest);
@ -434,6 +439,7 @@ abstract class Cache
* for redis and others. Different from lists, works with string map_keys * for redis and others. Different from lists, works with string map_keys
* *
* @param callable(?CacheItem $item, bool &$save): (string|object|array<string,mixed>) $calculate * @param callable(?CacheItem $item, bool &$save): (string|object|array<string,mixed>) $calculate
*
* @TODO cleanup * @TODO cleanup
*/ */
public static function getHashMap(string $map_key, callable $calculate, string $pool = 'default', float $beta = 1.0): array public static function getHashMap(string $map_key, callable $calculate, string $pool = 'default', float $beta = 1.0): array

View File

@ -40,17 +40,18 @@ use App\Util\Formatting;
use Closure; use Closure;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder;
use Exception; use Exception;
use Functional as F; use Functional as F;
/** /**
* @mixin EntityManager
*
* @template T of Entity * @template T of Entity
* *
* Finds an Entity by its identifier. You probably want to use DB::findBy instead. * Finds an Entity by its identifier. You probably want to use DB::findBy instead.
@ -77,10 +78,14 @@ use Functional as F;
* @method static void flush() * @method static void flush()
* *
* Executes a function in a transaction. Warning: suppresses exceptions. Returns the result of the callable. * Executes a function in a transaction. Warning: suppresses exceptions. Returns the result of the callable.
* @method mixed wrapInTransaction(callable $func) * @method static mixed wrapInTransaction(callable $func)
* *
* Refetch the given entity * Refetch the given entity
* @method static T refetch(T $entity) * @method static T refetch(T $entity)
* @method static QueryBuilder createQueryBuilder()
* @method static Connection getConnection()
* @method static EntityRepository getRepository(string $table)
* @method static ClassMetadata<T> getClassMetadata(string $table)
*/ */
class DB class DB
{ {
@ -317,8 +322,10 @@ class DB
$seqName = $metadata->getSequenceName($conn->getDatabasePlatform()); $seqName = $metadata->getSequenceName($conn->getDatabasePlatform());
self::persist($owner); self::persist($owner);
$id = (int) $conn->lastInsertId($seqName); $id = (int) $conn->lastInsertId($seqName);
F\map(\is_array($others) ? $others : [$others], function ($o) use ($id) { $o->setId($id); F\map(\is_array($others) ? $others : [$others], function ($o) use ($id) {
self::persist($o); }); $o->setId($id);
self::persist($o);
});
if (!\is_null($extra)) { if (!\is_null($extra)) {
$extra($id); $extra($id);
} }

View File

@ -37,7 +37,7 @@ use Functional as F;
/** /**
* Base class to all entities, with some utilities * Base class to all entities, with some utilities
* *
* @method int getId() // Not strictly true * @method int getId() // Not strictly true FIXME
*/ */
abstract class Entity abstract class Entity
{ {
@ -67,12 +67,14 @@ abstract class Entity
* Create an instance of the called class or fill in the * Create an instance of the called class or fill in the
* properties of $obj with the associative array $args. Doesn't * properties of $obj with the associative array $args. Doesn't
* persist the result * persist the result
*
* @return static
*/ */
public static function create(array $args, bool $_delegated_call = false): static public static function create(array $args, bool $_delegated_call = false): self
{ {
$date = new DateTime(); $date = new DateTime();
$class = static::class; $class = static::class;
$obj = new $class(); $obj = new $class;
foreach (['created', 'modified'] as $prop) { foreach (['created', 'modified'] as $prop) {
if (property_exists($class, $prop) && !isset($args[$prop])) { if (property_exists($class, $prop) && !isset($args[$prop])) {
$args[$prop] = $date; $args[$prop] = $date;
@ -87,9 +89,9 @@ abstract class Entity
} }
/** /**
* @param ?static $obj * @return static
*/ */
public static function createOrUpdate(?self $obj, array $args, bool $_delegated_call = false): static public static function createOrUpdate(?self $obj, array $args, bool $_delegated_call = false): self
{ {
$date = new DateTime(); $date = new DateTime();
$class = static::class; $class = static::class;
@ -100,7 +102,7 @@ abstract class Entity
} }
if (\is_null($obj)) { if (\is_null($obj)) {
$obj = static::create($args, _delegated_call: true); $obj = $class::create($args, _delegated_call: true);
} }
foreach ($args as $prop => $val) { foreach ($args as $prop => $val) {
@ -112,6 +114,7 @@ abstract class Entity
} }
} }
// @phpstan-ignore-next-line
return $obj; return $obj;
} }

View File

@ -64,7 +64,6 @@ class Extension extends AbstractExtension
new TwigFunction('active', [Runtime::class, 'isCurrentRouteActive']), new TwigFunction('active', [Runtime::class, 'isCurrentRouteActive']),
new TwigFunction('config', [Runtime::class, 'getConfig']), new TwigFunction('config', [Runtime::class, 'getConfig']),
new TwigFunction('dd', 'dd'), new TwigFunction('dd', 'dd'),
new TwigFunction('die', 'die'),
new TwigFunction('get_profile_actions', [Runtime::class, 'getProfileActions']), new TwigFunction('get_profile_actions', [Runtime::class, 'getProfileActions']),
new TwigFunction('get_extra_note_actions', [Runtime::class, 'getExtraNoteActions']), new TwigFunction('get_extra_note_actions', [Runtime::class, 'getExtraNoteActions']),
new TwigFunction('get_feeds', [Runtime::class, 'getFeeds']), new TwigFunction('get_feeds', [Runtime::class, 'getFeeds']),

View File

@ -206,12 +206,11 @@ abstract class Formatting
/** /**
* Convert a user supplied string to array and return whether the conversion was successful * Convert a user supplied string to array and return whether the conversion was successful
*
* @param static::SPLIT_BY_BOTH|static::SPLIT_BY_COMMA|static::SPLIT_BY_SPACE $split_type
*/ */
public static function toArray(string $input, &$output, string $split_type = self::SPLIT_BY_COMMA): bool public static function toArray(string $input, &$output, string $split_type = self::SPLIT_BY_COMMA): bool
{ {
if (!\in_array($split_type, [static::SPLIT_BY_SPACE, static::SPLIT_BY_COMMA, static::SPLIT_BY_BOTH])) {
throw new Exception('Formatting::toArray received invalid split option');
}
if ($input == '') { if ($input == '') {
$output = []; $output = [];
return true; return true;
@ -226,7 +225,7 @@ abstract class Formatting
$arr = preg_split('/, ?/', $matches[1]); $arr = preg_split('/, ?/', $matches[1]);
break; break;
default: default:
$arr = explode($split_type[0], $matches[1]); $arr = explode(self::SPLIT_BY_SPACE, $matches[1]);
} }
$output = str_replace([' \'', '\'', ' "', '"'], '', $arr); $output = str_replace([' \'', '\'', ' "', '"'], '', $arr);
$output = F\map($output, F\ary('trim', 1)); $output = F\map($output, F\ary('trim', 1));

View File

@ -207,7 +207,13 @@ class TemporaryFile extends SplFileInfo
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); set_error_handler(
function (int $errno, string $msg, string $errfile, int $errline, array $errcontext = []) use (&$error): bool {
$error = $msg;
return $msg === '';
},
);
$renamed = rename($this->getPathname(), $destpath); $renamed = rename($this->getPathname(), $destpath);
$chmoded = chmod($destpath, $filemode); $chmoded = chmod($destpath, $filemode);
restore_error_handler(); restore_error_handler();

View File

@ -31,6 +31,13 @@ use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
class CacheTest extends GNUsocialTestCase class CacheTest extends GNUsocialTestCase
{ {
// Needs to return something to conform to the interface
private function notCalled(mixed $i): array
{
static::assertFalse('should not be called');
return [];
}
private function doTest(array $adapters, $result_pool, $throws = null, $recompute = \INF) private function doTest(array $adapters, $result_pool, $throws = null, $recompute = \INF)
{ {
static::bootKernel(); static::bootKernel();
@ -105,15 +112,15 @@ class CacheTest extends GNUsocialTestCase
static::assertSame([], Cache::getList($key . '0', fn ($i) => [])); static::assertSame([], Cache::getList($key . '0', fn ($i) => []));
static::assertSame(['foo'], Cache::getList($key . '1', fn ($i) => ['foo'])); static::assertSame(['foo'], Cache::getList($key . '1', fn ($i) => ['foo']));
static::assertSame(['foo', 'bar'], Cache::getList($key, fn ($i) => ['foo', 'bar'])); static::assertSame(['foo', 'bar'], Cache::getList($key, fn ($i) => ['foo', 'bar']));
static::assertSame(['foo', 'bar'], Cache::getList($key, function () { $this->assertFalse('should not be called'); })); // Repeat to test no recompute lrange static::assertSame(['foo', 'bar'], Cache::getList($key, [$this, 'notCalled'])); // Repeat to test no recompute lrange
Cache::listPushLeft($key, 'quux'); Cache::listPushLeft($key, 'quux');
static::assertSame(['quux', 'foo', 'bar'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); })); static::assertSame(['quux', 'foo', 'bar'], Cache::getList($key, [$this, 'notCalled']));
Cache::listPushLeft($key, 'foobar', max_count: 2); Cache::listPushLeft($key, 'foobar', max_count: 2);
static::assertSame(['foobar', 'quux'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); })); static::assertSame(['foobar', 'quux'], Cache::getList($key, [$this, 'notCalled']));
Cache::listPushRight($key, 'foo'); Cache::listPushRight($key, 'foo');
static::assertSame(['foobar', 'quux', 'foo'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); })); static::assertSame(['foobar', 'quux', 'foo'], Cache::getList($key, [$this, 'notCalled']));
Cache::listPushRight($key, 'bar', max_count: 2); Cache::listPushRight($key, 'bar', max_count: 2);
static::assertSame(['foo', 'bar'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); })); static::assertSame(['foo', 'bar'], Cache::getList($key, [$this, 'notCalled']));
static::assertTrue(Cache::deleteList($key)); static::assertTrue(Cache::deleteList($key));
} }
@ -135,15 +142,15 @@ class CacheTest extends GNUsocialTestCase
static::assertSame([], Cache::getList($key . '0', fn ($i) => [], pool: 'file')); static::assertSame([], Cache::getList($key . '0', fn ($i) => [], pool: 'file'));
static::assertSame(['foo'], Cache::getList($key . '1', fn ($i) => ['foo'], pool: 'file')); static::assertSame(['foo'], Cache::getList($key . '1', fn ($i) => ['foo'], pool: 'file'));
static::assertSame(['foo', 'bar'], Cache::getList($key, fn ($i) => ['foo', 'bar'], pool: 'file')); static::assertSame(['foo', 'bar'], Cache::getList($key, fn ($i) => ['foo', 'bar'], pool: 'file'));
static::assertSame(['foo', 'bar'], Cache::getList($key, function () { $this->assertFalse('should not be called'); }, pool: 'file')); // Repeat to test no recompute lrange static::assertSame(['foo', 'bar'], Cache::getList($key, [$this, 'notCalled'], pool: 'file')); // Repeat to test no recompute lrange
Cache::listPushLeft($key, 'quux', pool: 'file'); Cache::listPushLeft($key, 'quux', pool: 'file');
static::assertSame(['quux', 'foo', 'bar'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); }, pool: 'file')); static::assertSame(['quux', 'foo', 'bar'], Cache::getList($key, [$this, 'notCalled'], pool: 'file'));
Cache::listPushLeft($key, 'foobar', max_count: 2, pool: 'file'); Cache::listPushLeft($key, 'foobar', max_count: 2, pool: 'file');
static::assertSame(['foobar', 'quux'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); }, pool: 'file')); static::assertSame(['foobar', 'quux'], Cache::getList($key, [$this, 'notCalled'], pool: 'file'));
Cache::listPushRight($key, 'foo', pool: 'file'); Cache::listPushRight($key, 'foo', pool: 'file');
static::assertSame(['foobar', 'quux', 'foo'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); }, pool: 'file')); static::assertSame(['foobar', 'quux', 'foo'], Cache::getList($key, [$this, 'notCalled'], pool: 'file'));
Cache::listPushRight($key, 'bar', max_count: 2, pool: 'file'); Cache::listPushRight($key, 'bar', max_count: 2, pool: 'file');
static::assertSame(['foo', 'bar'], Cache::getList($key, function ($i) { $this->assertFalse('should not be called'); }, pool: 'file')); static::assertSame(['foo', 'bar'], Cache::getList($key, [$this, 'notCalled'], pool: 'file'));
static::assertTrue(Cache::deleteList($key, pool: 'file')); static::assertTrue(Cache::deleteList($key, pool: 'file'));
} }
} }

View File

@ -34,13 +34,13 @@ class ArrayTransformerTest extends WebTestCase
{ {
static::assertSame('', (new ArrayTransformer)->transform([])); static::assertSame('', (new ArrayTransformer)->transform([]));
static::assertSame('foo bar quux', (new ArrayTransformer)->transform(['foo', 'bar', 'quux'])); static::assertSame('foo bar quux', (new ArrayTransformer)->transform(['foo', 'bar', 'quux']));
static::assertThrows(TransformationFailedException::class, fn () => (new ArrayTransformer)->transform('')); static::assertThrows(TransformationFailedException::class, fn () => (new ArrayTransformer)->transform('')); // @phpstan-ignore-line
} }
public function testReverseTransform() public function testReverseTransform()
{ {
static::assertSame([], (new ArrayTransformer)->reverseTransform('')); static::assertSame([], (new ArrayTransformer)->reverseTransform(''));
static::assertSame(['foo', 'bar', 'quux'], (new ArrayTransformer)->reverseTransform('foo bar quux')); static::assertSame(['foo', 'bar', 'quux'], (new ArrayTransformer)->reverseTransform('foo bar quux'));
static::assertThrows(TransformationFailedException::class, fn () => (new ArrayTransformer)->reverseTransform(1)); static::assertThrows(TransformationFailedException::class, fn () => (new ArrayTransformer)->reverseTransform(1)); // @phpstan-ignore-line
} }
} }

View File

@ -170,7 +170,7 @@ class FormattingTest extends WebTestCase
static::assertSame(' foo', Formatting::indent('foo', level: 1, count: 2)); static::assertSame(' foo', Formatting::indent('foo', level: 1, count: 2));
static::assertSame(" foo\n bar", Formatting::indent("foo\nbar")); static::assertSame(" foo\n bar", Formatting::indent("foo\nbar"));
static::assertSame(" foo\n bar", Formatting::indent(['foo', 'bar'])); static::assertSame(" foo\n bar", Formatting::indent(['foo', 'bar']));
static::assertThrows(InvalidArgumentException::class, fn () => Formatting::indent(1)); static::assertThrows(InvalidArgumentException::class, fn () => Formatting::indent(1)); // @phpstan-ignore-line
} }
public function testToString() public function testToString()
@ -185,7 +185,7 @@ class FormattingTest extends WebTestCase
public function testToArray() public function testToArray()
{ {
static::assertThrows(Exception::class, fn () => Formatting::toArray('foo', $a, '')); static::assertThrows(Exception::class, fn () => Formatting::toArray('foo', $a, '')); // @phpstan-ignore-line
static::assertTrue(Formatting::toArray('', $a)); static::assertTrue(Formatting::toArray('', $a));
static::assertSame([], $a); static::assertSame([], $a);

View File

@ -41,7 +41,7 @@ class HTMLTest extends WebTestCase
static::assertSame('<a><p>foo</p><br></a>', HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']])); static::assertSame('<a><p>foo</p><br></a>', HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']]));
static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]])); static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
static::assertSame('<div><a><p>foo</p><br></a></div>', HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false])); static::assertSame('<div><a><p>foo</p><br></a></div>', HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
static::assertThrows(TypeError::class, fn () => HTML::html(1)); static::assertThrows(TypeError::class, fn () => HTML::html(1)); // @phpstan-ignore-line
static::assertSame('<a href="test">foo</a>', HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false])); static::assertSame('<a href="test">foo</a>', HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
static::assertSame('<br>', HTML::tag('br', attrs: null, content: null, options: ['empty' => true])); static::assertSame('<br>', HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
} }