diff --git a/.github/workflows/test-old.yaml b/.github/workflows/test-old.yaml index c4f2328..c860a46 100644 --- a/.github/workflows/test-old.yaml +++ b/.github/workflows/test-old.yaml @@ -15,7 +15,7 @@ jobs: os: ['ubuntu-latest'] steps: - name: 'Checkout' - uses: 'actions/checkout@v2' + uses: 'actions/checkout@v4' - name: 'Install PHP' uses: 'shivammathur/setup-php@v2' with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3b261d3..cb0fdde 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,14 +11,15 @@ jobs: runs-on: '${{ matrix.os }}' strategy: matrix: - php: ['7.4', '8.0', '8.1', '8.2'] + php: ['7.4', '8.0', '8.1', '8.2', '8.3'] os: ['ubuntu-latest'] failure: [false] include: - - { php: '8.3', os: 'ubuntu-latest', failure: true } # '8.3' means 'nightly' + - { php: '8.4', os: 'ubuntu-latest', failure: true } # Psalm does not support PHP 8.4 yet + - { php: '8.5', os: 'ubuntu-latest', failure: true } # '8.5' means 'nightly' steps: - name: 'Checkout' - uses: 'actions/checkout@v2' + uses: 'actions/checkout@v4' - name: 'Install PHP' uses: 'shivammathur/setup-php@v2' with: @@ -36,11 +37,13 @@ jobs: continue-on-error: '${{ matrix.failure }}' - name: 'Psalm' run: | - composer require --dev vimeo/psalm + composer remove --dev -W 'phpunit/phpunit' + composer require --dev -W 'vimeo/psalm=^5.0' 'nikic/php-parser=^4.0' php vendor/bin/psalm --shepherd --php-version=${{ matrix.php }} continue-on-error: '${{ matrix.failure }}' - name: 'Infection' run: | - composer require --dev --with-all-dependencies infection/infection + composer remove --dev -W 'vimeo/psalm' + composer require --dev -W phpunit/phpunit infection/infection php vendor/bin/infection continue-on-error: '${{ matrix.failure }}' diff --git a/LICENSE b/LICENSE index 2a7ec74..3e714cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2023 Tomasz Kowalczyk +Copyright (c) 2015-2025 Tomasz Kowalczyk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 071a377..447949c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Each part is described in the dedicated section in this document. ## Installation -There are no required dependencies and all PHP versions from 5.3 up to latest 8.1 [are tested](https://github.com/thunderer/Shortcode/actions/workflows/test.yaml) and supported. This library is available on Composer/Packagist as `thunderer/shortcode`, to install it execute: +There are no required dependencies and all PHP versions from 5.3 up to latest 8.x [are tested](https://github.com/thunderer/Shortcode/actions/workflows/test.yaml) and supported. This library is available on Composer/Packagist as `thunderer/shortcode`, to install it execute: ``` composer require thunderer/shortcode=^0.7 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f7c807d..6620361 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,18 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"> + + + + src + + @@ -19,9 +21,6 @@ - - src - diff --git a/src/Handler/EmailHandler.php b/src/Handler/EmailHandler.php index 0312e9b..eeda984 100644 --- a/src/Handler/EmailHandler.php +++ b/src/Handler/EmailHandler.php @@ -19,7 +19,7 @@ final class EmailHandler */ public function __invoke(ShortcodeInterface $shortcode) { - $email = $shortcode->getBbCode() ?: $shortcode->getContent(); + $email = null !== $shortcode->getBbCode() ? $shortcode->getBbCode() : $shortcode->getContent(); $content = $shortcode->getContent() === null ? $email : $shortcode->getContent(); return ''.(string)$content.''; diff --git a/src/Handler/UrlHandler.php b/src/Handler/UrlHandler.php index 27d02ce..032a848 100644 --- a/src/Handler/UrlHandler.php +++ b/src/Handler/UrlHandler.php @@ -19,7 +19,7 @@ final class UrlHandler */ public function __invoke(ShortcodeInterface $shortcode) { - $url = $shortcode->getBbCode() ?: $shortcode->getContent(); + $url = null !== $shortcode->getBbCode() ? $shortcode->getBbCode() : $shortcode->getContent(); return ''.(string)$shortcode->getContent().''; } diff --git a/src/Parser/RegexParser.php b/src/Parser/RegexParser.php index 4a83b39..3346680 100644 --- a/src/Parser/RegexParser.php +++ b/src/Parser/RegexParser.php @@ -14,11 +14,11 @@ final class RegexParser implements ParserInterface { /** @var SyntaxInterface */ private $syntax; - /** @var string */ + /** @var non-empty-string */ private $shortcodeRegex; - /** @var string */ + /** @var non-empty-string */ private $singleShortcodeRegex; - /** @var string */ + /** @var non-empty-string */ private $parametersRegex; public function __construct(SyntaxInterface $syntax = null) diff --git a/src/Parser/RegularParser.php b/src/Parser/RegularParser.php index bfddc2c..ca85ecc 100644 --- a/src/Parser/RegularParser.php +++ b/src/Parser/RegularParser.php @@ -12,9 +12,9 @@ */ final class RegularParser implements ParserInterface { - /** @var string */ + /** @var non-empty-string */ private $lexerRegex; - /** @var string */ + /** @var non-empty-string */ private $nameRegex; /** @psalm-var list */ private $tokens = array(); @@ -184,6 +184,7 @@ private function shortcode(array &$names) return array_merge(array($this->getObject($name, $parameters, $bbCode, $offset, null, $text)), $shortcodes); } $content = $this->getBacktrack(); + /** @psalm-suppress RiskyTruthyFalsyComparison */ if(!$this->close($names)) { return false; } array_pop($names); @@ -314,6 +315,7 @@ private function match($type, $ws) } $token = $this->tokens[$this->position]; + /** @psalm-suppress RiskyTruthyFalsyComparison */ if(!empty($type) && $token[0] !== $type) { return ''; } @@ -352,7 +354,7 @@ private function tokenize($text) case -1 !== $match['separator'][1]: { $token = $match['separator'][0]; $type = self::TOKEN_SEPARATOR; break; } case -1 !== $match['open'][1]: { $token = $match['open'][0]; $type = self::TOKEN_OPEN; break; } case -1 !== $match['close'][1]: { $token = $match['close'][0]; $type = self::TOKEN_CLOSE; break; } - default: { throw new \RuntimeException(sprintf('Invalid token.')); } + default: { throw new \RuntimeException('Invalid token.'); } } $tokens[] = array($type, $token, $position); $position += mb_strlen($token, 'utf-8'); @@ -361,7 +363,7 @@ private function tokenize($text) return $tokens; } - /** @return string */ + /** @return non-empty-string */ private function prepareLexer(SyntaxInterface $syntax) { // FIXME: for some reason Psalm does not understand the `@psalm-var callable() $var` annotation diff --git a/src/Parser/WordpressParser.php b/src/Parser/WordpressParser.php index 03a4611..07eb872 100644 --- a/src/Parser/WordpressParser.php +++ b/src/Parser/WordpressParser.php @@ -21,14 +21,15 @@ * * @see https://core.trac.wordpress.org/browser/tags/4.3.1/src/wp-includes/shortcodes.php#L239 * @see https://core.trac.wordpress.org/browser/tags/4.3.1/src/wp-includes/shortcodes.php#L448 + * @psalm-suppress RiskyTruthyFalsyComparison * * @author Tomasz Kowalczyk */ final class WordpressParser implements ParserInterface { - /** @var string */ + /** @var non-empty-string */ private static $shortcodeRegex = '/\\[(\\[?)()(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*+(?:\\[(?!\\/\\2\\])[^\\[]*+)*+)\\[\\/\\2\\])?)(\\]?)/s'; - /** @var string */ + /** @var non-empty-string */ private static $argumentsRegex = '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/'; /** @var string[] */ @@ -74,6 +75,7 @@ public function parse($text) $names = $this->names ? implode('|', array_map(function($name) { return preg_quote($name, '/'); }, $this->names)) : RegexBuilderUtility::buildNameRegex(); + /** @var non-empty-string $regex */ $regex = str_replace('', $names, static::$shortcodeRegex); preg_match_all($regex, $text, $matches, PREG_OFFSET_CAPTURE); diff --git a/src/Processor/Processor.php b/src/Processor/Processor.php index e3e5745..af68cab 100644 --- a/src/Processor/Processor.php +++ b/src/Processor/Processor.php @@ -157,7 +157,7 @@ private function processHandler(ParsedShortcodeInterface $parsed, ProcessorConte $content = $this->processRecursion($processed, $context); $processed = $processed->withContent($content); - if($handler) { + if(null !== $handler) { return $handler($processed); } diff --git a/src/Serializer/XmlSerializer.php b/src/Serializer/XmlSerializer.php index a4e67f7..3d45742 100644 --- a/src/Serializer/XmlSerializer.php +++ b/src/Serializer/XmlSerializer.php @@ -133,7 +133,7 @@ private function getAttribute(\DOMNode $node, $name) */ $attribute = $node->attributes->getNamedItem($name); - /** @psalm-suppress DocblockTypeContradiction */ + /** @psalm-suppress DocblockTypeContradiction,RiskyTruthyFalsyComparison */ if(!$attribute || !$attribute->nodeValue) { throw new \InvalidArgumentException('Invalid shortcode XML!'); } diff --git a/src/Syntax/CommonSyntax.php b/src/Syntax/CommonSyntax.php index f3bb64b..1e79e85 100644 --- a/src/Syntax/CommonSyntax.php +++ b/src/Syntax/CommonSyntax.php @@ -6,31 +6,31 @@ */ final class CommonSyntax implements SyntaxInterface { - /** @return string */ + /** @return non-empty-string */ public function getOpeningTag() { return '['; } - /** @return string */ + /** @return non-empty-string */ public function getClosingTag() { return ']'; } - /** @return string */ + /** @return non-empty-string */ public function getClosingTagMarker() { return '/'; } - /** @return string */ + /** @return non-empty-string */ public function getParameterValueSeparator() { return '='; } - /** @return string */ + /** @return non-empty-string */ public function getParameterValueDelimiter() { return '"'; diff --git a/src/Syntax/Syntax.php b/src/Syntax/Syntax.php index 0b8b6cd..6797d72 100644 --- a/src/Syntax/Syntax.php +++ b/src/Syntax/Syntax.php @@ -6,23 +6,23 @@ */ final class Syntax implements SyntaxInterface { - /** @var string|null */ + /** @var non-empty-string|null */ private $openingTag; - /** @var string|null */ + /** @var non-empty-string|null */ private $closingTag; - /** @var string|null */ + /** @var non-empty-string|null */ private $closingTagMarker; - /** @var string|null */ + /** @var non-empty-string|null */ private $parameterValueSeparator; - /** @var string|null */ + /** @var non-empty-string|null */ private $parameterValueDelimiter; /** - * @param string|null $openingTag - * @param string|null $closingTag - * @param string|null $closingTagMarker - * @param string|null $parameterValueSeparator - * @param string|null $parameterValueDelimiter + * @param non-empty-string|null $openingTag + * @param non-empty-string|null $closingTag + * @param non-empty-string|null $closingTagMarker + * @param non-empty-string|null $parameterValueSeparator + * @param non-empty-string|null $parameterValueDelimiter */ public function __construct( $openingTag = null, @@ -38,33 +38,33 @@ public function __construct( $this->parameterValueDelimiter = $parameterValueDelimiter; } - /** @return string */ + /** @return non-empty-string */ public function getOpeningTag() { - return $this->openingTag ?: '['; + return null !== $this->openingTag ? $this->openingTag : '['; } - /** @return string */ + /** @return non-empty-string */ public function getClosingTag() { - return $this->closingTag ?: ']'; + return null !== $this->closingTag ? $this->closingTag : ']'; } - /** @return string */ + /** @return non-empty-string */ public function getClosingTagMarker() { - return $this->closingTagMarker ?: '/'; + return null !== $this->closingTagMarker ? $this->closingTagMarker : '/'; } - /** @return string */ + /** @return non-empty-string */ public function getParameterValueSeparator() { - return $this->parameterValueSeparator ?: '='; + return null !== $this->parameterValueSeparator ? $this->parameterValueSeparator : '='; } - /** @return string */ + /** @return non-empty-string */ public function getParameterValueDelimiter() { - return $this->parameterValueDelimiter ?: '"'; + return null !== $this->parameterValueDelimiter ? $this->parameterValueDelimiter : '"'; } } diff --git a/src/Syntax/SyntaxBuilder.php b/src/Syntax/SyntaxBuilder.php index 15437fb..12d4f2a 100644 --- a/src/Syntax/SyntaxBuilder.php +++ b/src/Syntax/SyntaxBuilder.php @@ -6,15 +6,15 @@ */ final class SyntaxBuilder { - /** @var string|null */ + /** @var non-empty-string|null */ private $openingTag; - /** @var string|null */ + /** @var non-empty-string|null */ private $closingTag; - /** @var string|null */ + /** @var non-empty-string|null */ private $closingTagMarker; - /** @var string|null */ + /** @var non-empty-string|null */ private $parameterValueSeparator; - /** @var string|null */ + /** @var non-empty-string|null */ private $parameterValueDelimiter; public function __construct() @@ -34,7 +34,7 @@ public function getSyntax() } /** - * @param string $tag + * @param non-empty-string $tag * * @return $this */ @@ -46,7 +46,7 @@ public function setOpeningTag($tag) } /** - * @param string $tag + * @param non-empty-string $tag * * @return $this */ @@ -58,7 +58,7 @@ public function setClosingTag($tag) } /** - * @param string $marker + * @param non-empty-string $marker * * @return $this */ @@ -70,7 +70,7 @@ public function setClosingTagMarker($marker) } /** - * @param string $separator + * @param non-empty-string $separator * * @return $this */ @@ -82,7 +82,7 @@ public function setParameterValueSeparator($separator) } /** - * @param string $delimiter + * @param non-empty-string $delimiter * * @return $this */ diff --git a/src/Syntax/SyntaxInterface.php b/src/Syntax/SyntaxInterface.php index 3f352c6..b18516f 100644 --- a/src/Syntax/SyntaxInterface.php +++ b/src/Syntax/SyntaxInterface.php @@ -6,18 +6,18 @@ */ interface SyntaxInterface { - /** @return string */ + /** @return non-empty-string */ public function getOpeningTag(); - /** @return string */ + /** @return non-empty-string */ public function getClosingTag(); - /** @return string */ + /** @return non-empty-string */ public function getClosingTagMarker(); - /** @return string */ + /** @return non-empty-string */ public function getParameterValueSeparator(); - /** @return string */ + /** @return non-empty-string */ public function getParameterValueDelimiter(); } diff --git a/src/Utility/RegexBuilderUtility.php b/src/Utility/RegexBuilderUtility.php index 847ff38..3184df1 100644 --- a/src/Utility/RegexBuilderUtility.php +++ b/src/Utility/RegexBuilderUtility.php @@ -8,25 +8,25 @@ */ final class RegexBuilderUtility { - /** @return string */ + /** @return non-empty-string */ public static function buildNameRegex() { return '[a-zA-Z0-9-_\\*]+'; } - /** @return string */ + /** @return non-empty-string */ public static function buildShortcodeRegex(SyntaxInterface $syntax) { return '~('.self::createShortcodeRegexContent($syntax).')~us'; } - /** @return string */ + /** @return non-empty-string */ public static function buildSingleShortcodeRegex(SyntaxInterface $syntax) { return '~(\A'.self::createShortcodeRegexContent($syntax).'\Z)~us'; } - /** @return string */ + /** @return non-empty-string */ public static function buildParametersRegex(SyntaxInterface $syntax) { $equals = self::quote($syntax->getParameterValueSeparator()); @@ -43,7 +43,7 @@ public static function buildParametersRegex(SyntaxInterface $syntax) return '~(?:\s*(\w+(?:'.$complex.'|'.$simple.'|'.$empty.')))~us'; } - /** @return string */ + /** @return non-empty-string */ private static function createShortcodeRegexContent(SyntaxInterface $syntax) { $open = self::quote($syntax->getOpeningTag()); @@ -84,12 +84,15 @@ private static function createShortcodeRegexContent(SyntaxInterface $syntax) } /** - * @param string $text + * @param non-empty-string $text * - * @return string + * @return non-empty-string */ private static function quote($text) { - return preg_replace('/(.)/us', '\\\\$0', $text); + /** @var non-empty-string $quoted */ + $quoted = preg_replace('/(.)/us', '\\\\$0', $text); + + return $quoted; } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index a4b5df9..63bfcb9 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -42,7 +42,7 @@ private function assertShortcodes(array $actual, array $expected) } } - public function provideShortcodes() + public static function provideShortcodes() { $s = new CommonSyntax(); diff --git a/tests/ProcessorTest.php b/tests/ProcessorTest.php index ca116d1..2d5b96b 100644 --- a/tests/ProcessorTest.php +++ b/tests/ProcessorTest.php @@ -69,7 +69,7 @@ public function testProcessorProcess($text, $result) static::assertSame($result, $processor->process($text)); } - public function provideTexts() + public static function provideTexts() { return array( array('[name]', 'name'), @@ -185,7 +185,7 @@ public function testBuiltInHandlers($text, $result) static::assertSame($result, $processor->process($text)); } - public function provideBuiltInTests() + public static function provideBuiltInTests() { return array( array('[declare date]%year%.%month%.%day%[/declare][date year=2015 month=08 day=26]', '2015.08.26'), diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index a25006e..4165086 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -29,7 +29,7 @@ public function testSerializer(SerializerInterface $serializer, ShortcodeInterfa static::assertSame($test->getBbCode(), $tested->getBbCode(), 'bbCode: '.$result); } - public function provideShortcodes() + public static function provideShortcodes() { $shortcodes = array( new Shortcode('x', array(), null), @@ -71,7 +71,7 @@ public function testUnserialize(SerializerInterface $serializer, ShortcodeInterf static::assertSame($test->getBbCode(), $tested->getBbCode(), 'bbCode: '.$text); } - public function provideUnserialized() + public static function provideUnserialized() { return array( array(new JsonSerializer(), new Shortcode('x', array(), null), '{"name":"x"}'), @@ -95,7 +95,7 @@ public function testSerializerExceptions(SerializerInterface $serializer, $value $serializer->unserialize($value); } - public function provideExceptions() + public static function provideExceptions() { $xml = new XmlSerializer(); $yaml = new YamlSerializer(); diff --git a/tests/ShortcodeTest.php b/tests/ShortcodeTest.php index 05f1b50..b790aa7 100644 --- a/tests/ShortcodeTest.php +++ b/tests/ShortcodeTest.php @@ -31,7 +31,7 @@ public function testShortcode($expected, $name, array $args, $content) static::assertTrue($s->hasParameters()); } - public function provideShortcodes() + public static function provideShortcodes() { return array( array('[x arg=val /]', 'x', array('arg' => 'val'), null), diff --git a/tests/SyntaxTest.php b/tests/SyntaxTest.php index 9228d46..7bc09d3 100644 --- a/tests/SyntaxTest.php +++ b/tests/SyntaxTest.php @@ -23,7 +23,7 @@ public function testSyntax(SyntaxInterface $syntax, $open, $close, $slash, $para static::assertSame($value, $syntax->getParameterValueDelimiter()); } - public function provideSyntaxes() + public static function provideSyntaxes() { return array( array(new Syntax(), '[', ']', '/', '=', '"'),